diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect.tsx new file mode 100644 index 000000000..5191b1968 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect.tsx @@ -0,0 +1,64 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition'; +import { FormMultiSelectFieldInput } from '@/object-record/record-field/form-types/components/FormMultiSelectFieldInput'; +import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata'; +import { SUPPORTED_FIELD_METADATA_TYPES } from '@/workflow/constants/SupportedFieldMetadataTypes'; +import { isDefined } from 'twenty-shared/utils'; +import { useIcons } from 'twenty-ui/display'; + +export const WorkflowFieldsMultiSelect = ({ + label, + objectMetadataItem, + handleFieldsChange, + readonly, + defaultFields, + placeholder, +}: { + label: string; + placeholder: string; + objectMetadataItem: ObjectMetadataItem; + handleFieldsChange: (field: FieldMultiSelectValue | string) => void; + readonly: boolean; + defaultFields: string[] | undefined | null; +}) => { + const { getIcon } = useIcons(); + + const inlineFieldMetadataItems = objectMetadataItem?.fields + .filter( + (fieldMetadataItem) => + !fieldMetadataItem.isSystem && + fieldMetadataItem.isActive && + SUPPORTED_FIELD_METADATA_TYPES.includes(fieldMetadataItem.type), + ) + .sort((fieldMetadataItemA, fieldMetadataItemB) => + fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name), + ); + + const inlineFieldDefinitions = isDefined(objectMetadataItem) + ? inlineFieldMetadataItems.map((fieldMetadataItem) => + formatFieldMetadataItemAsFieldDefinition({ + field: fieldMetadataItem, + objectMetadataItem: objectMetadataItem, + showLabel: true, + labelWidth: 90, + }), + ) + : []; + + return ( + ({ + label: field.label, + value: field.metadata.fieldName, + icon: getIcon(field.iconName), + color: 'gray', + }))} + onChange={handleFieldsChange} + placeholder={placeholder} + readonly={readonly} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/__stories__/WorkflowFieldsMultiSelect.stories.ts b/packages/twenty-front/src/modules/workflow/components/__stories__/WorkflowFieldsMultiSelect.stories.ts new file mode 100644 index 000000000..665cc4387 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/__stories__/WorkflowFieldsMultiSelect.stories.ts @@ -0,0 +1,120 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { expect } from '@storybook/jest'; +import type { Meta, StoryObj } from '@storybook/react'; +import { within } from '@storybook/testing-library'; +import { FieldMetadataType } from '~/generated/graphql'; +import { WorkflowFieldsMultiSelect } from '../WorkflowEditUpdateEventFieldsMultiSelect'; + +const meta: Meta = { + title: 'Modules/Workflow/WorkflowFieldsMultiSelect', + component: WorkflowFieldsMultiSelect, + parameters: { + layout: 'centered', + }, +}; + +export default meta; +type Story = StoryObj; + +const mockObjectMetadataItem: ObjectMetadataItem = { + id: '1', + nameSingular: 'company', + namePlural: 'companies', + labelSingular: 'Company', + labelPlural: 'Companies', + description: 'A company', + icon: 'IconBuilding', + isSystem: false, + isCustom: false, + isActive: true, + createdAt: '', + updatedAt: '', + isLabelSyncedWithName: true, + isRemote: false, + isSearchable: true, + labelIdentifierFieldMetadataId: '1', + indexMetadatas: [], + fields: [ + { + id: '1', + name: 'name', + label: 'Name', + type: FieldMetadataType.TEXT, + description: 'Company name', + isCustom: false, + isActive: true, + isSystem: false, + isNullable: false, + createdAt: '', + updatedAt: '', + }, + { + id: '2', + name: 'domainName', + label: 'Domain Name', + type: FieldMetadataType.TEXT, + description: 'Company domain name', + isCustom: false, + isActive: true, + isSystem: false, + isNullable: true, + createdAt: '', + updatedAt: '', + }, + { + id: '3', + name: 'employees', + label: 'Employees', + type: FieldMetadataType.NUMBER, + description: 'Number of employees', + isCustom: false, + isActive: true, + isSystem: false, + isNullable: true, + createdAt: '', + updatedAt: '', + }, + ], +}; + +export const Default: Story = { + args: { + label: 'Fields to update', + placeholder: 'Select fields to update', + objectMetadataItem: mockObjectMetadataItem, + handleFieldsChange: () => {}, + readonly: false, + defaultFields: [], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect( + await canvas.findByTestId('workflow-fields-multi-select'), + ).toBeVisible(); + }, +}; + +export const WithDefaultValues: Story = { + args: { + ...Default.args, + defaultFields: ['name', 'domainName'], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(await canvas.findByText('Name')).toBeVisible(); + expect(await canvas.findByText('Domain Name')).toBeVisible(); + }, +}; + +export const ReadOnly: Story = { + args: { + ...Default.args, + readonly: true, + defaultFields: ['name', 'domainName'], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(await canvas.findByText('Name')).toBeVisible(); + expect(await canvas.findByText('Domain Name')).toBeVisible(); + }, +}; diff --git a/packages/twenty-front/src/modules/workflow/constants/SupportedFieldMetadataTypes.ts b/packages/twenty-front/src/modules/workflow/constants/SupportedFieldMetadataTypes.ts new file mode 100644 index 000000000..0b882f71a --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/constants/SupportedFieldMetadataTypes.ts @@ -0,0 +1,19 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +export const SUPPORTED_FIELD_METADATA_TYPES = [ + FieldMetadataType.TEXT, + FieldMetadataType.NUMBER, + FieldMetadataType.DATE, + FieldMetadataType.BOOLEAN, + FieldMetadataType.SELECT, + FieldMetadataType.MULTI_SELECT, + FieldMetadataType.EMAILS, + FieldMetadataType.LINKS, + FieldMetadataType.FULL_NAME, + FieldMetadataType.ADDRESS, + FieldMetadataType.PHONES, + FieldMetadataType.CURRENCY, + FieldMetadataType.DATE_TIME, + FieldMetadataType.RAW_JSON, + FieldMetadataType.UUID, +]; 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 baf534625..2842c3074 100644 --- a/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts +++ b/packages/twenty-front/src/modules/workflow/validation-schemas/workflowSchema.ts @@ -164,6 +164,7 @@ export const workflowDatabaseEventTriggerSchema = baseTriggerSchema.extend({ input: z.object({}).passthrough().optional(), outputSchema: z.object({}).passthrough(), objectType: z.string().optional(), + fields: z.array(z.string()).optional().nullable(), }), }); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx index 9e86d303e..45c440884 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx @@ -5,8 +5,9 @@ import { useEffect, useState } from 'react'; import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition'; import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput'; -import { FormMultiSelectFieldInput } from '@/object-record/record-field/form-types/components/FormMultiSelectFieldInput'; import { FormSingleRecordPicker } from '@/object-record/record-field/form-types/components/FormSingleRecordPicker'; +import { WorkflowFieldsMultiSelect } from '@/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect'; +import { SUPPORTED_FIELD_METADATA_TYPES } from '@/workflow/constants/SupportedFieldMetadataTypes'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader'; import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow'; @@ -18,7 +19,6 @@ import { HorizontalSeparator, useIcons } from 'twenty-ui/display'; import { SelectOption } from 'twenty-ui/input'; import { JsonValue } from 'type-fest'; import { useDebouncedCallback } from 'use-debounce'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; type WorkflowEditActionUpdateRecordProps = { action: WorkflowUpdateRecordAction; @@ -39,24 +39,6 @@ type UpdateRecordFormData = { [field: string]: unknown; }; -const AVAILABLE_FIELD_METADATA_TYPES = [ - FieldMetadataType.TEXT, - FieldMetadataType.NUMBER, - FieldMetadataType.DATE, - FieldMetadataType.BOOLEAN, - FieldMetadataType.SELECT, - FieldMetadataType.MULTI_SELECT, - FieldMetadataType.EMAILS, - FieldMetadataType.LINKS, - FieldMetadataType.FULL_NAME, - FieldMetadataType.ADDRESS, - FieldMetadataType.PHONES, - FieldMetadataType.CURRENCY, - FieldMetadataType.DATE_TIME, - FieldMetadataType.RAW_JSON, - FieldMetadataType.UUID, -]; - export const WorkflowEditActionUpdateRecord = ({ action, actionOptions, @@ -106,7 +88,7 @@ export const WorkflowEditActionUpdateRecord = ({ (fieldMetadataItem) => !fieldMetadataItem.isSystem && fieldMetadataItem.isActive && - AVAILABLE_FIELD_METADATA_TYPES.includes(fieldMetadataItem.type), + SUPPORTED_FIELD_METADATA_TYPES.includes(fieldMetadataItem.type), ) .sort((fieldMetadataItemA, fieldMetadataItemB) => fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name), @@ -222,22 +204,16 @@ export const WorkflowEditActionUpdateRecord = ({ /> )} - {isDefined(inlineFieldDefinitions) && ( - ({ - label: field.label, - value: field.metadata.fieldName, - icon: getIcon(field.iconName), - color: 'gray', - }))} - onChange={(fieldsToUpdate) => + placeholder="Select fields to update" + objectMetadataItem={selectedObjectMetadataItem} + handleFieldsChange={(fieldsToUpdate) => handleFieldChange('fieldsToUpdate', fieldsToUpdate) } - placeholder="Select fields to update" - readonly={isFormDisabled} + readonly={isFormDisabled ?? false} + defaultFields={formData.fieldsToUpdate} /> )} diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionUpdateRecord.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionUpdateRecord.stories.tsx index 838b8f24d..49402b440 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionUpdateRecord.stories.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionUpdateRecord.stories.tsx @@ -119,9 +119,7 @@ export const DisabledWithEmptyValues: Story = { expect(openRecordSelectButton).not.toBeInTheDocument(); const firstSelectedUpdatableField = await within( - await canvas.findByTestId( - 'workflow-edit-action-record-update-fields-to-update', - ), + await canvas.findByTestId('workflow-fields-multi-select'), ).findByText('Creation date'); await userEvent.click(firstSelectedUpdatableField); @@ -189,9 +187,7 @@ export const DisabledWithDefaultStaticValues: Story = { expect(openRecordSelectButton).not.toBeInTheDocument(); const firstSelectedUpdatableField = await within( - await canvas.findByTestId( - 'workflow-edit-action-record-update-fields-to-update', - ), + await canvas.findByTestId('workflow-fields-multi-select'), ).findByText('Creation date'); await userEvent.click(firstSelectedUpdatableField); @@ -253,9 +249,7 @@ export const DisabledWithDefaultVariableValues: Story = { expect(openRecordSelectButton).not.toBeInTheDocument(); const firstSelectedUpdatableField = await within( - await canvas.findByTestId( - 'workflow-edit-action-record-update-fields-to-update', - ), + await canvas.findByTestId('workflow-fields-multi-select'), ).findByText('Creation date'); await userEvent.click(firstSelectedUpdatableField); diff --git a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm.tsx b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm.tsx index ec5858868..35b16c308 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm.tsx @@ -1,4 +1,5 @@ import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; +import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata'; import { SelectControl } from '@/ui/input/components/SelectControl'; import { SelectHotkeyScope } from '@/ui/input/types/SelectHotkeyScope'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; @@ -9,6 +10,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { WorkflowFieldsMultiSelect } from '@/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect'; import { WorkflowDatabaseEventTrigger } from '@/workflow/types/Workflow'; import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; @@ -20,6 +22,7 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { Trans } from '@lingui/react/macro'; import { useCallback, useMemo, useState } from 'react'; +import { isDefined } from 'twenty-shared/utils'; import { IconChevronLeft, IconSettings, useIcons } from 'twenty-ui/display'; import { MenuItem } from 'twenty-ui/navigation'; @@ -31,7 +34,7 @@ const StyledLabel = styled.span` margin-bottom: ${({ theme }) => theme.spacing(1)}; `; -const StyledContainer = styled.div<{ fullWidth?: boolean }>` +const StyledRecordTypeSelectContainer = styled.div<{ fullWidth?: boolean }>` width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')}; `; @@ -75,6 +78,7 @@ export const WorkflowEditTriggerDatabaseEventForm = ({ const triggerEvent = splitWorkflowTriggerEventName( trigger.settings.eventName, ); + const isUpdateEvent = triggerEvent.event === 'updated'; const regularObjects = objectMetadataItems .filter((item) => item.isActive && !item.isSystem) @@ -97,6 +101,10 @@ export const WorkflowEditTriggerDatabaseEventForm = ({ (option) => option.value === triggerEvent?.objectType, ) || DEFAULT_SELECTED_OPTION; + const selectedObjectMetadataItem = objectMetadataItems.find( + (item) => item.nameSingular === selectedOption.value, + ); + const filteredRegularObjects = useMemo( () => filterOptionsBySearch(regularObjects, searchInputValue), [regularObjects, searchInputValue], @@ -126,6 +134,20 @@ export const WorkflowEditTriggerDatabaseEventForm = ({ closeDropdown(); }; + const handleFieldsChange = (fields: FieldMultiSelectValue | string) => { + if (triggerOptions.readonly === true) { + return; + } + + triggerOptions.onTriggerUpdate({ + ...trigger, + settings: { + ...trigger.settings, + fields: fields ? (Array.isArray(fields) ? fields : [fields]) : null, + }, + }); + }; + const handleSystemObjectsClick = () => { setIsSystemObjectsOpen(true); setSearchInputValue(''); @@ -163,7 +185,7 @@ export const WorkflowEditTriggerDatabaseEventForm = ({ disabled={triggerOptions.readonly} /> - + Record Type - + + {isDefined(selectedObjectMetadataItem) && isUpdateEvent && ( + + )} );