Compute output schema on frontend (#16530)
Fixes https://github.com/twentyhq/core-team-issues/issues/1382 Current issue : all step output schemas are computed and stored on backend side. Which means that, when the database schema is updated - like a field creation - steps needs to be deleted an recreated. Which is invisible to users. Solution : schema generation is moved on frontend side 1. Coming on the page the first time, the schema is populated for all steps except a few ones that are handled differently (Code, Webhook, http node, Agent) 2. A separated state allow to determine if a step needs a recomputation. 3. The user only needs a refresh to see the whole schema re-computed Follow-up: - check if remaining backend steps could be moved to runtime computation. But Code will still require storage. - Clean backend service that is not used anymore
This commit is contained in:
+1
-1
@@ -25,7 +25,7 @@ jest.mock('@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@/workflow/hooks/useStepsOutputSchema', () => ({
|
||||
jest.mock('@/workflow/workflow-variables/hooks/useStepsOutputSchema', () => ({
|
||||
useStepsOutputSchema: () => ({
|
||||
deleteStepsOutputSchema: mockDeleteStepsOutputSchema,
|
||||
}),
|
||||
|
||||
+77
-2
@@ -1,8 +1,10 @@
|
||||
import { useUpdateStep } from '@/workflow/workflow-steps/hooks/useUpdateStep';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
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',
|
||||
@@ -19,6 +21,12 @@ jest.mock('@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@/workflow/workflow-variables/hooks/useStepsOutputSchema', () => ({
|
||||
useStepsOutputSchema: jest.fn(() => ({
|
||||
markStepForRecomputation: mockMarkStepForRecomputation,
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('useUpdateStep', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -52,7 +60,9 @@ describe('useUpdateStep', () => {
|
||||
mockGetUpdatableWorkflowVersion.mockResolvedValue(mockWorkflowVersionId);
|
||||
|
||||
const { result } = renderHook(() => useUpdateStep());
|
||||
await result.current.updateStep(mockStep);
|
||||
await act(async () => {
|
||||
await result.current.updateStep(mockStep);
|
||||
});
|
||||
|
||||
expect(mockGetUpdatableWorkflowVersion).toHaveBeenCalled();
|
||||
expect(mockUpdateWorkflowVersionStep).toHaveBeenCalledWith({
|
||||
@@ -60,4 +70,69 @@ 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,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
|
||||
import { useStepsOutputSchema } from '@/workflow/hooks/useStepsOutputSchema';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||
import { useDeleteWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useDeleteWorkflowVersionStep';
|
||||
import { useResetWorkflowAiAgentPermissionsStateOnCommandMenuClose } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/hooks/useResetWorkflowAiAgentPermissionsStateOnCommandMenuClose';
|
||||
import { useStepsOutputSchema } from '@/workflow/workflow-variables/hooks/useStepsOutputSchema';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useDeleteStep = () => {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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();
|
||||
@@ -15,6 +17,11 @@ export const useUpdateStep = () => {
|
||||
step: updatedStep,
|
||||
});
|
||||
|
||||
markStepForRecomputation({
|
||||
stepId: updatedStep.id,
|
||||
workflowVersionId,
|
||||
});
|
||||
|
||||
return {
|
||||
updatedStep: result?.data?.updateWorkflowVersionStep,
|
||||
};
|
||||
|
||||
+6
-8
@@ -218,7 +218,7 @@ export const WithObjectStringBody: Story = {
|
||||
body: `{
|
||||
"hey": "frontend",
|
||||
"oh": "backend",
|
||||
"amazing": "database {{${MOCKED_STEP_ID}.salary}}"
|
||||
"amazing": "database {{${MOCKED_STEP_ID}.name}}"
|
||||
}`,
|
||||
},
|
||||
outputSchema: {},
|
||||
@@ -253,7 +253,7 @@ export const WithObjectStringBody: Story = {
|
||||
|
||||
expect(textboxes[6]).toHaveTextContent('frontend');
|
||||
expect(textboxes[8]).toHaveTextContent('backend');
|
||||
expect(textboxes[10]).toHaveTextContent('database Salary');
|
||||
expect(textboxes[10]).toHaveTextContent('database Name');
|
||||
},
|
||||
};
|
||||
|
||||
@@ -273,7 +273,7 @@ export const WithArrayContainingNonStringVariablesBody: Story = {
|
||||
},
|
||||
body: `[
|
||||
"frontend",
|
||||
{{${MOCKED_STEP_ID}.salary}},
|
||||
{{${MOCKED_STEP_ID}.name}},
|
||||
"database"
|
||||
]`,
|
||||
},
|
||||
@@ -300,9 +300,7 @@ export const WithArrayContainingNonStringVariablesBody: Story = {
|
||||
await waitFor(() => {
|
||||
const textboxes = canvas.getAllByRole('textbox');
|
||||
|
||||
expect(textboxes[5]).toHaveTextContent(
|
||||
'[ "frontend", Salary, "database"]',
|
||||
);
|
||||
expect(textboxes[5]).toHaveTextContent('[ "frontend", Name, "database"]');
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -323,7 +321,7 @@ export const WithObjectContainingNonStringVariablesBody: Story = {
|
||||
},
|
||||
body: `{
|
||||
"speciality": "frontend",
|
||||
"salary": {{${MOCKED_STEP_ID}.salary}}
|
||||
"name": {{${MOCKED_STEP_ID}.name}}
|
||||
}`,
|
||||
},
|
||||
outputSchema: {},
|
||||
@@ -350,7 +348,7 @@ export const WithObjectContainingNonStringVariablesBody: Story = {
|
||||
const textboxes = canvas.getAllByRole('textbox');
|
||||
|
||||
expect(textboxes[5]).toHaveTextContent(
|
||||
'{ "speciality": "frontend", "salary": Salary}',
|
||||
'{ "speciality": "frontend", "name": Name}',
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user