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
This commit is contained in:
Thomas Trompette
2026-05-26 17:38:36 +02:00
committed by GitHub
parent be8c25dfc2
commit 424c6737a1
4 changed files with 134 additions and 81 deletions
@@ -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,
});
}
});
});
@@ -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',
});
});
});
@@ -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,
};
@@ -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<WorkflowVersion>(
input.workflowVersionId,
);
if (!isDefined(cachedRecord)) {
return;
return result;
}
const newCachedRecord = {
@@ -80,6 +87,7 @@ export const useUpdateWorkflowVersionStep = () => {
recordGqlFields,
objectPermissionsByObjectMetadataId,
});
return result;
};