From dac5d5a23ae5e1d25f9157fa74df1045c6362110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Wed, 5 Aug 2026 20:33:33 +0200 Subject: [PATCH] Extend live update coverage: workflow draft updates and side-panel records (#23830) Stacked on #23811. Closes the two remaining live-update coverage gaps found while auditing record-seeded editing surfaces. ## Workflow diagram misses updates to the current draft version `WorkflowSSESubscribeEffect` triggered a content refetch only on `create-one` of a workflow version (new draft created) and on SSE reconnection. Step and trigger edits on the existing draft arrive as `update-one` events and left the open diagram stale until a refresh, which is the common case when the AI chat or a teammate edits a draft workflow. Refetch on `update-one`/`update-many` too. Local workflow mutations do not dispatch these browser events (they only originate from SSE deliveries), and an own-persist echo reseeds the diagram with the state it already shows, so this does not fight local editing. ## Side-panel records receive no SSE events `SidePanelRecordPage` registered no SSE query, so a record opened in the side panel (notes and tasks most commonly) got no events for itself unless another surface happened to subscribe to a matching query. With #23811's rich text adoption this mattered doubly: events could not reach the editor at all. Register the record query like `RecordShowPage` does. The subscribe effect takes a `queryScope` so the record page and side panel keep independent registrations when they display the same record, and closing one surface does not unsubscribe the other. --- _Generated by [Claude Code](https://claude.ai/code/session_018nGvGhFahw1pcefb3P4iCk)_ Review in cubic --- .../RecordShowPageSSESubscribeEffect.tsx | 7 +- .../components/SidePanelRecordPage.tsx | 6 ++ .../components/WorkflowSSESubscribeEffect.tsx | 71 +++++++++++++++++-- 3 files changed, 79 insertions(+), 5 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowPageSSESubscribeEffect.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowPageSSESubscribeEffect.tsx index d374f234fc..88638b328c 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowPageSSESubscribeEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowPageSSESubscribeEffect.tsx @@ -3,13 +3,18 @@ import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEvent type RecordShowPageSSESubscribeEffectProps = { objectNameSingular: string; recordId: string; + // The record page and the side panel can display the same record at the + // same time; distinct scopes keep their query registrations independent so + // closing one surface does not unsubscribe the other. + queryScope?: 'record-show' | 'side-panel-record'; }; export const RecordShowPageSSESubscribeEffect = ({ objectNameSingular, recordId, + queryScope = 'record-show', }: RecordShowPageSSESubscribeEffectProps) => { - const queryId = `record-show-${objectNameSingular}-${recordId}`; + const queryId = `${queryScope}-${objectNameSingular}-${recordId}`; useListenToEventsForQuery({ queryId, diff --git a/packages/twenty-front/src/modules/side-panel/pages/record-page/components/SidePanelRecordPage.tsx b/packages/twenty-front/src/modules/side-panel/pages/record-page/components/SidePanelRecordPage.tsx index 18c85e3388..eb16ca6b30 100644 --- a/packages/twenty-front/src/modules/side-panel/pages/record-page/components/SidePanelRecordPage.tsx +++ b/packages/twenty-front/src/modules/side-panel/pages/record-page/components/SidePanelRecordPage.tsx @@ -7,6 +7,7 @@ import { ContextStoreComponentInstanceContext } from '@/context-store/states/con import { INFORMATION_BANNER_HEIGHT } from '@/information-banner/constants/InformationBannerHeight'; import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper'; import { PageLayoutRecordPageRenderer } from '@/object-record/record-show/components/PageLayoutRecordPageRenderer'; +import { RecordShowPageSSESubscribeEffect } from '@/object-record/record-show/components/RecordShowPageSSESubscribeEffect'; import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { useComponentInstanceStateContext } from '@/ui/utilities/state/component-state/hooks/useComponentInstanceStateContext'; @@ -88,6 +89,11 @@ export const SidePanelRecordPage = () => { }} isInSidePanel /> + diff --git a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowSSESubscribeEffect.tsx b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowSSESubscribeEffect.tsx index 4b0d969c75..6a9500d7a8 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowSSESubscribeEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-diagram/components/WorkflowSSESubscribeEffect.tsx @@ -1,11 +1,14 @@ import { useCallback } from 'react'; -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { CoreObjectNameSingular } from 'twenty-shared/types'; import { useListenToObjectRecordOperationBrowserEvent } from '@/browser-event/hooks/useListenToObjectRecordOperationBrowserEvent'; +import { type ObjectRecordOperationBrowserEventDetail } from '@/browser-event/types/ObjectRecordOperationBrowserEventDetail'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery'; import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState'; import { shouldWorkflowRefetchRequestFamilyState } from '@/workflow/states/shouldWorkflowRefetchRequestFamilyState'; +import { CoreObjectNameSingular } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; export const WorkflowSSESubscribeEffect = ({ workflowId, @@ -28,6 +31,23 @@ export const WorkflowSSESubscribeEffect = ({ setShouldWorkflowRefetchRequest(true); }, [setShouldWorkflowRefetchRequest]); + // Subset of the workflow query the page already runs, so the version ids + // are served from the Apollo cache without an extra request. + const { record: workflowWithVersionIds } = useFindOneRecord<{ + __typename: string; + id: string; + versions: Array<{ id: string }>; + }>({ + objectNameSingular: CoreObjectNameSingular.Workflow, + objectRecordId: workflowId, + recordGqlFields: { + id: true, + versions: { + id: true, + }, + }, + }); + useListenToEventsForQuery({ queryId, operationSignature: { @@ -41,10 +61,53 @@ export const WorkflowSSESubscribeEffect = ({ onSseReconnected: requestWorkflowRefetch, }); + // Creations cover new draft versions; updates cover step and trigger edits + // on the current draft (the AI chat, another user, another tab). Local + // workflow mutations do not dispatch these events, only SSE deliveries do, + // and refetching on an own-persist echo reseeds the diagram with the state + // it already shows. + const handleWorkflowVersionOperationBrowserEvent = useCallback( + (detail: ObjectRecordOperationBrowserEventDetail) => { + if (detail.operation.type === 'create-one') { + requestWorkflowRefetch(); + + return; + } + + const updateInputs = + detail.operation.type === 'update-one' + ? [detail.operation.result.updateInput] + : detail.operation.type === 'update-many' + ? detail.operation.result.updateInputs + : []; + + const workflowVersionIds = workflowWithVersionIds?.versions?.map( + (version) => version.id, + ); + + // Without the version mapping, refetch rather than risk a stale diagram. + if (!isDefined(workflowVersionIds)) { + requestWorkflowRefetch(); + + return; + } + + if ( + updateInputs.some((updateInput) => + workflowVersionIds.includes(updateInput.recordId), + ) + ) { + requestWorkflowRefetch(); + } + }, + [workflowWithVersionIds, requestWorkflowRefetch], + ); + useListenToObjectRecordOperationBrowserEvent({ - onObjectRecordOperationBrowserEvent: requestWorkflowRefetch, + onObjectRecordOperationBrowserEvent: + handleWorkflowVersionOperationBrowserEvent, objectMetadataItemId: workflowVersionMetadataItem.id, - operationTypes: ['create-one'], + operationTypes: ['create-one', 'update-one', 'update-many'], }); return null;