diff --git a/shared/src/api/generated/graphql.ts b/shared/src/api/generated/graphql.ts index 6bf0e35ee..fb8749338 100644 --- a/shared/src/api/generated/graphql.ts +++ b/shared/src/api/generated/graphql.ts @@ -1741,7 +1741,7 @@ export type StatsOperation = | 'PERCENTAGE_NOT_FILLED' | 'SUM'; -export type ActivityFragmentFragment = { activityId: string, activityType: string, activityData: string, referenceType: string, referenceId: string, entityId: string | null, body: string, createdAt: unknown, updatedAt: unknown, author: { name: string, deleted: boolean, active: boolean, attrib: { fullName: string | null } } | null, files: Array<{ id: string, name: string | null, size: string, mime: string | null }>, origin: { id: string, name: string, label: string | null, type: string } | null, reactions: Array<{ fullName: string | null, userName: string, reaction: string, timestamp: unknown }>, version: { thumbnailHash: string, attrib: { comment: string | null } } | null }; +export type ActivityFragmentFragment = { activityId: string, activityType: string, activityData: string, referenceType: string, referenceId: string, entityId: string | null, body: string, createdAt: unknown, updatedAt: unknown, author: { name: string, deleted: boolean, active: boolean, attrib: { fullName: string | null } } | null, files: Array<{ id: string, name: string | null, size: string, mime: string | null }>, origin: { id: string, name: string, label: string | null, type: string } | null, reactions: Array<{ fullName: string | null, userName: string, reaction: string, timestamp: unknown }>, version: { thumbnailHash: string, status: string, attrib: { comment: string | null } } | null }; export type GetActivitiesByIdQueryVariables = Exact<{ projectName: string; @@ -1750,7 +1750,7 @@ export type GetActivitiesByIdQueryVariables = Exact<{ }>; -export type GetActivitiesByIdQuery = { project: { name: string, activities: { pageInfo: { hasPreviousPage: boolean, hasNextPage: boolean, startCursor: string | null, endCursor: string | null }, edges: Array<{ cursor: string | null, node: { activityId: string, activityType: string, activityData: string, referenceType: string, referenceId: string, entityId: string | null, body: string, createdAt: unknown, updatedAt: unknown, author: { name: string, deleted: boolean, active: boolean, attrib: { fullName: string | null } } | null, files: Array<{ id: string, name: string | null, size: string, mime: string | null }>, origin: { id: string, name: string, label: string | null, type: string } | null, reactions: Array<{ fullName: string | null, userName: string, reaction: string, timestamp: unknown }>, version: { thumbnailHash: string, attrib: { comment: string | null } } | null } }> } } }; +export type GetActivitiesByIdQuery = { project: { name: string, activities: { pageInfo: { hasPreviousPage: boolean, hasNextPage: boolean, startCursor: string | null, endCursor: string | null }, edges: Array<{ cursor: string | null, node: { activityId: string, activityType: string, activityData: string, referenceType: string, referenceId: string, entityId: string | null, body: string, createdAt: unknown, updatedAt: unknown, author: { name: string, deleted: boolean, active: boolean, attrib: { fullName: string | null } } | null, files: Array<{ id: string, name: string | null, size: string, mime: string | null }>, origin: { id: string, name: string, label: string | null, type: string } | null, reactions: Array<{ fullName: string | null, userName: string, reaction: string, timestamp: unknown }>, version: { thumbnailHash: string, status: string, attrib: { comment: string | null } } | null } }> } } }; export type GetActivityUsersQueryVariables = Exact<{ projects?: Array | string | null | undefined; @@ -1773,7 +1773,7 @@ export type GetActivitiesQueryVariables = Exact<{ }>; -export type GetActivitiesQuery = { project: { name: string, activities: { pageInfo: { hasPreviousPage: boolean, hasNextPage: boolean, startCursor: string | null, endCursor: string | null }, edges: Array<{ cursor: string | null, node: { activityId: string, activityType: string, activityData: string, referenceType: string, referenceId: string, entityId: string | null, body: string, createdAt: unknown, updatedAt: unknown, author: { name: string, deleted: boolean, active: boolean, attrib: { fullName: string | null } } | null, files: Array<{ id: string, name: string | null, size: string, mime: string | null }>, origin: { id: string, name: string, label: string | null, type: string } | null, reactions: Array<{ fullName: string | null, userName: string, reaction: string, timestamp: unknown }>, version: { thumbnailHash: string, attrib: { comment: string | null } } | null } }> } } }; +export type GetActivitiesQuery = { project: { name: string, activities: { pageInfo: { hasPreviousPage: boolean, hasNextPage: boolean, startCursor: string | null, endCursor: string | null }, edges: Array<{ cursor: string | null, node: { activityId: string, activityType: string, activityData: string, referenceType: string, referenceId: string, entityId: string | null, body: string, createdAt: unknown, updatedAt: unknown, author: { name: string, deleted: boolean, active: boolean, attrib: { fullName: string | null } } | null, files: Array<{ id: string, name: string | null, size: string, mime: string | null }>, origin: { id: string, name: string, label: string | null, type: string } | null, reactions: Array<{ fullName: string | null, userName: string, reaction: string, timestamp: unknown }>, version: { thumbnailHash: string, status: string, attrib: { comment: string | null } } | null } }> } } }; export type GetEntitiesChecklistsQueryVariables = Exact<{ projectName: string; @@ -1957,7 +1957,7 @@ export type GetTasksByParentQueryVariables = Exact<{ }>; -export type GetTasksByParentQuery = { project: { name: string, tasks: { edges: Array<{ node: { id: string, folderId: string, label: string | null, name: string, ownAttrib: Array, status: string, tags: Array, taskType: string, updatedAt: unknown, createdAt: unknown, thumbnailHash: string, active: boolean, assignees: Array, allAttrib: string, hasReviewables: boolean, parents: Array, subtasks: Array<{ id: string, name: string, label: string, assignees: Array, description: string | null, startDate: unknown, endDate: unknown, isDone: boolean }>, latestComments?: Array<{ activityId: string, body: string, author: string | null, createdAt: string }> | null, folder: { folderType: string, taskCount: number } } }>, pageInfo: { hasNextPage: boolean, endCursor: string | null } } } }; +export type GetTasksByParentQuery = { project: { name: string, tasks: { edges: Array<{ node: { id: string, folderId: string, label: string | null, name: string, ownAttrib: Array, status: string, tags: Array, taskType: string, updatedAt: unknown, createdAt: unknown, thumbnailHash: string, active: boolean, assignees: Array, allAttrib: string, hasReviewables: boolean, parents: Array, subtasks: Array<{ id: string, name: string, label: string, assignees: Array, description: string | null, startDate: unknown, endDate: unknown, isDone: boolean }>, latestComments?: Array<{ activityId: string, body: string, author: string | null, createdAt: string }> | null, folder: { folderType: string } } }>, pageInfo: { hasNextPage: boolean, endCursor: string | null } } } }; export type GetTasksListQueryVariables = Exact<{ projectName: string; @@ -1976,11 +1976,11 @@ export type GetTasksListQueryVariables = Exact<{ }>; -export type GetTasksListQuery = { project: { name: string, tasks: { pageInfo: { startCursor: string | null, endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean }, edges: Array<{ cursor: string | null, node: { id: string, folderId: string, label: string | null, name: string, ownAttrib: Array, status: string, tags: Array, taskType: string, updatedAt: unknown, createdAt: unknown, thumbnailHash: string, active: boolean, assignees: Array, allAttrib: string, hasReviewables: boolean, parents: Array, subtasks: Array<{ id: string, name: string, label: string, assignees: Array, description: string | null, startDate: unknown, endDate: unknown, isDone: boolean }>, latestComments?: Array<{ activityId: string, body: string, author: string | null, createdAt: string }> | null, folder: { folderType: string, taskCount: number } } }> } } }; +export type GetTasksListQuery = { project: { name: string, tasks: { pageInfo: { startCursor: string | null, endCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean }, edges: Array<{ cursor: string | null, node: { id: string, folderId: string, label: string | null, name: string, ownAttrib: Array, status: string, tags: Array, taskType: string, updatedAt: unknown, createdAt: unknown, thumbnailHash: string, active: boolean, assignees: Array, allAttrib: string, hasReviewables: boolean, parents: Array, subtasks: Array<{ id: string, name: string, label: string, assignees: Array, description: string | null, startDate: unknown, endDate: unknown, isDone: boolean }>, latestComments?: Array<{ activityId: string, body: string, author: string | null, createdAt: string }> | null, folder: { folderType: string } } }> } } }; export type SubTaskFragmentFragment = { id: string, name: string, label: string, assignees: Array, description: string | null, startDate: unknown, endDate: unknown, isDone: boolean }; -export type TaskPropsFragmentFragment = { id: string, folderId: string, label: string | null, name: string, ownAttrib: Array, status: string, tags: Array, taskType: string, updatedAt: unknown, createdAt: unknown, thumbnailHash: string, active: boolean, assignees: Array, allAttrib: string, hasReviewables: boolean, parents: Array, subtasks: Array<{ id: string, name: string, label: string, assignees: Array, description: string | null, startDate: unknown, endDate: unknown, isDone: boolean }>, latestComments?: Array<{ activityId: string, body: string, author: string | null, createdAt: string }> | null, folder: { folderType: string, taskCount: number } }; +export type TaskPropsFragmentFragment = { id: string, folderId: string, label: string | null, name: string, ownAttrib: Array, status: string, tags: Array, taskType: string, updatedAt: unknown, createdAt: unknown, thumbnailHash: string, active: boolean, assignees: Array, allAttrib: string, hasReviewables: boolean, parents: Array, subtasks: Array<{ id: string, name: string, label: string, assignees: Array, description: string | null, startDate: unknown, endDate: unknown, isDone: boolean }>, latestComments?: Array<{ activityId: string, body: string, author: string | null, createdAt: string }> | null, folder: { folderType: string } }; export type GetFolderProductsQueryVariables = Exact<{ projectName: string; @@ -2279,6 +2279,7 @@ export const ActivityFragmentFragmentDoc = new TypedDocumentString(` } version { thumbnailHash + status attrib { comment } @@ -2501,7 +2502,6 @@ export const TaskPropsFragmentFragmentDoc = new TypedDocumentString(` } folder { folderType - taskCount } } fragment SubTaskFragment on SubTaskNode { @@ -2783,6 +2783,7 @@ export const GetActivitiesByIdDocument = new TypedDocumentString(` } version { thumbnailHash + status attrib { comment } @@ -2870,6 +2871,7 @@ export const GetActivitiesDocument = new TypedDocumentString(` } version { thumbnailHash + status attrib { comment } @@ -3525,7 +3527,6 @@ fragment TaskPropsFragment on TaskNode { } folder { folderType - taskCount } }`); export const GetTasksListDocument = new TypedDocumentString(` @@ -3598,7 +3599,6 @@ fragment TaskPropsFragment on TaskNode { } folder { folderType - taskCount } }`); export const GetFolderProductsDocument = new TypedDocumentString(` diff --git a/shared/src/api/queries/activities/getActivities.ts b/shared/src/api/queries/activities/getActivities.ts index 554efa3c5..770261bce 100644 --- a/shared/src/api/queries/activities/getActivities.ts +++ b/shared/src/api/queries/activities/getActivities.ts @@ -6,7 +6,7 @@ import { GetActivityUsersQuery, GetEntitiesChecklistsQuery, } from '@shared/api/generated' -import { taskProvideTags } from './util/activitiesHelpers' +import { provideSharedActivityTags, taskProvideTags } from './util/activitiesHelpers' import { ActivitiesResult, countChecklists, @@ -43,28 +43,7 @@ const enhanceActivitiesApi = gqlApi.enhanceEndpoints transformActivityData(res.project.activities.edges, res.project.activities.pageInfo), - // @ts-expect-error - filter is not an query arg - providesTags: (result, _e, { entityIds, activityTypes = [], filter }) => - result - ? [ - ...result.activities.map((a) => ({ type: 'activity', id: a.activityId })), - { type: 'activity', id: 'LIST' }, - ...(Array.isArray(entityIds) ? entityIds : [entityIds]).map((id) => ({ - type: 'entityActivities', - id: id, - })), - { type: 'entityActivities', id: 'LIST' }, - ...(Array.isArray(activityTypes) ? activityTypes : [activityTypes])?.map((type) => ({ - type: 'entityActivities', - id: type as string, - })), - // filter is used when a comment is made, to refetch the activities of other filters - ...(Array.isArray(entityIds) ? entityIds : [entityIds]).map((id) => ({ - type: 'entityActivities', - id: id + '-' + filterKey(filter), - })), - ] - : [{ type: 'activity', id: 'LIST' }], + providesTags: provideSharedActivityTags, }, GetActivitiesById: { transformResponse: (res: GetActivitiesByIdQuery) => @@ -94,13 +73,18 @@ const enhanceActivitiesApi = gqlApi.enhanceEndpoints & { filter?: any } + const ACTIVITIES_INFINITE_QUERY_COUNT = 30 const getActivitiesGQLApi = enhanceActivitiesApi.injectEndpoints({ endpoints: (build) => ({ getActivitiesInfinite: build.infiniteQuery< ActivitiesResult, - Omit & { filter?: any }, + GetActivitiesInfiniteQueryArgs, { cursor: string; first?: number; last?: number } >({ infiniteQueryOptions: { @@ -168,31 +152,7 @@ const getActivitiesGQLApi = enhanceActivitiesApi.injectEndpoints({ gqlApi: enhanceActivitiesApi, }) }, - providesTags: (result, _e, { entityIds, activityTypes, filter }) => - result - ? [ - ...result.pages - .flatMap((page) => page.activities) - .map((a) => ({ type: 'activity', id: a.activityId })), - { type: 'activity', id: 'LIST' }, - ...(Array.isArray(entityIds) ? entityIds : [entityIds]).filter(Boolean).map((id) => ({ - type: 'entityActivities', - id: id as string, - })), - { type: 'entityActivities', id: 'LIST' }, - ...(Array.isArray(activityTypes) ? activityTypes : [activityTypes]) - ?.filter(Boolean) - .map((type) => ({ - type: 'entityActivities', - id: type as string, - })), - // filter is used when a comment is made, to refetch the activities of other filters - ...(Array.isArray(entityIds) ? entityIds : [entityIds]).filter(Boolean).map((id) => ({ - type: 'entityActivities', - id: `${id}-${filterKey(filter)}`, - })), - ] - : [{ type: 'activity', id: 'LIST' }], +providesTags: provideSharedActivityTags, }), // get data for a reference tooltip based on type,id and projectName getEntityTooltip: build.query({ diff --git a/shared/src/api/queries/activities/gql/ActivityFragment.graphql b/shared/src/api/queries/activities/gql/ActivityFragment.graphql index 7efd34b38..15bfa96a5 100644 --- a/shared/src/api/queries/activities/gql/ActivityFragment.graphql +++ b/shared/src/api/queries/activities/gql/ActivityFragment.graphql @@ -36,6 +36,7 @@ fragment ActivityFragment on ActivityNode { } version { thumbnailHash + status attrib { comment } diff --git a/shared/src/api/queries/activities/util/activitiesHelpers.ts b/shared/src/api/queries/activities/util/activitiesHelpers.ts index 435cb5020..46dfb84de 100644 --- a/shared/src/api/queries/activities/util/activitiesHelpers.ts +++ b/shared/src/api/queries/activities/util/activitiesHelpers.ts @@ -230,6 +230,64 @@ export const filterKey = (filter: any): string => { } } +/** + * Shared helper to generate activity tags for both standard and infinite queries. + */ +export const provideSharedActivityTags = ( + result: { activities?: any[]; pages?: Array<{ activities: any[] }> } | undefined, + _error: unknown, + // Widened type signatures to allow string[] | string | null | undefined + arg: { + entityIds?: string | string[] | null + activityTypes?: string | string[] | null + filter?: any + }, +) => { + if (!result) { + return [{ type: 'activity' as const, id: 'LIST' }] + } + + const { entityIds, activityTypes, filter } = arg + + // Helper now safely skips null/undefined values + const normalizeIds = (ids: unknown): string[] => + (Array.isArray(ids) ? ids : [ids]).filter(Boolean) as string[] + + const cleanEntityIds = normalizeIds(entityIds) + const cleanActivityTypes = normalizeIds(activityTypes) + + const activities = result.pages + ? result.pages.flatMap((page) => page.activities) + : result.activities || [] + + return [ + // Individual Activity Tags + ...activities.map((a) => ({ type: 'activity' as const, id: a.activityId })), + { type: 'activity' as const, id: 'LIST' }, + + // Base Entity Activity Tags + ...cleanEntityIds.map((id) => ({ type: 'entityActivities' as const, id })), + { type: 'entityActivities' as const, id: 'LIST' }, + + // Activity Type Tags + ...cleanActivityTypes.map((type) => ({ type: 'entityActivities' as const, id: type })), + + // Filtered Entity Activity Tags + ...cleanEntityIds.map((id) => ({ + type: 'entityActivities' as const, + id: `${id}-${filterKey(filter)}`, + })), + + // Published Version Tags + ...activities + .filter((a) => a.activityType === 'version.publish') + .map((a) => ({ + type: 'entityActivities' as const, + id: `version.publish-${a.origin.id}`, + })), + ] +} + export const filterActivityTypes: Record = { activity: [ 'comment', diff --git a/shared/src/api/queries/activities/util/activityRealtimeHandler.ts b/shared/src/api/queries/activities/util/activityRealtimeHandler.ts index 36d07a6a0..79ad1c5f2 100644 --- a/shared/src/api/queries/activities/util/activityRealtimeHandler.ts +++ b/shared/src/api/queries/activities/util/activityRealtimeHandler.ts @@ -275,8 +275,30 @@ export const handleActivityRealtimeUpdates = async ( } else { console.warn('[Activity RT] Cannot add activity: no pages or activities array') } - } else if (!existingActivityFound) { - } else { + } + + // If this is a status change, update any version.publish activities in the cache + if ( + newActivity.activityType === 'status.change' && + newActivity.origin?.type === 'version' + ) { + const versionId = newActivity.origin.id + const newStatus = newActivity.activityData?.newValue + if (newStatus) { + draft.pages.forEach((page) => { + page.activities?.forEach((activity) => { + if ( + activity.activityType === 'version.publish' && + activity.origin?.id === versionId + ) { + if (!activity.version) { + activity.version = {} as any + } + activity.version!.status = newStatus + } + }) + }) + } } }) } catch (error) { diff --git a/shared/src/api/queries/entities/updateEntity.ts b/shared/src/api/queries/entities/updateEntity.ts index 5d7941ce4..2472122fd 100644 --- a/shared/src/api/queries/entities/updateEntity.ts +++ b/shared/src/api/queries/entities/updateEntity.ts @@ -369,12 +369,6 @@ const updateEntity = api.injectEndpoints({ throw 'Failed to update some tasks' } - const activityTags = [] - - if (activityTags.length) { - dispatch(api.util.invalidateTags(activityTags)) - } - return { data: operations } } catch (error) { console.error(error) @@ -391,6 +385,13 @@ const updateEntity = api.injectEndpoints({ if (entityType === 'version') { tags.push({ type: 'version', id: 'LIST' }, { type: 'product', id: 'LIST' }) operations.forEach((o) => tags.push({ type: 'version', id: o.id })) + if (operations.some((o) => !!o.data.status)) { + console.log('invalidate version.publish activities') + // invalidate feeds with that version in it (published versions activity) + operations.forEach((o) => + tags.push({ type: 'entityActivities', id: `version.publish-${o.id}` }), + ) + } } return tags diff --git a/shared/src/containers/Feed/components/ActivityItem.tsx b/shared/src/containers/Feed/components/ActivityItem.tsx index a96ab7237..c1c175cc9 100644 --- a/shared/src/containers/Feed/components/ActivityItem.tsx +++ b/shared/src/containers/Feed/components/ActivityItem.tsx @@ -63,7 +63,7 @@ const ActivityItem: React.FC = ({ case 'assignee.remove': return case 'version.publish': - return + return case 'version.review': return case 'group': diff --git a/shared/src/containers/Feed/components/ActivityStatusChange/ActivityStatusChange.tsx b/shared/src/containers/Feed/components/ActivityStatusChange/ActivityStatusChange.tsx index 94bf79ad9..96445a5d5 100644 --- a/shared/src/containers/Feed/components/ActivityStatusChange/ActivityStatusChange.tsx +++ b/shared/src/containers/Feed/components/ActivityStatusChange/ActivityStatusChange.tsx @@ -3,6 +3,7 @@ import * as Styled from './ActivityStatusChange.styled' import ActivityDate from '../ActivityDate' import { Icon } from '@ynput/ayon-react-components' import useGetContextParents from './hooks/getContextParents' +import { ActivityStatusSecondary } from './ActivityStatusSecondary' interface StatusInfo { icon?: string @@ -34,11 +35,17 @@ const ActivityStatusChange: React.FC = ({ {authorFullName || authorName} - {tagList.join(' / ')} - - {oldStatus.icon && } - {oldStatus.name} + - {newStatus.icon && } - {newStatus.name} + diff --git a/shared/src/containers/Feed/components/ActivityStatusChange/ActivityStatusSecondary.tsx b/shared/src/containers/Feed/components/ActivityStatusChange/ActivityStatusSecondary.tsx new file mode 100644 index 000000000..0c86bfeb7 --- /dev/null +++ b/shared/src/containers/Feed/components/ActivityStatusChange/ActivityStatusSecondary.tsx @@ -0,0 +1,32 @@ +import { Icon } from '@ynput/ayon-react-components'; +import { forwardRef } from 'react'; +import styled from 'styled-components'; +import { Body } from './ActivityStatusChange.styled'; + +const StatusText = styled.span` + color: var(--md-sys-color-outline); + white-space: nowrap; + font-size: 12px; + + strong { + font-size: 12px; + font-weight: 800; + } +` + +interface ActivityStatusSecondaryProps extends React.HTMLAttributes { + icon?: string; + color?: string; + name: string; +} + +export const ActivityStatusSecondary = forwardRef(({ +icon, color, name, + ...props }, ref) => { + return ( + + {icon && } + {name} + + ); +}); \ No newline at end of file diff --git a/shared/src/containers/Feed/components/ActivityVersions/ActivityVersions.tsx b/shared/src/containers/Feed/components/ActivityVersions/ActivityVersions.tsx index d4524e262..1d93bf48b 100644 --- a/shared/src/containers/Feed/components/ActivityVersions/ActivityVersions.tsx +++ b/shared/src/containers/Feed/components/ActivityVersions/ActivityVersions.tsx @@ -5,6 +5,8 @@ import * as Styled from './ActivityVersions.styled' import { More } from '../ActivityGroup/ActivityGroup.styled' import ActivityDate from '../ActivityDate' import { useDetailsPanelContext } from '@shared/context' +import { Status } from '@shared/api' +import { ActivityStatusSecondary } from '../ActivityStatusChange/ActivityStatusSecondary' interface Version { name: string @@ -14,6 +16,7 @@ interface Version { updatedAt: string thumbnailHash?: string comment?: string + status: string } interface ActivityVersionsProps { @@ -28,6 +31,7 @@ interface ActivityVersionsProps { entityType?: string onReferenceClick?: (ref: any) => void filter?: any + statuses: Status[] } const ActivityVersions: React.FC = ({ @@ -36,6 +40,7 @@ const ActivityVersions: React.FC = ({ entityType, onReferenceClick, filter, + statuses = [], }) => { const { onOpenViewer } = useDetailsPanelContext() let { authorName, authorFullName, createdAt, versions = [] } = activity @@ -67,6 +72,7 @@ const ActivityVersions: React.FC = ({ /> {versions.flatMap((version, index) => { const { name, id, productId, productName, thumbnailHash, comment } = version + const status = statuses.find((s) => s.name === version.status) return ( (index < limit || showAll) && ( handleClick(id, productId)} key={id}> @@ -76,7 +82,14 @@ const ActivityVersions: React.FC = ({ {productName} - {name} + + {name} - + + { updatedAt, createdAt, comment: version?.attrib?.comment || null, + status: version?.status, thumbnailHash: version?.thumbnailHash, } }