Skip to content

Commit 9eeb50c

Browse files
authored
Add relations in workflow action fields (#12359)
1 parent 1115f6f commit 9eeb50c

File tree

9 files changed

+222
-39
lines changed

9 files changed

+222
-39
lines changed

packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { FormRichTextV2FieldInput } from '@/object-record/record-field/form-type
1414
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
1515
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
1616
import { FormUuidFieldInput } from '@/object-record/record-field/form-types/components/FormUuidFieldInput';
17+
import { FormRelationToOneFieldInput } from '@/object-record/record-field/form-types/components/FormRelationToOneFieldInput';
1718
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
1819
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
1920
import {
@@ -24,6 +25,8 @@ import {
2425
FieldMetadata,
2526
FieldMultiSelectValue,
2627
FieldPhonesValue,
28+
FieldRelationToOneValue,
29+
FieldRelationValue,
2730
FieldRichTextV2Value,
2831
FormFieldCurrencyValue,
2932
} from '@/object-record/record-field/types/FieldMetadata';
@@ -43,6 +46,7 @@ import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isF
4346
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
4447
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
4548
import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid';
49+
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
4650
import { JsonValue } from 'type-fest';
4751

4852
type FormFieldInputProps = {
@@ -205,5 +209,14 @@ export const FormFieldInput = ({
205209
readonly={readonly}
206210
placeholder={placeholder}
207211
/>
212+
) : isFieldRelationToOneObject(field) ? (
213+
<FormRelationToOneFieldInput
214+
label={field.label}
215+
objectNameSingular={field.metadata.relationObjectMetadataNameSingular}
216+
defaultValue={defaultValue as FieldRelationValue<FieldRelationToOneValue>}
217+
onChange={onChange}
218+
VariablePicker={VariablePicker}
219+
readonly={readonly}
220+
/>
208221
) : null;
209222
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
2+
import { FormSingleRecordPicker } from '@/object-record/record-field/form-types/components/FormSingleRecordPicker';
3+
import { isDefined } from 'twenty-shared/utils';
4+
import { JsonValue } from 'type-fest';
5+
import {
6+
FieldRelationToOneValue,
7+
FieldRelationValue,
8+
} from '@/object-record/record-field/types/FieldMetadata';
9+
10+
export type FormRelationToOneFieldInputProps = {
11+
label?: string;
12+
objectNameSingular?: string;
13+
defaultValue?: FieldRelationValue<FieldRelationToOneValue>;
14+
onChange: (value: JsonValue) => void;
15+
readonly?: boolean;
16+
VariablePicker?: VariablePickerComponent;
17+
};
18+
19+
export const FormRelationToOneFieldInput = ({
20+
label,
21+
objectNameSingular,
22+
onChange,
23+
defaultValue,
24+
readonly,
25+
VariablePicker,
26+
}: FormRelationToOneFieldInputProps) => {
27+
return (
28+
isDefined(objectNameSingular) && (
29+
<FormSingleRecordPicker
30+
label={label}
31+
defaultValue={defaultValue?.id}
32+
onChange={(recordId) => {
33+
onChange({
34+
id: recordId,
35+
});
36+
}}
37+
objectNameSingular={objectNameSingular}
38+
disabled={readonly}
39+
VariablePicker={VariablePicker}
40+
/>
41+
)
42+
);
43+
};

packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSingleRecordPicker.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ type FormSingleRecordPickerValue =
5858

5959
export type FormSingleRecordPickerProps = {
6060
label?: string;
61-
defaultValue: RecordId | Variable;
61+
defaultValue?: RecordId | Variable;
6262
onChange: (value: RecordId | Variable) => void;
6363
objectNameSingular: string;
6464
disabled?: boolean;
@@ -95,7 +95,7 @@ export const FormSingleRecordPicker = ({
9595
: '',
9696
objectNameSingular,
9797
withSoftDeleted: true,
98-
skip: !isValidUuid(defaultValue),
98+
skip: !isDefined(defaultValue) || !isValidUuid(defaultValue),
9999
});
100100

101101
const dropdownId = `form-record-picker-${objectNameSingular}`;

packages/twenty-front/src/modules/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
22
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
33
import { FormMultiSelectFieldInput } from '@/object-record/record-field/form-types/components/FormMultiSelectFieldInput';
44
import { FieldMultiSelectValue } from '@/object-record/record-field/types/FieldMetadata';
5-
import { SUPPORTED_FIELD_METADATA_TYPES } from '@/workflow/constants/SupportedFieldMetadataTypes';
65
import { isDefined } from 'twenty-shared/utils';
76
import { useIcons } from 'twenty-ui/display';
7+
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
88

99
export const WorkflowFieldsMultiSelect = ({
1010
label,
@@ -24,11 +24,11 @@ export const WorkflowFieldsMultiSelect = ({
2424
const { getIcon } = useIcons();
2525

2626
const inlineFieldMetadataItems = objectMetadataItem?.fields
27-
.filter(
28-
(fieldMetadataItem) =>
29-
!fieldMetadataItem.isSystem &&
30-
fieldMetadataItem.isActive &&
31-
SUPPORTED_FIELD_METADATA_TYPES.includes(fieldMetadataItem.type),
27+
.filter((fieldMetadataItem) =>
28+
shouldDisplayFormField({
29+
fieldMetadataItem,
30+
actionType: 'UPDATE_RECORD',
31+
}),
3232
)
3333
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
3434
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),

packages/twenty-front/src/modules/workflow/constants/SupportedFieldMetadataTypes.ts

Lines changed: 0 additions & 19 deletions
This file was deleted.

packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { HorizontalSeparator, useIcons } from 'twenty-ui/display';
1717
import { SelectOption } from 'twenty-ui/input';
1818
import { JsonValue } from 'type-fest';
1919
import { useDebouncedCallback } from 'use-debounce';
20-
import { FieldMetadataType } from '~/generated/graphql';
20+
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
2121

2222
type WorkflowEditActionCreateRecordProps = {
2323
action: WorkflowCreateRecordAction;
@@ -92,11 +92,8 @@ export const WorkflowEditActionCreateRecord = ({
9292
const viewFields = indexView?.viewFields ?? [];
9393

9494
const inlineFieldMetadataItems = objectMetadataItem?.fields
95-
.filter(
96-
(fieldMetadataItem) =>
97-
fieldMetadataItem.type !== FieldMetadataType.RELATION &&
98-
!fieldMetadataItem.isSystem &&
99-
fieldMetadataItem.isActive,
95+
.filter((fieldMetadataItem) =>
96+
shouldDisplayFormField({ fieldMetadataItem, actionType: action.type }),
10097
)
10198
.map((fieldMetadataItem) => {
10299
const viewField = viewFields.find(

packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/util
77
import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput';
88
import { FormSingleRecordPicker } from '@/object-record/record-field/form-types/components/FormSingleRecordPicker';
99
import { WorkflowFieldsMultiSelect } from '@/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect';
10-
import { SUPPORTED_FIELD_METADATA_TYPES } from '@/workflow/constants/SupportedFieldMetadataTypes';
1110
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
1211
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
1312
import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow';
@@ -19,6 +18,7 @@ import { HorizontalSeparator, useIcons } from 'twenty-ui/display';
1918
import { SelectOption } from 'twenty-ui/input';
2019
import { JsonValue } from 'type-fest';
2120
import { useDebouncedCallback } from 'use-debounce';
21+
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
2222

2323
type WorkflowEditActionUpdateRecordProps = {
2424
action: WorkflowUpdateRecordAction;
@@ -84,11 +84,8 @@ export const WorkflowEditActionUpdateRecord = ({
8484
const objectNameSingular = selectedObjectMetadataItem?.nameSingular;
8585

8686
const inlineFieldMetadataItems = selectedObjectMetadataItem?.fields
87-
.filter(
88-
(fieldMetadataItem) =>
89-
!fieldMetadataItem.isSystem &&
90-
fieldMetadataItem.isActive &&
91-
SUPPORTED_FIELD_METADATA_TYPES.includes(fieldMetadataItem.type),
87+
.filter((fieldMetadataItem) =>
88+
shouldDisplayFormField({ fieldMetadataItem, actionType: action.type }),
9289
)
9390
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
9491
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
2+
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
3+
import { FieldMetadataType } from 'twenty-shared/types';
4+
5+
const baseField = {
6+
name: 'testField',
7+
label: 'Test Field',
8+
type: FieldMetadataType.TEXT,
9+
isSystem: false,
10+
isActive: true,
11+
settings: null,
12+
} as FieldMetadataItem;
13+
14+
describe('shouldDisplayFormField', () => {
15+
it('returns true for valid CREATE_RECORD field (non-relation)', () => {
16+
const result = shouldDisplayFormField({
17+
fieldMetadataItem: baseField,
18+
actionType: 'CREATE_RECORD',
19+
});
20+
expect(result).toBe(true);
21+
});
22+
23+
it('returns false for CREATE_RECORD with RELATION not MANY_TO_ONE', () => {
24+
const field = {
25+
...baseField,
26+
type: FieldMetadataType.RELATION,
27+
settings: { relationType: 'ONE_TO_MANY' },
28+
} as FieldMetadataItem;
29+
const result = shouldDisplayFormField({
30+
fieldMetadataItem: field,
31+
actionType: 'CREATE_RECORD',
32+
});
33+
expect(result).toBe(false);
34+
});
35+
36+
it('returns false for CREATE_RECORD with RELATION MANY_TO_ONE', () => {
37+
const field = {
38+
...baseField,
39+
type: FieldMetadataType.RELATION,
40+
settings: { relationType: 'MANY_TO_ONE' },
41+
} as FieldMetadataItem;
42+
const result = shouldDisplayFormField({
43+
fieldMetadataItem: field,
44+
actionType: 'CREATE_RECORD',
45+
});
46+
expect(result).toBe(true);
47+
});
48+
49+
it('returns true for UPDATE_RECORD with displayable type', () => {
50+
const result = shouldDisplayFormField({
51+
fieldMetadataItem: baseField,
52+
actionType: 'UPDATE_RECORD',
53+
});
54+
expect(result).toBe(true);
55+
});
56+
57+
it('returns false for UPDATE_RECORD with RELATION not MANY_TO_ONE', () => {
58+
const field = {
59+
...baseField,
60+
type: FieldMetadataType.RELATION,
61+
settings: { relationType: 'ONE_TO_MANY' },
62+
} as FieldMetadataItem;
63+
const result = shouldDisplayFormField({
64+
fieldMetadataItem: field,
65+
actionType: 'UPDATE_RECORD',
66+
});
67+
expect(result).toBe(false);
68+
});
69+
70+
it('returns false for UPDATE_RECORD with RELATION MANY_TO_ONE', () => {
71+
const field = {
72+
...baseField,
73+
type: FieldMetadataType.RELATION,
74+
settings: { relationType: 'MANY_TO_ONE' },
75+
} as FieldMetadataItem;
76+
const result = shouldDisplayFormField({
77+
fieldMetadataItem: field,
78+
actionType: 'UPDATE_RECORD',
79+
});
80+
expect(result).toBe(true);
81+
});
82+
83+
it('returns false for system field', () => {
84+
const field = { ...baseField, isSystem: true };
85+
const result = shouldDisplayFormField({
86+
fieldMetadataItem: field,
87+
actionType: 'UPDATE_RECORD',
88+
});
89+
expect(result).toBe(false);
90+
});
91+
92+
it('throws error on unsupported action', () => {
93+
expect(() =>
94+
shouldDisplayFormField({
95+
fieldMetadataItem: baseField,
96+
actionType: 'DELETE_RECORD',
97+
}),
98+
).toThrow('Action "DELETE_RECORD" is not supported');
99+
});
100+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
2+
import { WorkflowActionType } from '@/workflow/types/Workflow';
3+
import { FieldMetadataType } from '~/generated/graphql';
4+
5+
const DISPLAYABLE_FIELD_TYPES_FOR_UPDATE = [
6+
FieldMetadataType.TEXT,
7+
FieldMetadataType.NUMBER,
8+
FieldMetadataType.DATE,
9+
FieldMetadataType.BOOLEAN,
10+
FieldMetadataType.SELECT,
11+
FieldMetadataType.MULTI_SELECT,
12+
FieldMetadataType.EMAILS,
13+
FieldMetadataType.LINKS,
14+
FieldMetadataType.FULL_NAME,
15+
FieldMetadataType.ADDRESS,
16+
FieldMetadataType.PHONES,
17+
FieldMetadataType.CURRENCY,
18+
FieldMetadataType.DATE_TIME,
19+
FieldMetadataType.RAW_JSON,
20+
FieldMetadataType.UUID,
21+
];
22+
23+
export const shouldDisplayFormField = ({
24+
fieldMetadataItem,
25+
actionType,
26+
}: {
27+
fieldMetadataItem: FieldMetadataItem;
28+
actionType: WorkflowActionType;
29+
}) => {
30+
let isTypeAllowedForAction = false;
31+
32+
switch (actionType) {
33+
case 'CREATE_RECORD':
34+
isTypeAllowedForAction =
35+
fieldMetadataItem.type !== FieldMetadataType.RELATION ||
36+
fieldMetadataItem.settings?.['relationType'] === 'MANY_TO_ONE';
37+
break;
38+
case 'UPDATE_RECORD':
39+
isTypeAllowedForAction =
40+
DISPLAYABLE_FIELD_TYPES_FOR_UPDATE.includes(fieldMetadataItem.type) ||
41+
fieldMetadataItem.settings?.['relationType'] === 'MANY_TO_ONE';
42+
break;
43+
default:
44+
throw new Error(`Action "${actionType}" is not supported`);
45+
}
46+
47+
return (
48+
isTypeAllowedForAction &&
49+
!fieldMetadataItem.isSystem &&
50+
fieldMetadataItem.isActive
51+
);
52+
};

0 commit comments

Comments
 (0)