From 424c6737a1fbc20819fc874659cf39a57e9664cc Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Tue, 26 May 2026 17:38:36 +0200 Subject: [PATCH] Fix workflow step output schema not reflecting user-renamed step name (#20922) ## Summary - Fixes https://github.com/twentyhq/twenty/issues/20906 - Moves `markStepForRecomputation` from `useUpdateStep` into the lower-level `useUpdateWorkflowVersionStep` hook, so **every** caller that updates a step also triggers output schema recomputation. - Previously, renaming a step via the side panel title input called `useUpdateWorkflowVersionStep` directly (bypassing `useUpdateStep`), so the variable picker kept showing the initial default name (e.g. "Create Record") instead of the user's custom name. ## Test plan - [x] Rename a workflow step via the side panel title input - [x] Verify the variable picker dropdown shows the updated name - [x] Verify variable tags/chips in subsequent steps reflect the updated name - [x] Verify that updating step settings (e.g. changing object type) still refreshes the output schema correctly --- .../hooks/__tests__/useUpdateStep.test.ts | 73 ---------- .../useUpdateWorkflowVersionStep.test.ts | 125 ++++++++++++++++++ .../workflow-steps/hooks/useUpdateStep.ts | 7 - .../hooks/useUpdateWorkflowVersionStep.ts | 10 +- 4 files changed, 134 insertions(+), 81 deletions(-) create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useUpdateWorkflowVersionStep.test.ts diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useUpdateStep.test.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useUpdateStep.test.ts index 3ba4a2a8a8..f8beb06193 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useUpdateStep.test.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useUpdateStep.test.ts @@ -1,10 +1,8 @@ import { useUpdateStep } from '@/workflow/workflow-steps/hooks/useUpdateStep'; import { act, renderHook } from '@testing-library/react'; -import { type WorkflowAction } from '~/generated/graphql'; const mockUpdateWorkflowVersionStep = jest.fn(); const mockGetUpdatableWorkflowVersion = jest.fn(); -const mockMarkStepForRecomputation = jest.fn(); jest.mock( '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep', @@ -21,12 +19,6 @@ jest.mock('@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow', () => ({ }), })); -jest.mock('@/workflow/workflow-variables/hooks/useStepsOutputSchema', () => ({ - useStepsOutputSchema: jest.fn(() => ({ - markStepForRecomputation: mockMarkStepForRecomputation, - })), -})); - describe('useUpdateStep', () => { beforeEach(() => { jest.clearAllMocks(); @@ -69,69 +61,4 @@ describe('useUpdateStep', () => { step: mockStep, }); }); - - it('should mark step for recomputation after update', async () => { - const mockWorkflowVersionId = 'version-123'; - const mockStep = { - id: 'step-1', - name: 'Create Record', - valid: true, - type: 'CREATE_RECORD' as const, - settings: { - input: { - objectName: 'company', - }, - errorHandlingOptions: { - retryOnFailure: { value: false }, - continueOnFailure: { value: false }, - }, - }, - }; - - mockGetUpdatableWorkflowVersion.mockResolvedValue(mockWorkflowVersionId); - - const { result } = renderHook(() => useUpdateStep()); - await act(async () => { - await result.current.updateStep(mockStep as WorkflowAction); - }); - - expect(mockMarkStepForRecomputation).toHaveBeenCalledWith({ - stepId: 'step-1', - workflowVersionId: mockWorkflowVersionId, - }); - }); - - it('should mark step for recomputation for all step types', async () => { - const mockWorkflowVersionId = 'version-123'; - const stepTypes = ['CODE', 'HTTP_REQUEST', 'CREATE_RECORD', 'SEND_EMAIL']; - - for (const stepType of stepTypes) { - mockMarkStepForRecomputation.mockClear(); - mockGetUpdatableWorkflowVersion.mockResolvedValue(mockWorkflowVersionId); - - const mockStep = { - id: `step-${stepType}`, - name: `${stepType} Step`, - valid: true, - type: stepType as any, - settings: { - input: {}, - errorHandlingOptions: { - retryOnFailure: { value: false }, - continueOnFailure: { value: false }, - }, - }, - }; - - const { result } = renderHook(() => useUpdateStep()); - await act(async () => { - await result.current.updateStep(mockStep as WorkflowAction); - }); - - expect(mockMarkStepForRecomputation).toHaveBeenCalledWith({ - stepId: `step-${stepType}`, - workflowVersionId: mockWorkflowVersionId, - }); - } - }); }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useUpdateWorkflowVersionStep.test.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useUpdateWorkflowVersionStep.test.ts new file mode 100644 index 0000000000..8dae10b4f9 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/__tests__/useUpdateWorkflowVersionStep.test.ts @@ -0,0 +1,125 @@ +import { useUpdateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep'; +import { act, renderHook } from '@testing-library/react'; + +const mockMutate = jest.fn(); +const mockGetRecordFromCache = jest.fn(); +const mockMarkStepForRecomputation = jest.fn(); +const mockEnqueueErrorSnackBar = jest.fn(); + +jest.mock('@/object-metadata/hooks/useApolloCoreClient', () => ({ + useApolloCoreClient: () => ({ cache: {} }), +})); + +jest.mock('@/object-metadata/hooks/useObjectMetadataItems', () => ({ + useObjectMetadataItems: () => ({ objectMetadataItems: [] }), +})); + +jest.mock('@/object-metadata/hooks/useObjectMetadataItem', () => ({ + useObjectMetadataItem: () => ({ objectMetadataItem: {} }), +})); + +jest.mock('@/object-record/hooks/useObjectPermissions', () => ({ + useObjectPermissions: () => ({ objectPermissionsByObjectMetadataId: {} }), +})); + +jest.mock('@/ui/feedback/snack-bar-manager/hooks/useSnackBar', () => ({ + useSnackBar: () => ({ enqueueErrorSnackBar: mockEnqueueErrorSnackBar }), +})); + +jest.mock('@/object-record/cache/hooks/useGetRecordFromCache', () => ({ + useGetRecordFromCache: () => mockGetRecordFromCache, +})); + +jest.mock('@/object-record/cache/utils/updateRecordFromCache', () => ({ + updateRecordFromCache: jest.fn(), +})); + +jest.mock('@/workflow/workflow-variables/hooks/useStepsOutputSchema', () => ({ + useStepsOutputSchema: () => ({ + markStepForRecomputation: mockMarkStepForRecomputation, + }), +})); + +jest.mock('@apollo/client/react', () => ({ + useMutation: () => [mockMutate], +})); + +describe('useUpdateWorkflowVersionStep', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should mark step for recomputation after successful update', async () => { + const updatedStep = { + id: 'step-1', + name: 'My Custom Name', + type: 'CREATE_RECORD', + }; + + mockMutate.mockResolvedValue({ + data: { updateWorkflowVersionStep: updatedStep }, + }); + + mockGetRecordFromCache.mockReturnValue({ + id: 'version-1', + steps: [{ id: 'step-1', name: 'Create Record', type: 'CREATE_RECORD' }], + }); + + const { result } = renderHook(() => useUpdateWorkflowVersionStep()); + + await act(async () => { + await result.current.updateWorkflowVersionStep({ + workflowVersionId: 'version-1', + step: updatedStep, + }); + }); + + expect(mockMarkStepForRecomputation).toHaveBeenCalledWith({ + stepId: 'step-1', + workflowVersionId: 'version-1', + }); + }); + + it('should not mark step for recomputation when mutation returns no data', async () => { + mockMutate.mockResolvedValue({ data: null }); + + const { result } = renderHook(() => useUpdateWorkflowVersionStep()); + + await act(async () => { + await result.current.updateWorkflowVersionStep({ + workflowVersionId: 'version-1', + step: { id: 'step-1', name: 'Step', type: 'CODE' }, + }); + }); + + expect(mockMarkStepForRecomputation).not.toHaveBeenCalled(); + }); + + it('should still mark step for recomputation when cached record is missing', async () => { + mockMutate.mockResolvedValue({ + data: { + updateWorkflowVersionStep: { + id: 'step-1', + name: 'Step', + type: 'CODE', + }, + }, + }); + + mockGetRecordFromCache.mockReturnValue(undefined); + + const { result } = renderHook(() => useUpdateWorkflowVersionStep()); + + await act(async () => { + await result.current.updateWorkflowVersionStep({ + workflowVersionId: 'version-1', + step: { id: 'step-1', name: 'Step', type: 'CODE' }, + }); + }); + + expect(mockMarkStepForRecomputation).toHaveBeenCalledWith({ + stepId: 'step-1', + workflowVersionId: 'version-1', + }); + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useUpdateStep.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useUpdateStep.ts index dfc32b9475..46f892d6c1 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useUpdateStep.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useUpdateStep.ts @@ -1,13 +1,11 @@ import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow'; import { type WorkflowAction } from '@/workflow/types/Workflow'; import { useUpdateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep'; -import { useStepsOutputSchema } from '@/workflow/workflow-variables/hooks/useStepsOutputSchema'; export const useUpdateStep = () => { const { getUpdatableWorkflowVersion } = useGetUpdatableWorkflowVersionOrThrow(); const { updateWorkflowVersionStep } = useUpdateWorkflowVersionStep(); - const { markStepForRecomputation } = useStepsOutputSchema(); const updateStep = async (updatedStep: WorkflowAction) => { const workflowVersionId = await getUpdatableWorkflowVersion(); @@ -17,11 +15,6 @@ export const useUpdateStep = () => { step: updatedStep, }); - markStepForRecomputation({ - stepId: updatedStep.id, - workflowVersionId, - }); - return { updatedStep: result?.data?.updateWorkflowVersionStep, }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep.ts index e30231b1b9..6adeaf3716 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep.ts @@ -10,6 +10,7 @@ import { type WorkflowVersion, type WorkflowStep, } from '@/workflow/types/Workflow'; +import { useStepsOutputSchema } from '@/workflow/workflow-variables/hooks/useStepsOutputSchema'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useMutation } from '@apollo/client/react'; import { isDefined } from 'twenty-shared/utils'; @@ -24,6 +25,7 @@ export const useUpdateWorkflowVersionStep = () => { const { objectMetadataItems } = useObjectMetadataItems(); const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); const { enqueueErrorSnackBar } = useSnackBar(); + const { markStepForRecomputation } = useStepsOutputSchema(); const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular: CoreObjectNameSingular.WorkflowVersion, @@ -52,11 +54,16 @@ export const useUpdateWorkflowVersionStep = () => { return; } + markStepForRecomputation({ + stepId: updatedStep.id, + workflowVersionId: input.workflowVersionId, + }); + const cachedRecord = getRecordFromCache( input.workflowVersionId, ); if (!isDefined(cachedRecord)) { - return; + return result; } const newCachedRecord = { @@ -80,6 +87,7 @@ export const useUpdateWorkflowVersionStep = () => { recordGqlFields, objectPermissionsByObjectMetadataId, }); + return result; };