Add filter fields on update record trigger (#12354)

Fixes https://github.com/twentyhq/core-team-issues/issues/928

<img width="503" alt="Capture d’écran 2025-05-28 à 15 04 08"
src="https://github.com/user-attachments/assets/b83ceced-4b3a-454c-83c1-1176f6836d96"
/>
This commit is contained in:
Thomas Trompette
2025-05-28 16:56:29 +02:00
committed by GitHub
parent 630e4780b8
commit b90cb3e1f9
7 changed files with 252 additions and 46 deletions

View File

@ -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 (
<FormMultiSelectFieldInput
testId="workflow-fields-multi-select"
label={label}
defaultValue={defaultFields}
options={inlineFieldDefinitions.map((field) => ({
label: field.label,
value: field.metadata.fieldName,
icon: getIcon(field.iconName),
color: 'gray',
}))}
onChange={handleFieldsChange}
placeholder={placeholder}
readonly={readonly}
/>
);
};

View File

@ -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<typeof WorkflowFieldsMultiSelect> = {
title: 'Modules/Workflow/WorkflowFieldsMultiSelect',
component: WorkflowFieldsMultiSelect,
parameters: {
layout: 'centered',
},
};
export default meta;
type Story = StoryObj<typeof WorkflowFieldsMultiSelect>;
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();
},
};

View File

@ -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,
];

View File

@ -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(),
}),
});

View File

@ -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) && (
<FormMultiSelectFieldInput
testId="workflow-edit-action-record-update-fields-to-update"
{isDefined(selectedObjectMetadataItem) && (
<WorkflowFieldsMultiSelect
label="Fields to update"
defaultValue={formData.fieldsToUpdate}
options={inlineFieldDefinitions.map((field) => ({
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}
/>
)}

View File

@ -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);

View File

@ -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}
/>
<WorkflowStepBody>
<StyledContainer fullWidth>
<StyledRecordTypeSelectContainer fullWidth>
<StyledLabel>Record Type</StyledLabel>
<Dropdown
dropdownId="workflow-edit-trigger-record-type"
@ -240,7 +262,17 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
}
dropdownHotkeyScope={{ scope: SelectHotkeyScope.Select }}
/>
</StyledContainer>
</StyledRecordTypeSelectContainer>
{isDefined(selectedObjectMetadataItem) && isUpdateEvent && (
<WorkflowFieldsMultiSelect
label="Fields (Optional)"
placeholder="Select specific fields to listen to"
objectMetadataItem={selectedObjectMetadataItem}
handleFieldsChange={handleFieldsChange}
readonly={triggerOptions.readonly ?? false}
defaultFields={trigger.settings.fields}
/>
)}
</WorkflowStepBody>
</>
);