diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect.tsx index b13b8e72bd..71c85455b2 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect.tsx @@ -16,6 +16,7 @@ export const WorkflowFieldsMultiSelect = ({ defaultFields, placeholder, hint, + actionType, }: { label: string; placeholder: string; @@ -23,6 +24,7 @@ export const WorkflowFieldsMultiSelect = ({ handleFieldsChange: (field: FieldMultiSelectValue | string) => void; readonly: boolean; defaultFields: string[] | undefined | null; + actionType: 'UPDATE_RECORD' | 'UPSERT_RECORD'; hint?: string; }) => { const { getIcon } = useIcons(); @@ -31,7 +33,7 @@ export const WorkflowFieldsMultiSelect = ({ .filter((fieldMetadataItem) => shouldDisplayFormField({ fieldMetadataItem, - actionType: 'UPDATE_RECORD', + actionType, }), ) .sort((fieldMetadataItemA, fieldMetadataItemB) => @@ -55,6 +57,17 @@ export const WorkflowFieldsMultiSelect = ({ label={label} defaultValue={defaultFields} options={inlineFieldDefinitions.map((field) => { + const isIdField = field.metadata.fieldName === 'id'; + + if (isIdField && actionType === 'UPSERT_RECORD') { + return { + label: 'ID', + value: field.metadata.fieldName, + Icon: getIcon('IconId'), + color: 'gray', + }; + } + const isFieldRelationManyToOne = isFieldRelation(field) && field.metadata.relationType === RelationType.MANY_TO_ONE; 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 index 7212e64d12..7cb856c1f5 100644 --- a/packages/twenty-front/src/modules/workflow/components/__stories__/WorkflowFieldsMultiSelect.stories.ts +++ b/packages/twenty-front/src/modules/workflow/components/__stories__/WorkflowFieldsMultiSelect.stories.ts @@ -92,6 +92,7 @@ export const Default: Story = { handleFieldsChange: () => {}, readonly: false, defaultFields: [], + actionType: 'UPDATE_RECORD', }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -105,6 +106,7 @@ export const WithDefaultValues: Story = { args: { ...Default.args, defaultFields: ['name', 'domainName'], + actionType: 'UPDATE_RECORD', }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); @@ -118,6 +120,7 @@ export const ReadOnly: Story = { ...Default.args, readonly: true, defaultFields: ['name', 'domainName'], + actionType: 'UPDATE_RECORD', }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowCreateRecordBody.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowCreateRecordBody.tsx deleted file mode 100644 index b6fd62d577..0000000000 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowCreateRecordBody.tsx +++ /dev/null @@ -1,266 +0,0 @@ -import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; -import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition'; -import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput'; -import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; -import { Select } from '@/ui/input/components/Select'; -import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; -import { useViewOrDefaultViewFromPrefetchedViews } from '@/views/hooks/useViewOrDefaultViewFromPrefetchedViews'; -import { WorkflowFieldsMultiSelect } from '@/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect'; -import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; -import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField'; -import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; -import { useTheme } from '@emotion/react'; -import { t } from '@lingui/core/macro'; -import { useEffect, useState } from 'react'; -import { isDefined } from 'twenty-shared/utils'; -import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow'; -import { HorizontalSeparator, useIcons } from 'twenty-ui/display'; -import { type SelectOption } from 'twenty-ui/input'; -import { type JsonValue } from 'type-fest'; -import { useDebouncedCallback } from 'use-debounce'; -import { RelationType } from '~/generated-metadata/graphql'; - -type RelationManyToOneField = { - id: string; -}; - -export type CreateRecordFormData = { - objectName: string; - [field: string]: RelationManyToOneField | JsonValue; -}; - -type WorkflowCreateRecordBodyProps = { - defaultObjectName: string; - defaultObjectRecord: Record; - readonly: boolean; - actionType: 'CREATE_RECORD' | 'UPSERT_RECORD'; - onUpdate: (formData: CreateRecordFormData) => void; -}; - -const sortByViewFieldPosition = ( - a: { viewFieldPosition?: number }, - b: { viewFieldPosition?: number }, -) => { - if (isDefined(a.viewFieldPosition) && isDefined(b.viewFieldPosition)) { - return a.viewFieldPosition - b.viewFieldPosition; - } - - if (isDefined(a.viewFieldPosition)) { - return -1; - } - - if (isDefined(b.viewFieldPosition)) { - return 1; - } - - return 0; -}; - -export const WorkflowCreateRecordBody = ({ - defaultObjectName, - defaultObjectRecord, - readonly, - actionType, - onUpdate, -}: WorkflowCreateRecordBodyProps) => { - const theme = useTheme(); - - const { getIcon } = useIcons(); - - const { activeNonSystemObjectMetadataItems } = - useFilteredObjectMetadataItems(); - - const availableMetadata: Array> = - activeNonSystemObjectMetadataItems - .filter((objectMetadataItem) => - canObjectBeManagedByWorkflow({ - nameSingular: objectMetadataItem.nameSingular, - isSystem: objectMetadataItem.isSystem, - }), - ) - .map((item) => ({ - Icon: getIcon(item.icon), - label: item.labelPlural, - value: item.nameSingular, - })); - - const [formData, setFormData] = useState({ - objectName: defaultObjectName, - ...defaultObjectRecord, - }); - - const objectNameSingular = formData.objectName; - - const { objectMetadataItems } = useObjectMetadataItems(); - - const objectMetadataItem = objectMetadataItems.find( - (item) => item.nameSingular === objectNameSingular, - ); - - const objectLabelSingular = objectMetadataItem?.labelSingular; - - const { view: indexView } = useViewOrDefaultViewFromPrefetchedViews({ - objectMetadataItemId: objectMetadataItem?.id ?? '', - }); - - const viewFields = indexView?.viewFields ?? []; - - const inlineFieldMetadataItems = objectMetadataItem?.fields - .filter((fieldMetadataItem) => - shouldDisplayFormField({ fieldMetadataItem, actionType }), - ) - .map((fieldMetadataItem) => { - const viewField = viewFields.find( - (viewField) => viewField.fieldMetadataId === fieldMetadataItem.id, - ); - return { - ...fieldMetadataItem, - viewFieldPosition: viewField?.position, - }; - }) - .sort(sortByViewFieldPosition); - - const uniqueFieldMetadataItems = inlineFieldMetadataItems?.filter( - (fieldMetadataItem) => fieldMetadataItem.isUnique, - ); - - const inlineFieldDefinitions = isDefined(objectMetadataItem) - ? inlineFieldMetadataItems?.map((fieldMetadataItem) => - formatFieldMetadataItemAsFieldDefinition({ - field: fieldMetadataItem, - objectMetadataItem, - showLabel: true, - labelWidth: 90, - }), - ) - : []; - - const handleFieldChange = ( - fieldName: keyof CreateRecordFormData, - updatedValue: JsonValue, - ) => { - const fieldDefinition = inlineFieldDefinitions?.find( - (definition) => definition.metadata.fieldName === fieldName, - ); - - if (!isDefined(fieldDefinition)) { - return; - } - - const isFieldRelationManyToOne = - isFieldRelation(fieldDefinition) && - fieldDefinition.metadata.relationType === RelationType.MANY_TO_ONE; - - const fieldValue = isFieldRelationManyToOne - ? { - id: updatedValue, - } - : updatedValue; - - const newFormData: CreateRecordFormData = { - ...formData, - [fieldName]: fieldValue, - }; - - setFormData(newFormData); - - saveAction(newFormData); - }; - - const saveAction = useDebouncedCallback( - async (formData: CreateRecordFormData) => { - if (readonly === true) { - return; - } - - const { objectName: updatedObjectName, ...updatedOtherFields } = formData; - - onUpdate({ - objectName: updatedObjectName, - ...updatedOtherFields, - }); - }, - 1_000, - ); - - useEffect(() => { - return () => { - saveAction.flush(); - }; - }, [saveAction]); - - return ( - - { + const newFormData: CreateRecordFormData = { + objectName: updatedObjectName, + }; + + setFormData(newFormData); + + saveAction(newFormData); + }} + withSearchInput + dropdownOffset={{ y: parseInt(theme.spacing(1), 10) }} + dropdownWidth={GenericDropdownContentWidth.ExtraLarge} + /> + + + + {inlineFieldDefinitions?.map((fieldDefinition) => { + const isFieldRelationManyToOne = + isFieldRelation(fieldDefinition) && + fieldDefinition.metadata.relationType === RelationType.MANY_TO_ONE; + + const currentValue = isFieldRelationManyToOne + ? ( + formData[ + fieldDefinition.metadata.fieldName + ] as RelationManyToOneField + )?.id + : (formData[fieldDefinition.metadata.fieldName] as JsonValue); + + return ( + { + handleFieldChange(fieldDefinition.metadata.fieldName, value); + }} + VariablePicker={WorkflowVariablePicker} + readonly={isFormDisabled} + /> + ); + })} + {!actionOptions.readonly && } ); 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 8ae6bf2ba9..8181504172 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 @@ -198,6 +198,7 @@ export const WorkflowEditActionUpdateRecord = ({ } readonly={isFormDisabled ?? false} defaultFields={formData.fieldsToUpdate} + actionType="UPDATE_RECORD" /> )} diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpsertRecord.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpsertRecord.tsx index 01566ffa17..413233dc1c 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpsertRecord.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpsertRecord.tsx @@ -1,9 +1,37 @@ +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition'; +import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput'; +import { FormSingleRecordPicker } from '@/object-record/record-field/ui/form-types/components/FormSingleRecordPicker'; +import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation'; +import { Select } from '@/ui/input/components/Select'; +import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; +import { useViewOrDefaultViewFromPrefetchedViews } from '@/views/hooks/useViewOrDefaultViewFromPrefetchedViews'; +import { WorkflowFieldsMultiSelect } from '@/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect'; import { type WorkflowUpsertRecordAction } from '@/workflow/types/Workflow'; +import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter'; -import { - WorkflowCreateRecordBody, - type CreateRecordFormData, -} from '@/workflow/workflow-steps/workflow-actions/components/WorkflowCreateRecordBody'; +import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField'; +import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; +import { useTheme } from '@emotion/react'; +import { t } from '@lingui/core/macro'; +import { useEffect, useState } from 'react'; +import { isDefined } from 'twenty-shared/utils'; +import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow'; +import { HorizontalSeparator, useIcons } from 'twenty-ui/display'; +import { type SelectOption } from 'twenty-ui/input'; +import { type JsonValue } from 'type-fest'; +import { useDebouncedCallback } from 'use-debounce'; +import { RelationType } from '~/generated-metadata/graphql'; + +type RelationManyToOneField = { + id: string; +}; + +type UpsertRecordFormData = { + objectName: string; + [field: string]: RelationManyToOneField | JsonValue; +}; type WorkflowEditActionUpsertRecordProps = { action: WorkflowUpsertRecordAction; @@ -17,40 +45,269 @@ type WorkflowEditActionUpsertRecordProps = { }; }; +const sortFieldsWithIdFirst = ( + a: { name: string; viewFieldPosition?: number }, + b: { name: string; viewFieldPosition?: number }, +) => { + if (a.name === 'id') { + return -1; + } + + if (b.name === 'id') { + return 1; + } + + if (isDefined(a.viewFieldPosition) && isDefined(b.viewFieldPosition)) { + return a.viewFieldPosition - b.viewFieldPosition; + } + + if (isDefined(a.viewFieldPosition)) { + return -1; + } + + if (isDefined(b.viewFieldPosition)) { + return 1; + } + + return 0; +}; + export const WorkflowEditActionUpsertRecord = ({ action, actionOptions, }: WorkflowEditActionUpsertRecordProps) => { + const theme = useTheme(); + + const { getIcon } = useIcons(); + + const { activeNonSystemObjectMetadataItems } = + useFilteredObjectMetadataItems(); + + const availableMetadata: Array> = + activeNonSystemObjectMetadataItems + .filter((objectMetadataItem) => + canObjectBeManagedByWorkflow({ + nameSingular: objectMetadataItem.nameSingular, + isSystem: objectMetadataItem.isSystem, + }), + ) + .map((item) => ({ + Icon: getIcon(item.icon), + label: item.labelPlural, + value: item.nameSingular, + })); + + const [formData, setFormData] = useState({ + objectName: action.settings.input.objectName, + ...action.settings.input.objectRecord, + }); + const isFormDisabled = actionOptions.readonly === true; - const handleUpdate = (formData: CreateRecordFormData) => { - if (actionOptions.readonly === true) { + const objectNameSingular = formData.objectName; + + const { objectMetadataItems } = useObjectMetadataItems(); + + const objectMetadataItem = objectMetadataItems.find( + (item) => item.nameSingular === objectNameSingular, + ); + + const objectLabelSingular = objectMetadataItem?.labelSingular; + + const { view: indexView } = useViewOrDefaultViewFromPrefetchedViews({ + objectMetadataItemId: objectMetadataItem?.id ?? '', + }); + + const viewFields = indexView?.viewFields ?? []; + + const inlineFieldMetadataItems = objectMetadataItem?.fields + .filter((fieldMetadataItem) => + shouldDisplayFormField({ + fieldMetadataItem, + actionType: 'UPSERT_RECORD', + }), + ) + .map((fieldMetadataItem) => { + const viewField = viewFields.find( + (viewField) => viewField.fieldMetadataId === fieldMetadataItem.id, + ); + return { + ...fieldMetadataItem, + viewFieldPosition: viewField?.position, + }; + }) + .sort(sortFieldsWithIdFirst); + + const uniqueFieldMetadataItems = inlineFieldMetadataItems?.filter( + (fieldMetadataItem) => + fieldMetadataItem.isUnique || fieldMetadataItem.name === 'id', + ); + + const inlineFieldDefinitions = isDefined(objectMetadataItem) + ? inlineFieldMetadataItems?.map((fieldMetadataItem) => + formatFieldMetadataItemAsFieldDefinition({ + field: fieldMetadataItem, + objectMetadataItem, + showLabel: true, + labelWidth: 90, + }), + ) + : []; + + const handleFieldChange = ( + fieldName: keyof UpsertRecordFormData, + updatedValue: JsonValue, + ) => { + const fieldDefinition = inlineFieldDefinitions?.find( + (definition) => definition.metadata.fieldName === fieldName, + ); + + if (!isDefined(fieldDefinition)) { return; } - const { objectName: updatedObjectName, ...updatedOtherFields } = formData; + const isFieldRelationManyToOne = + isFieldRelation(fieldDefinition) && + fieldDefinition.metadata.relationType === RelationType.MANY_TO_ONE; - actionOptions.onActionUpdate({ - ...action, - settings: { - ...action.settings, - input: { - objectName: updatedObjectName, - objectRecord: updatedOtherFields, - }, - }, - }); + const fieldValue = isFieldRelationManyToOne + ? { + id: updatedValue, + } + : updatedValue; + + const newFormData: UpsertRecordFormData = { + ...formData, + [fieldName]: fieldValue, + }; + + setFormData(newFormData); + + saveAction(newFormData); }; + const saveAction = useDebouncedCallback( + async (formData: UpsertRecordFormData) => { + if (actionOptions.readonly === true) { + return; + } + + const { objectName: updatedObjectName, ...updatedOtherFields } = formData; + + actionOptions.onActionUpdate({ + ...action, + settings: { + ...action.settings, + input: { + objectName: updatedObjectName, + objectRecord: updatedOtherFields, + }, + }, + }); + }, + 1_000, + ); + + useEffect(() => { + return () => { + saveAction.flush(); + }; + }, [saveAction]); + return ( <> - + +