Add relations in workflow action fields (#12359)

This commit is contained in:
martmull
2025-05-28 18:32:24 +02:00
committed by GitHub
parent 1115f6fc57
commit 9eeb50cb14
9 changed files with 222 additions and 39 deletions

View File

@ -14,6 +14,7 @@ import { FormRichTextV2FieldInput } from '@/object-record/record-field/form-type
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
import { FormUuidFieldInput } from '@/object-record/record-field/form-types/components/FormUuidFieldInput';
import { FormRelationToOneFieldInput } from '@/object-record/record-field/form-types/components/FormRelationToOneFieldInput';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import {
@ -24,6 +25,8 @@ import {
FieldMetadata,
FieldMultiSelectValue,
FieldPhonesValue,
FieldRelationToOneValue,
FieldRelationValue,
FieldRichTextV2Value,
FormFieldCurrencyValue,
} from '@/object-record/record-field/types/FieldMetadata';
@ -43,6 +46,7 @@ import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isF
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid';
import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject';
import { JsonValue } from 'type-fest';
type FormFieldInputProps = {
@ -205,5 +209,14 @@ export const FormFieldInput = ({
readonly={readonly}
placeholder={placeholder}
/>
) : isFieldRelationToOneObject(field) ? (
<FormRelationToOneFieldInput
label={field.label}
objectNameSingular={field.metadata.relationObjectMetadataNameSingular}
defaultValue={defaultValue as FieldRelationValue<FieldRelationToOneValue>}
onChange={onChange}
VariablePicker={VariablePicker}
readonly={readonly}
/>
) : null;
};

View File

@ -0,0 +1,43 @@
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { FormSingleRecordPicker } from '@/object-record/record-field/form-types/components/FormSingleRecordPicker';
import { isDefined } from 'twenty-shared/utils';
import { JsonValue } from 'type-fest';
import {
FieldRelationToOneValue,
FieldRelationValue,
} from '@/object-record/record-field/types/FieldMetadata';
export type FormRelationToOneFieldInputProps = {
label?: string;
objectNameSingular?: string;
defaultValue?: FieldRelationValue<FieldRelationToOneValue>;
onChange: (value: JsonValue) => void;
readonly?: boolean;
VariablePicker?: VariablePickerComponent;
};
export const FormRelationToOneFieldInput = ({
label,
objectNameSingular,
onChange,
defaultValue,
readonly,
VariablePicker,
}: FormRelationToOneFieldInputProps) => {
return (
isDefined(objectNameSingular) && (
<FormSingleRecordPicker
label={label}
defaultValue={defaultValue?.id}
onChange={(recordId) => {
onChange({
id: recordId,
});
}}
objectNameSingular={objectNameSingular}
disabled={readonly}
VariablePicker={VariablePicker}
/>
)
);
};

View File

@ -58,7 +58,7 @@ type FormSingleRecordPickerValue =
export type FormSingleRecordPickerProps = {
label?: string;
defaultValue: RecordId | Variable;
defaultValue?: RecordId | Variable;
onChange: (value: RecordId | Variable) => void;
objectNameSingular: string;
disabled?: boolean;
@ -95,7 +95,7 @@ export const FormSingleRecordPicker = ({
: '',
objectNameSingular,
withSoftDeleted: true,
skip: !isValidUuid(defaultValue),
skip: !isDefined(defaultValue) || !isValidUuid(defaultValue),
});
const dropdownId = `form-record-picker-${objectNameSingular}`;

View File

@ -2,9 +2,9 @@ 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';
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
export const WorkflowFieldsMultiSelect = ({
label,
@ -24,11 +24,11 @@ export const WorkflowFieldsMultiSelect = ({
const { getIcon } = useIcons();
const inlineFieldMetadataItems = objectMetadataItem?.fields
.filter(
(fieldMetadataItem) =>
!fieldMetadataItem.isSystem &&
fieldMetadataItem.isActive &&
SUPPORTED_FIELD_METADATA_TYPES.includes(fieldMetadataItem.type),
.filter((fieldMetadataItem) =>
shouldDisplayFormField({
fieldMetadataItem,
actionType: 'UPDATE_RECORD',
}),
)
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),

View File

@ -1,19 +0,0 @@
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

@ -17,7 +17,7 @@ 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/graphql';
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
type WorkflowEditActionCreateRecordProps = {
action: WorkflowCreateRecordAction;
@ -92,11 +92,8 @@ export const WorkflowEditActionCreateRecord = ({
const viewFields = indexView?.viewFields ?? [];
const inlineFieldMetadataItems = objectMetadataItem?.fields
.filter(
(fieldMetadataItem) =>
fieldMetadataItem.type !== FieldMetadataType.RELATION &&
!fieldMetadataItem.isSystem &&
fieldMetadataItem.isActive,
.filter((fieldMetadataItem) =>
shouldDisplayFormField({ fieldMetadataItem, actionType: action.type }),
)
.map((fieldMetadataItem) => {
const viewField = viewFields.find(

View File

@ -7,7 +7,6 @@ import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/util
import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput';
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';
@ -19,6 +18,7 @@ import { HorizontalSeparator, useIcons } from 'twenty-ui/display';
import { SelectOption } from 'twenty-ui/input';
import { JsonValue } from 'type-fest';
import { useDebouncedCallback } from 'use-debounce';
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
type WorkflowEditActionUpdateRecordProps = {
action: WorkflowUpdateRecordAction;
@ -84,11 +84,8 @@ export const WorkflowEditActionUpdateRecord = ({
const objectNameSingular = selectedObjectMetadataItem?.nameSingular;
const inlineFieldMetadataItems = selectedObjectMetadataItem?.fields
.filter(
(fieldMetadataItem) =>
!fieldMetadataItem.isSystem &&
fieldMetadataItem.isActive &&
SUPPORTED_FIELD_METADATA_TYPES.includes(fieldMetadataItem.type),
.filter((fieldMetadataItem) =>
shouldDisplayFormField({ fieldMetadataItem, actionType: action.type }),
)
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),

View File

@ -0,0 +1,100 @@
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { FieldMetadataType } from 'twenty-shared/types';
const baseField = {
name: 'testField',
label: 'Test Field',
type: FieldMetadataType.TEXT,
isSystem: false,
isActive: true,
settings: null,
} as FieldMetadataItem;
describe('shouldDisplayFormField', () => {
it('returns true for valid CREATE_RECORD field (non-relation)', () => {
const result = shouldDisplayFormField({
fieldMetadataItem: baseField,
actionType: 'CREATE_RECORD',
});
expect(result).toBe(true);
});
it('returns false for CREATE_RECORD with RELATION not MANY_TO_ONE', () => {
const field = {
...baseField,
type: FieldMetadataType.RELATION,
settings: { relationType: 'ONE_TO_MANY' },
} as FieldMetadataItem;
const result = shouldDisplayFormField({
fieldMetadataItem: field,
actionType: 'CREATE_RECORD',
});
expect(result).toBe(false);
});
it('returns false for CREATE_RECORD with RELATION MANY_TO_ONE', () => {
const field = {
...baseField,
type: FieldMetadataType.RELATION,
settings: { relationType: 'MANY_TO_ONE' },
} as FieldMetadataItem;
const result = shouldDisplayFormField({
fieldMetadataItem: field,
actionType: 'CREATE_RECORD',
});
expect(result).toBe(true);
});
it('returns true for UPDATE_RECORD with displayable type', () => {
const result = shouldDisplayFormField({
fieldMetadataItem: baseField,
actionType: 'UPDATE_RECORD',
});
expect(result).toBe(true);
});
it('returns false for UPDATE_RECORD with RELATION not MANY_TO_ONE', () => {
const field = {
...baseField,
type: FieldMetadataType.RELATION,
settings: { relationType: 'ONE_TO_MANY' },
} as FieldMetadataItem;
const result = shouldDisplayFormField({
fieldMetadataItem: field,
actionType: 'UPDATE_RECORD',
});
expect(result).toBe(false);
});
it('returns false for UPDATE_RECORD with RELATION MANY_TO_ONE', () => {
const field = {
...baseField,
type: FieldMetadataType.RELATION,
settings: { relationType: 'MANY_TO_ONE' },
} as FieldMetadataItem;
const result = shouldDisplayFormField({
fieldMetadataItem: field,
actionType: 'UPDATE_RECORD',
});
expect(result).toBe(true);
});
it('returns false for system field', () => {
const field = { ...baseField, isSystem: true };
const result = shouldDisplayFormField({
fieldMetadataItem: field,
actionType: 'UPDATE_RECORD',
});
expect(result).toBe(false);
});
it('throws error on unsupported action', () => {
expect(() =>
shouldDisplayFormField({
fieldMetadataItem: baseField,
actionType: 'DELETE_RECORD',
}),
).toThrow('Action "DELETE_RECORD" is not supported');
});
});

View File

@ -0,0 +1,52 @@
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { WorkflowActionType } from '@/workflow/types/Workflow';
import { FieldMetadataType } from '~/generated/graphql';
const DISPLAYABLE_FIELD_TYPES_FOR_UPDATE = [
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 shouldDisplayFormField = ({
fieldMetadataItem,
actionType,
}: {
fieldMetadataItem: FieldMetadataItem;
actionType: WorkflowActionType;
}) => {
let isTypeAllowedForAction = false;
switch (actionType) {
case 'CREATE_RECORD':
isTypeAllowedForAction =
fieldMetadataItem.type !== FieldMetadataType.RELATION ||
fieldMetadataItem.settings?.['relationType'] === 'MANY_TO_ONE';
break;
case 'UPDATE_RECORD':
isTypeAllowedForAction =
DISPLAYABLE_FIELD_TYPES_FOR_UPDATE.includes(fieldMetadataItem.type) ||
fieldMetadataItem.settings?.['relationType'] === 'MANY_TO_ONE';
break;
default:
throw new Error(`Action "${actionType}" is not supported`);
}
return (
isTypeAllowedForAction &&
!fieldMetadataItem.isSystem &&
fieldMetadataItem.isActive
);
};