diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx index f7e3893d8..729a8f68d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx @@ -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) ? ( + } + onChange={onChange} + VariablePicker={VariablePicker} + readonly={readonly} + /> ) : null; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormRelationToOneFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormRelationToOneFieldInput.tsx new file mode 100644 index 000000000..b9a0690c1 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormRelationToOneFieldInput.tsx @@ -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; + onChange: (value: JsonValue) => void; + readonly?: boolean; + VariablePicker?: VariablePickerComponent; +}; + +export const FormRelationToOneFieldInput = ({ + label, + objectNameSingular, + onChange, + defaultValue, + readonly, + VariablePicker, +}: FormRelationToOneFieldInputProps) => { + return ( + isDefined(objectNameSingular) && ( + { + onChange({ + id: recordId, + }); + }} + objectNameSingular={objectNameSingular} + disabled={readonly} + VariablePicker={VariablePicker} + /> + ) + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSingleRecordPicker.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSingleRecordPicker.tsx index 775ef771a..f53d7ef49 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSingleRecordPicker.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSingleRecordPicker.tsx @@ -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}`; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect.tsx index 5191b1968..a31c4b9c2 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect.tsx @@ -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), diff --git a/packages/twenty-front/src/modules/workflow/constants/SupportedFieldMetadataTypes.ts b/packages/twenty-front/src/modules/workflow/constants/SupportedFieldMetadataTypes.ts deleted file mode 100644 index 0b882f71a..000000000 --- a/packages/twenty-front/src/modules/workflow/constants/SupportedFieldMetadataTypes.ts +++ /dev/null @@ -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, -]; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord.tsx index 0f7a0b4f9..8d2dc4cbf 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord.tsx @@ -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( 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 45c440884..685a07d35 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 @@ -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), diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/__tests__/shouldDisplayFormField.test.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/__tests__/shouldDisplayFormField.test.ts new file mode 100644 index 000000000..7cbf8fbc3 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/__tests__/shouldDisplayFormField.test.ts @@ -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'); + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField.ts new file mode 100644 index 000000000..93f6f6f92 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField.ts @@ -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 + ); +};