fix: resolve workflow form step auto-open race condition (#21053)

## Summary
- Fix intermittent failure where the Quick Lead workflow form step did
not auto-open
- Root cause: race conditions between SSE events, Apollo cache writes,
and the `runWorkflowVersion` mutation timing
- Add generic monotonicity guard in the SSE handler that drops stale
updates for all records (not just WorkflowRun)

## Changes
- **`useTriggerOptimisticEffectFromSseUpdateEvents.ts`**: Compare
incoming `updatedAt` with cached record before writing — skip if stale.
Moved `upsertRecordsInStore` after the guard so neither Apollo cache nor
Jotai store receive stale data.
- **`useRunWorkflowVersion.tsx`**: Await mutation before opening side
panel; register SSE listener eagerly before mutation
- **`useWorkflowRun.ts`**: Simplified back to plain `useFindOneRecord` +
schema parse (no extra state needed)
- **`generateWorkflowRunDiagram.ts`**: `shouldOpenStep` matches both
PENDING and RUNNING for form steps (backend RUNNING means "waiting for
user input")
- **`WorkflowRunVisualizerEffect.tsx`**: Pass `runStatus` directly
without status mapping
- **`WorkflowRunStepNodeDetail.tsx`**: Form is interactive when step is
PENDING or RUNNING
- **Deleted `latestWorkflowRunFamilyState.ts`**: No longer needed — the
generic SSE guard replaces it

## Test plan
- [x] Hard refresh, run Quick Lead workflow 10+ times — form should
always auto-open
- [x] Complete the form and verify all subsequent steps execute without
getting stuck
- [x] Verify the workflow diagram is always visible (never disappears)
- [x] Verify other record types still update correctly via SSE (e.g.
edit a person in another tab)
This commit is contained in:
Thomas Trompette
2026-05-29 16:18:24 +02:00
committed by GitHub
parent 57118a868f
commit bc1b7f6fdf
9 changed files with 104 additions and 52 deletions
@@ -15,7 +15,9 @@ import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useU
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { computeOptimisticCreateRecordBaseRecordInput } from '@/object-record/utils/computeOptimisticCreateRecordBaseRecordInput';
import { computeOptimisticRecordFromInput } from '@/object-record/utils/computeOptimisticRecordFromInput';
import { useChangeQueryListenState } from '@/sse-db-event/hooks/useChangeQueryListenState';
import { RUN_WORKFLOW_VERSION } from '@/workflow/graphql/mutations/runWorkflowVersion';
import { getWorkflowRunSseQueryId } from '@/workflow/utils/getWorkflowRunSseQueryId';
import { type WorkflowRun } from '@/workflow/types/Workflow';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useCallback } from 'react';
@@ -63,6 +65,7 @@ export const useRunWorkflowVersion = () => {
});
const { openRecordInSidePanel } = useOpenRecordInSidePanel();
const { changeQueryIdListenState } = useChangeQueryListenState();
const setRecordInStore = useCallback(
(workflowRun: WorkflowRun) => {
@@ -137,9 +140,24 @@ export const useRunWorkflowVersion = () => {
setRecordInStore(recordCreatedInCache);
await mutate({
variables: { input: { workflowVersionId, workflowRunId, payload } },
});
const sseQueryId = getWorkflowRunSseQueryId(workflowRunId);
const sseOperationSignature = {
objectNameSingular: CoreObjectNameSingular.WorkflowRun,
variables: {
filter: { id: { eq: workflowRunId } },
},
};
changeQueryIdListenState(true, sseQueryId, sseOperationSignature);
try {
await mutate({
variables: { input: { workflowVersionId, workflowRunId, payload } },
});
} catch (error) {
changeQueryIdListenState(false, sseQueryId, sseOperationSignature);
throw error;
}
openRecordInSidePanel({
objectNameSingular: CoreObjectNameSingular.WorkflowRun,