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)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23830?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Félix Malfait
2026-08-05 20:33:33 +02:00
committed by GitHub
parent 393e62ba9f
commit dac5d5a23a
3 changed files with 79 additions and 5 deletions
@@ -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,
@@ -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
/>
<RecordShowPageSSESubscribeEffect
objectNameSingular={objectNameSingular}
recordId={objectRecordId}
queryScope="side-panel-record"
/>
</TimelineActivityContext.Provider>
</StyledSidePanelRecord>
</CommandMenuComponentInstanceContext.Provider>
@@ -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;