From 0b406042a15c74c4c4928f1c5c0bc3ac63616e1a Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Wed, 11 Jun 2025 10:20:40 +0200 Subject: [PATCH] Allow many record fields relative to the same record type in workflow forms (#12522) Relative to https://github.com/twentyhq/twenty/issues/12517 ## Before https://private-user-images.githubusercontent.com/29370468/453438380-58c52f55-9145-40f9-a9e9-caec2a2281ea.mp4?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDk1NzAwODAsIm5iZiI6MTc0OTU2OTc4MCwicGF0aCI6Ii8yOTM3MDQ2OC80NTM0MzgzODAtNThjNTJmNTUtOTE0NS00MGY5LWE5ZTktY2FlYzJhMjI4MWVhLm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA2MTAlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwNjEwVDE1MzYyMFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWYxY2VlZWVmM2I2ZDBhOGQ3NzdlMjEyZTE3OTg0ZDZmMWRmMjQzZTVmYWM5MmU4NDM1NjkyZjNiYWZmMzUxZTAmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.n3nrJ4-I-pUjMz2YripGDHZtKc_P3hSlOFK7apFqVIA ## After https://github.com/user-attachments/assets/4877ca29-f900-48ea-ba3c-124f910d8cf3 --- .../components/FormSingleRecordPicker.tsx | 7 +- .../FormSingleRecordPicker.stories.tsx | 6 +- .../WorkflowEditActionFormFiller.stories.tsx | 82 ++++++++++++++++++- 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSingleRecordPicker.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSingleRecordPicker.tsx index f53d7ef49..d8ac76070 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSingleRecordPicker.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSingleRecordPicker.tsx @@ -15,7 +15,7 @@ import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-sta import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString'; import { css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { useCallback } from 'react'; +import { useCallback, useId } from 'react'; import { isDefined, isValidUuid } from 'twenty-shared/utils'; import { IconChevronDown, IconForbid } from 'twenty-ui/display'; @@ -98,8 +98,9 @@ export const FormSingleRecordPicker = ({ skip: !isDefined(defaultValue) || !isValidUuid(defaultValue), }); - const dropdownId = `form-record-picker-${objectNameSingular}`; - const variablesDropdownId = `form-record-picker-${objectNameSingular}-variables`; + const componentId = useId(); + const dropdownId = `form-record-picker-${componentId}`; + const variablesDropdownId = `form-record-picker-${componentId}-variables`; const { closeDropdown } = useDropdown(dropdownId); diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormSingleRecordPicker.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormSingleRecordPicker.stories.tsx index a05e13438..ae1900c38 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormSingleRecordPicker.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormSingleRecordPicker.stories.tsx @@ -41,9 +41,13 @@ export const Default: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await canvas.findByText('Company'); + const label = await canvas.findByText('Company'); + expect(label).toBeVisible(); + const dropdown = await canvas.findByRole('button'); expect(dropdown).toBeVisible(); + + await userEvent.click(dropdown); }, }; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/__stories__/WorkflowEditActionFormFiller.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/__stories__/WorkflowEditActionFormFiller.stories.tsx index b2200d316..af42e3dec 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/__stories__/WorkflowEditActionFormFiller.stories.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/form-action/components/__stories__/WorkflowEditActionFormFiller.stories.tsx @@ -1,10 +1,15 @@ import { WorkflowFormAction } from '@/workflow/types/Workflow'; import { Meta, StoryObj } from '@storybook/react'; -import { expect, within } from '@storybook/test'; +import { expect, userEvent, waitFor, within } from '@storybook/test'; import { FieldMetadataType } from 'twenty-shared/types'; -import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing'; +import { + ComponentDecorator, + getCanvasElementForDropdownTesting, + RouterDecorator, +} from 'twenty-ui/testing'; import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; +import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator'; import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator'; import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator'; @@ -25,6 +30,7 @@ const meta: Meta = { RouterDecorator, ObjectMetadataItemsDecorator, WorkspaceDecorator, + SnackBarDecorator, ], }; @@ -81,6 +87,42 @@ const mockAction: WorkflowFormAction = { }, }; +const mockActionWithDuplicatedRecordFields: WorkflowFormAction = { + id: 'form-action-1', + type: 'FORM', + name: 'Test Form', + valid: true, + settings: { + input: [ + { + id: 'field-1', + name: 'record', + label: 'Record', + type: 'RECORD', + placeholder: 'Select a record', + settings: { + objectName: 'company', + }, + }, + { + id: 'field-2', + name: 'record', + label: 'Record', + type: 'RECORD', + placeholder: 'Select a record', + settings: { + objectName: 'company', + }, + }, + ], + outputSchema: {}, + errorHandlingOptions: { + retryOnFailure: { value: false }, + continueOnFailure: { value: false }, + }, + }, +}; + export const Default: Story = { args: { action: mockAction, @@ -124,7 +166,41 @@ export const ReadonlyMode: Story = { const dateInput = await canvas.findByPlaceholderText('mm/dd/yyyy'); expect(dateInput).toBeDisabled(); - const submitButton = await canvas.queryByText('Submit'); + const submitButton = canvas.queryByText('Submit'); expect(submitButton).not.toBeInTheDocument(); }, }; + +export const CanHaveManyRecordFieldsForTheSameRecordType: Story = { + args: { + action: mockActionWithDuplicatedRecordFields, + actionOptions: { + readonly: false, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const recordSelects = await waitFor(() => { + const elements = canvas.getAllByText('Select a company'); + + expect(elements.length).toBe(2); + + return elements; + }); + + for (const recordSelect of recordSelects) { + expect(recordSelect).toBeVisible(); + + await userEvent.click(recordSelect); + + await waitFor(() => { + expect( + within(getCanvasElementForDropdownTesting()).getByText('Louis Duss'), + ).toBeVisible(); + }); + + await userEvent.click(canvasElement); + } + }, +};