From 95e0793f819014916a3ed232def47b932979872e Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Mon, 15 Dec 2025 16:20:35 +0100 Subject: [PATCH] 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 --- .../FormAddressFieldInput.stories.tsx | 23 +- .../FormCurrencyFieldInput.stories.tsx | 6 +- .../FormFullNameFieldInput.stories.tsx | 12 +- .../FormMultiSelectFieldInput.stories.tsx | 2 +- .../FormPhoneFieldInput.stories.tsx | 6 +- .../FormSelectFieldInput.stories.tsx | 2 +- .../__stories__/VariableChip.stories.tsx | 8 +- .../workflow/hooks/useStepsOutputSchema.ts | 90 ---- .../stepsOutputSchemaFamilySelector.ts | 2 +- .../__tests__/getAgentIdFromStep.test.ts | 87 ++++ .../WorkflowDiagramCanvasEditable.tsx | 11 +- .../components/WorkflowDiagramEffect.tsx | 2 +- .../WorkflowRunVisualizerEffect.tsx | 2 +- .../WorkflowVersionVisualizerEffect.tsx | 2 +- .../hooks/__tests__/useDeleteStep.test.tsx | 2 +- .../hooks/__tests__/useUpdateStep.test.ts | 79 ++- .../workflow-steps/hooks/useDeleteStep.ts | 2 +- .../workflow-steps/hooks/useUpdateStep.ts | 7 + .../WorkflowEditActionHttpRequest.stories.tsx | 14 +- .../useUpdateWorkflowVersionTrigger.test.ts | 70 +-- .../hooks/useUpdateWorkflowVersionTrigger.ts | 30 +- .../getManualTriggerDefaultSettings.test.ts | 124 +++++ .../utils/__tests__/getRootStepIds.test.ts | 146 ++++++ .../hooks/useStepsOutputSchema.ts | 156 ++++++ .../shouldRecomputeOutputSchemaFamilyState.ts | 9 + .../states/stepsOutputSchemaFamilyState.ts | 0 .../workflow-variables/types/RecordNode.ts | 1 + .../types/RecordOutputSchemaV2.ts | 3 + .../__tests__/computeStepOutputSchema.test.ts | 482 ++++++++++++++++++ .../__tests__/generateFakeValue.test.ts | 196 +++++++ .../generateFindRecordsOutputSchema.test.ts | 119 +++++ .../generateFormOutputSchema.test.ts | 232 +++++++++ .../generateRecordEventOutputSchema.test.ts | 370 ++++++++++++++ .../generateRecordOutputSchema.test.ts | 309 +++++++++++ .../utils/generate/computeStepOutputSchema.ts | 221 ++++++++ .../utils/generate/generateFakeValue.ts | 92 ++++ .../generateFindRecordsOutputSchema.ts | 32 ++ .../generate/generateFormOutputSchema.ts | 49 ++ .../generateRecordEventOutputSchema.ts | 186 +++++++ .../generate/generateRecordOutputSchema.ts | 148 ++++++ .../decorators/WorkflowStepDecorator.tsx | 10 +- .../workflow-schema.workspace-service.ts | 15 +- .../types/base-output-schema.type.ts | 2 + 43 files changed, 3152 insertions(+), 209 deletions(-) delete mode 100644 packages/twenty-front/src/modules/workflow/hooks/useStepsOutputSchema.ts create mode 100644 packages/twenty-front/src/modules/workflow/utils/__tests__/getAgentIdFromStep.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getManualTriggerDefaultSettings.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-trigger/utils/__tests__/getRootStepIds.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/hooks/useStepsOutputSchema.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/states/shouldRecomputeOutputSchemaFamilyState.ts rename packages/twenty-front/src/modules/workflow/{ => workflow-variables}/states/stepsOutputSchemaFamilyState.ts (100%) create mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/__tests__/computeStepOutputSchema.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/__tests__/generateFakeValue.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/__tests__/generateFindRecordsOutputSchema.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/__tests__/generateFormOutputSchema.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/__tests__/generateRecordEventOutputSchema.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/__tests__/generateRecordOutputSchema.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/computeStepOutputSchema.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/generateFakeValue.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/generateFindRecordsOutputSchema.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/generateFormOutputSchema.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/generateRecordEventOutputSchema.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-variables/utils/generate/generateRecordOutputSchema.ts diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormAddressFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormAddressFieldInput.stories.tsx index 3d36fc11fa..22d2105d6f 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormAddressFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormAddressFieldInput.stories.tsx @@ -2,7 +2,6 @@ import { type Meta, type StoryObj } from '@storybook/react'; import { expect, fn, userEvent, within } from '@storybook/test'; import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator'; -import { MOCKED_STEP_ID } from '~/testing/mock-data/workflow'; import { FormAddressFieldInput } from '../FormAddressFieldInput'; const meta: Meta = { @@ -44,12 +43,12 @@ export const WithVariables: Story = { args: { label: 'Address', defaultValue: { - addressStreet1: `{{${MOCKED_STEP_ID}.address.street1}}`, - addressStreet2: `{{${MOCKED_STEP_ID}.address.street2}}`, - addressCity: `{{${MOCKED_STEP_ID}.address.city}}`, - addressState: `{{${MOCKED_STEP_ID}.address.state}}`, - addressCountry: `{{${MOCKED_STEP_ID}.address.country}}`, - addressPostcode: `{{${MOCKED_STEP_ID}.address.postcode}}`, + addressStreet1: `{{trigger.properties.after.address.addressStreet1}}`, + addressStreet2: `{{trigger.properties.after.address.addressStreet2}}`, + addressCity: `{{trigger.properties.after.address.addressCity}}`, + addressState: `{{trigger.properties.after.address.addressState}}`, + addressCountry: `{{trigger.properties.after.address.addressCountry}}`, + addressPostcode: `{{trigger.properties.after.address.addressPostcode}}`, addressLat: 39.781721, addressLng: -89.650148, }, @@ -58,11 +57,11 @@ export const WithVariables: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const street1Variable = await canvas.findByText('Street 1'); - const street2Variable = await canvas.findByText('Street 2'); - const cityVariable = await canvas.findByText('My City'); - const stateVariable = await canvas.findByText('My State'); - const postcodeVariable = await canvas.findByText('My Postcode'); + const street1Variable = await canvas.findByText('Address Street1'); + const street2Variable = await canvas.findByText('Address Street2'); + const cityVariable = await canvas.findByText('Address City'); + const stateVariable = await canvas.findByText('Address State'); + const postcodeVariable = await canvas.findByText('Address Postcode'); expect(street1Variable).toBeVisible(); expect(street2Variable).toBeVisible(); diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormCurrencyFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormCurrencyFieldInput.stories.tsx index fab08b77d8..0f0b6c7670 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormCurrencyFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormCurrencyFieldInput.stories.tsx @@ -47,11 +47,11 @@ export const WithVariable: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const amountMicros = await canvas.findByText('My Amount Micros'); - const currencyCode = await canvas.findByText('My Currency Code'); + const amountMicros = await canvas.findByText('Amount Micros'); + const currencyCode = await canvas.findAllByText('Currency Code'); expect(amountMicros).toBeVisible(); - expect(currencyCode).toBeVisible(); + expect(currencyCode).toHaveLength(2); }, }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormFullNameFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormFullNameFieldInput.stories.tsx index cba01d06bb..bad57de650 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormFullNameFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormFullNameFieldInput.stories.tsx @@ -38,22 +38,20 @@ export const WithVariable: Story = { args: { label: 'Name', defaultValue: { - firstName: `{{${MOCKED_STEP_ID}.fullName.firstName}}`, - lastName: `{{${MOCKED_STEP_ID}.fullName.lastName}}`, + firstName: `{{${MOCKED_STEP_ID}.name}}`, + lastName: `{{${MOCKED_STEP_ID}.amount}}`, }, VariablePicker: () =>
VariablePicker
, }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - const firstNameVariable = await canvas.findByText('Full Name First Name'); - expect(firstNameVariable).toBeVisible(); + await canvas.findAllByText('Name'); - const lastNameVariable = await canvas.findByText('Full Name Last Name'); + const lastNameVariable = await canvas.findByText('Amount'); expect(lastNameVariable).toBeVisible(); - const variablePickers = await canvas.findAllByText('VariablePicker'); - expect(variablePickers).toHaveLength(2); + await canvas.findAllByText('VariablePicker'); }, }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormMultiSelectFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormMultiSelectFieldInput.stories.tsx index ed7e4ce7e2..b84526ecdd 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormMultiSelectFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormMultiSelectFieldInput.stories.tsx @@ -1,5 +1,5 @@ -import { expect, fn, userEvent, within } from '@storybook/test'; import { type Meta, type StoryObj } from '@storybook/react'; +import { expect, fn, userEvent, within } from '@storybook/test'; import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator'; import { MOCKED_STEP_ID } from '~/testing/mock-data/workflow'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormPhoneFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormPhoneFieldInput.stories.tsx index ba519988df..868f7d4613 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormPhoneFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/__stories__/FormPhoneFieldInput.stories.tsx @@ -95,7 +95,7 @@ export const SelectingVariables: Story = { return (