From 0ce91d84c193840c4519c7a7a7d71b34a5d08331 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Tue, 18 Mar 2025 17:24:52 +0100 Subject: [PATCH] Allow to add and delete fields (#10990) - Allow to add a new field - On field click, display a delete button - Use id instead of names for fields https://github.com/user-attachments/assets/4ebffe22-225a-4bae-aa49-99e66170181a --- .../validation-schemas/workflowSchema.ts | 2 +- .../components/WorkflowEditActionForm.tsx | 151 +++++++++++++++--- .../WorkflowEditActionForm.stories.tsx | 4 +- .../generate-fake-form-response.spec.ts | 12 +- .../utils/generate-fake-form-response.ts | 2 +- ...workflow-version-step.workspace-service.ts | 4 +- .../workflow-form-action-settings.type.ts | 2 +- 7 files changed, 139 insertions(+), 38 deletions(-) diff --git a/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts b/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts index 176472dd3..b6554370f 100644 --- a/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts +++ b/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts @@ -86,8 +86,8 @@ export const workflowFormActionSettingsSchema = baseWorkflowActionSettingsSchema.extend({ input: z.array( z.object({ + id: z.string().uuid(), label: z.string(), - name: z.string(), type: z.nativeEnum(FieldMetadataType), placeholder: z.string().optional(), settings: z.record(z.any()), diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionForm.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionForm.tsx index c4ba14796..64a85fc46 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionForm.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionForm.tsx @@ -9,8 +9,16 @@ import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; -import { isDefined } from 'twenty-shared'; -import { IconChevronDown, IconPlus, useIcons } from 'twenty-ui'; +import { useState } from 'react'; +import { FieldMetadataType, isDefined } from 'twenty-shared'; +import { + IconChevronDown, + IconChevronUp, + IconPlus, + IconTrash, + useIcons, +} from 'twenty-ui'; +import { v4 } from 'uuid'; type WorkflowEditActionFormProps = { action: WorkflowFormAction; @@ -24,7 +32,13 @@ type WorkflowEditActionFormProps = { }; }; -const StyledContainer = styled.div` +const StyledRowContainer = styled.div` + column-gap: ${({ theme }) => theme.spacing(1)}; + display: grid; + grid-template-columns: 1fr 16px; +`; + +const StyledFieldContainer = styled.div` align-items: center; background: transparent; border: none; @@ -47,6 +61,21 @@ const StyledPlaceholder = styled.div` width: 100%; `; +const StyledIconButtonContainer = styled.div` + align-items: center; + border-radius: ${({ theme }) => theme.border.radius.md}; + display: flex; + justify-content: center; + width: 24px; + + cursor: pointer; + + &:hover, + &[data-open='true'] { + background-color: ${({ theme }) => theme.background.transparent.lighter}; + } +`; + const StyledAddFieldContainer = styled.div` display: flex; color: ${({ theme }) => theme.font.color.secondary}; @@ -65,6 +94,19 @@ export const WorkflowEditActionForm = ({ const { t } = useLingui(); const headerTitle = isDefined(action.name) ? action.name : `Form`; const headerIcon = getActionIcon(action.type); + const [selectedField, setSelectedField] = useState(null); + const isFieldSelected = (fieldName: string) => selectedField === fieldName; + const handleFieldClick = (fieldName: string) => { + if (actionOptions.readonly === true) { + return; + } + + if (isFieldSelected(fieldName)) { + setSelectedField(null); + } else { + setSelectedField(fieldName); + } + }; return ( <> @@ -87,35 +129,94 @@ export const WorkflowEditActionForm = ({ /> {action.settings.input.map((field) => ( - + {field.label ? {field.label} : null} - - - {}}> - {field.placeholder} - + + { + handleFieldClick(field.id); + }} + > + + {field.placeholder} + {isFieldSelected(field.id) ? ( + + ) : ( + + )} + + + + {isFieldSelected(field.id) && ( + + { + if (actionOptions.readonly === true) { + return; + } + + actionOptions.onActionUpdate({ + ...action, + settings: { + ...action.settings, + input: action.settings.input.filter( + (f) => f.id !== field.id, + ), + }, + }); + }} /> - - - + + )} + ))} {!actionOptions.readonly && ( - - - - {}}> - - - {t`Add Field`} - - - - - + + + + { + actionOptions.onActionUpdate({ + ...action, + settings: { + ...action.settings, + input: [ + ...action.settings.input, + { + id: v4(), + type: FieldMetadataType.TEXT, + label: 'New Field', + placeholder: 'New Field', + settings: {}, + }, + ], + }, + }); + }} + > + + + + {t`Add Field`} + + + + + + )} diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionForm.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionForm.stories.tsx index ca0e6c984..3fce15fa3 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionForm.stories.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionForm.stories.tsx @@ -17,14 +17,14 @@ const DEFAULT_ACTION = { settings: { input: [ { - name: 'company', + id: 'ed00b897-519f-44cd-8201-a6502a3a9dc8', type: FieldMetadataType.TEXT, label: 'Company', placeholder: 'Select a company', settings: {}, }, { - name: 'number', + id: 'ed00b897-519f-44cd-8201-a6502a3a9dc9', type: FieldMetadataType.NUMBER, label: 'Number', placeholder: '1000', diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts index 701388878..7c0f52e37 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/__tests__/generate-fake-form-response.spec.ts @@ -6,17 +6,17 @@ describe('generateFakeFormResponse', () => { it('should generate fake responses for a form schema', () => { const schema = [ { - name: 'name', + id: '96939213-49ac-4dee-949d-56e6c7be98e6', type: FieldMetadataType.TEXT, label: 'Name', }, { - name: 'age', + id: '96939213-49ac-4dee-949d-56e6c7be98e7', type: FieldMetadataType.NUMBER, label: 'Age', }, { - name: 'email', + id: '96939213-49ac-4dee-949d-56e6c7be98e8', type: FieldMetadataType.EMAILS, label: 'Email', }, @@ -25,7 +25,7 @@ describe('generateFakeFormResponse', () => { const result = generateFakeFormResponse(schema); expect(result).toEqual({ - email: { + '96939213-49ac-4dee-949d-56e6c7be98e8': { isLeaf: false, label: 'Email', value: { @@ -44,14 +44,14 @@ describe('generateFakeFormResponse', () => { }, icon: undefined, }, - name: { + '96939213-49ac-4dee-949d-56e6c7be98e6': { isLeaf: true, label: 'Name', type: FieldMetadataType.TEXT, value: 'My text', icon: undefined, }, - age: { + '96939213-49ac-4dee-949d-56e6c7be98e7': { isLeaf: true, label: 'Age', type: FieldMetadataType.NUMBER, diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts index f48b390b6..28b73b22a 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response.ts @@ -9,7 +9,7 @@ export const generateFakeFormResponse = ( formMetadata: FormFieldMetadata[], ): Record => { return formMetadata.reduce((acc, formFieldMetadata) => { - acc[formFieldMetadata.name] = generateFakeField({ + acc[formFieldMetadata.id] = generateFakeField({ type: formFieldMetadata.type, label: formFieldMetadata.label, }); diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service.ts index f2e74650a..aaa6fb2b5 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service.ts @@ -505,14 +505,14 @@ export class WorkflowVersionStepWorkspaceService { ...BASE_STEP_DEFINITION, input: [ { + id: v4(), label: 'Company', - name: 'company', placeholder: 'Select a company', type: FieldMetadataType.TEXT, }, { + id: v4(), label: 'Number', - name: 'number', placeholder: '1000', type: FieldMetadataType.NUMBER, }, diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type.ts index a57f191ad..813aa6d80 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type.ts @@ -3,8 +3,8 @@ import { FieldMetadataType } from 'twenty-shared'; import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type'; export type FormFieldMetadata = { + id: string; label: string; - name: string; type: FieldMetadataType; placeholder?: string; settings?: Record;