diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx index 16a51164a88f..0c884b2035a0 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx +++ b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx @@ -171,15 +171,10 @@ export const ActivityTargetInlineCellEditMode = ({ }); }; - const handleCancel = () => { - closeEditableField(); - }; - return ( diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts index 3d06b28fa1f6..0c1cc9a3dae1 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts @@ -37,6 +37,8 @@ export const formatFieldMetadataItemAsFieldDefinition = ({ relationObjectMetadataNamePlural: relationObjectMetadataItem?.namePlural ?? '', objectMetadataNameSingular: objectMetadataItem.nameSingular ?? '', + targetFieldMetadataName: + field.relationDefinition?.targetFieldMetadata?.name ?? '', options: field.options, }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx index 42ae3153d060..ce1240e6449c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx @@ -6,6 +6,7 @@ import { FullNameFieldInput } from '@/object-record/record-field/meta-types/inpu import { LinksFieldInput } from '@/object-record/record-field/meta-types/input/components/LinksFieldInput'; import { MultiSelectFieldInput } from '@/object-record/record-field/meta-types/input/components/MultiSelectFieldInput'; import { RawJsonFieldInput } from '@/object-record/record-field/meta-types/input/components/RawJsonFieldInput'; +import { RelationManyFieldInput } from '@/object-record/record-field/meta-types/input/components/RelationManyFieldInput'; import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput'; import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope'; import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate'; @@ -14,6 +15,7 @@ import { isFieldFullName } from '@/object-record/record-field/types/guards/isFie import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks'; import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect'; import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson'; +import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects'; import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; @@ -71,7 +73,15 @@ export const FieldInput = ({ recordFieldInputScopeId={getScopeIdFromComponentId(recordFieldInputdId)} > {isFieldRelation(fieldDefinition) ? ( - + isFieldRelationFromManyObjects(fieldDefinition) ? ( + + ) : ( + + ) ) : isFieldPhone(fieldDefinition) || isFieldDisplayedAsPhone(fieldDefinition) ? ( { isFieldRelation(fieldDefinition) && isFieldRelationValue(valueToPersist); + const fieldIsRelationFromManyObjects = + isFieldRelationFromManyObjects( + fieldDefinition as FieldDefinition, + ) && isFieldRelationValue(valueToPersist); + const fieldIsText = isFieldText(fieldDefinition) && isFieldTextValue(valueToPersist); @@ -111,7 +120,7 @@ export const usePersistField = () => { isFieldRawJsonValue(valueToPersist); const isValuePersistable = - fieldIsRelation || + (fieldIsRelation && !fieldIsRelationFromManyObjects) || fieldIsText || fieldIsBoolean || fieldIsEmail || @@ -136,13 +145,14 @@ export const usePersistField = () => { valueToPersist, ); - if (fieldIsRelation) { + if (fieldIsRelation && !fieldIsRelationFromManyObjects) { + const value = valueToPersist as EntityForSelect; updateRecord?.({ variables: { where: { id: entityId }, updateOneRecordInput: { - [fieldName]: valueToPersist, - [`${fieldName}Id`]: valueToPersist?.id ?? null, + [fieldName]: value, + [`${fieldName}Id`]: value?.id ?? null, }, }, }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFieldDisplay.tsx index 87c14b583862..dace0123781c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFieldDisplay.tsx @@ -1,6 +1,10 @@ +import { isArray } from '@sniptt/guards'; import { EntityChip } from 'twenty-ui'; +import { RelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay'; import { useRelationFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFieldDisplay'; +import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64'; export const RelationFieldDisplay = () => { @@ -14,6 +18,12 @@ export const RelationFieldDisplay = () => { return null; } + if (isArray(fieldValue) && isFieldRelationFromManyObjects(fieldDefinition)) { + return ( + + ); + } + const recordChipData = generateRecordChipData(fieldValue); return ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx new file mode 100644 index 000000000000..d498faaa2966 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay.tsx @@ -0,0 +1,37 @@ +import { EntityChip } from 'twenty-ui'; + +import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus'; +import { useRelationFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRelationFieldDisplay'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList'; +import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64'; + +export const RelationFromManyFieldDisplay = ({ + fieldValue, +}: { + fieldValue: ObjectRecord[]; +}) => { + const { isFocused } = useFieldFocus(); + const { generateRecordChipData } = useRelationFieldDisplay(); + + const recordChipsData = fieldValue.map((fieldValueItem) => + generateRecordChipData(fieldValueItem), + ); + + return ( + + {recordChipsData.map((record) => { + return ( + + ); + })} + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RelationFromManyFieldDisplay.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RelationFromManyFieldDisplay.perf.stories.tsx new file mode 100644 index 000000000000..360abcf4c3b0 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RelationFromManyFieldDisplay.perf.stories.tsx @@ -0,0 +1,94 @@ +import { useEffect } from 'react'; +import { Meta, StoryObj } from '@storybook/react'; +import { useSetRecoilState } from 'recoil'; +import { ComponentDecorator } from 'twenty-ui'; + +import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { RelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay'; +import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; +import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; +import { + RecordFieldValueSelectorContextProvider, + useSetRecordValue, +} from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; +import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory'; + +import { + fieldValue, + relationFromManyFieldDisplayMock, +} from './relationFromManyFieldDisplayMock'; + +const RelationFieldValueSetterEffect = () => { + const setEntity = useSetRecoilState( + recordStoreFamilyState(relationFromManyFieldDisplayMock.entityId), + ); + + const setRelationEntity = useSetRecoilState( + recordStoreFamilyState(relationFromManyFieldDisplayMock.relationEntityId), + ); + + const setRecordValue = useSetRecordValue(); + + useEffect(() => { + setEntity(relationFromManyFieldDisplayMock.entityValue); + setRelationEntity(relationFromManyFieldDisplayMock.relationFieldValue); + + setRecordValue( + relationFromManyFieldDisplayMock.entityValue.id, + relationFromManyFieldDisplayMock.entityValue, + ); + setRecordValue( + relationFromManyFieldDisplayMock.relationFieldValue.id, + relationFromManyFieldDisplayMock.relationFieldValue, + ); + }, [setEntity, setRelationEntity, setRecordValue]); + + return null; +}; + +const meta: Meta = { + title: 'UI/Data/Field/Display/RelationFromManyFieldDisplay', + decorators: [ + MemoryRouterDecorator, + (Story) => ( + + , + hotkeyScope: 'hotkey-scope', + }} + > + + + + + ), + ComponentDecorator, + ], + component: RelationFromManyFieldDisplay, + argTypes: { value: { control: 'date' } }, + args: { fieldValue: fieldValue }, + parameters: { + chromatic: { disableSnapshot: true }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Performance = getProfilingStory({ + componentName: 'RelationFromManyFieldDisplay', + averageThresholdInMs: 0.5, + numberOfRuns: 20, + numberOfTestsPerRun: 100, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/relationFromManyFieldDisplayMock.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/relationFromManyFieldDisplayMock.ts new file mode 100644 index 000000000000..cbc63cb40be5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/relationFromManyFieldDisplayMock.ts @@ -0,0 +1,172 @@ +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +export const fieldValue = [ + { + __typename: 'Company', + domainName: 'google.com', + xLink: { + __typename: 'Link', + label: '', + url: '', + }, + name: 'Google', + annualRecurringRevenue: { + __typename: 'Currency', + amountMicros: null, + currencyCode: '', + }, + employees: null, + accountOwnerId: null, + address: '', + idealCustomerProfile: false, + createdAt: '2024-05-01T13:16:29.046Z', + id: '20202020-c21e-4ec2-873b-de4264d89025', + position: 6, + updatedAt: '2024-05-01T13:16:29.046Z', + linkedinLink: { + __typename: 'Link', + label: '', + url: '', + }, + }, + { + __typename: 'Company', + domainName: 'airbnb.com', + xLink: { + __typename: 'Link', + label: '', + url: '', + }, + name: 'Airbnb', + annualRecurringRevenue: { + __typename: 'Currency', + amountMicros: null, + currencyCode: '', + }, + employees: null, + accountOwnerId: null, + address: '', + idealCustomerProfile: false, + createdAt: '2024-05-01T13:16:29.046Z', + id: '20202020-171e-4bcc-9cf7-43448d6fb278', + position: 6, + updatedAt: '2024-05-01T13:16:29.046Z', + linkedinLink: { + __typename: 'Link', + label: '', + url: '', + }, + }, +]; + +export const relationFromManyFieldDisplayMock = { + entityId: '20202020-2d40-4e49-8df4-9c6a049191df', + relationEntityId: '20202020-c21e-4ec2-873b-de4264d89025', + entityValue: { + __typename: 'Person', + asd: '', + city: 'Seattle', + jobTitle: '', + name: { + __typename: 'FullName', + firstName: 'Lorie', + lastName: 'Vladim', + }, + createdAt: '2024-05-01T13:16:29.046Z', + company: { + __typename: 'Company', + domainName: 'google.com', + xLink: { + __typename: 'Link', + label: '', + url: '', + }, + name: 'Google', + annualRecurringRevenue: { + __typename: 'Currency', + amountMicros: null, + currencyCode: '', + }, + employees: null, + accountOwnerId: null, + address: '', + idealCustomerProfile: false, + createdAt: '2024-05-01T13:16:29.046Z', + id: '20202020-c21e-4ec2-873b-de4264d89025', + position: 6, + updatedAt: '2024-05-01T13:16:29.046Z', + linkedinLink: { + __typename: 'Link', + label: '', + url: '', + }, + }, + id: '20202020-2d40-4e49-8df4-9c6a049191df', + email: 'lorie.vladim@google.com', + phone: '+33788901235', + linkedinLink: { + __typename: 'Link', + label: '', + url: '', + }, + xLink: { + __typename: 'Link', + label: '', + url: '', + }, + tEst: '', + position: 15, + }, + relationFieldValue: { + __typename: 'Company', + domainName: 'microsoft.com', + xLink: { + __typename: 'Link', + label: '', + url: '', + }, + name: 'Microsoft', + annualRecurringRevenue: { + __typename: 'Currency', + amountMicros: null, + currencyCode: '', + }, + employees: null, + accountOwnerId: null, + address: '', + idealCustomerProfile: false, + createdAt: '2024-05-01T13:16:29.046Z', + id: '20202020-ed89-413a-b31a-962986e67bb4', + position: 4, + updatedAt: '2024-05-01T13:16:29.046Z', + linkedinLink: { + __typename: 'Link', + label: '', + url: '', + }, + }, + fieldDefinition: { + fieldMetadataId: '4e79f0b7-d100-4e89-a07b-315a710b8059', + label: 'Company', + metadata: { + fieldName: 'company', + placeHolder: 'Company', + relationType: 'FROM_MANY_OBJECTS', + relationFieldMetadataId: '01fa2247-7937-4493-b7e2-3d72f05d6d25', + relationObjectMetadataNameSingular: 'company', + relationObjectMetadataNamePlural: 'companies', + objectMetadataNameSingular: 'person', + options: null, + }, + type: FieldMetadataType.Relation, + iconName: 'IconUsers', + defaultValue: null, + editButtonIcon: { + propTypes: {}, + }, + position: 3, + size: 100, + isLabelIdentifier: false, + isVisible: true, + }, +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationField.ts index f61b22df666d..980f4c050205 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationField.ts @@ -5,14 +5,16 @@ import { useGetButtonIcon } from '@/object-record/record-field/hooks/useGetButto import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput'; import { FieldRelationValue } from '@/object-record/record-field/types/FieldMetadata'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; +import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldContext } from '../../contexts/FieldContext'; import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata'; import { isFieldRelation } from '../../types/guards/isFieldRelation'; -// TODO: we will be able to type more precisely when we will have custom field and custom entities support -export const useRelationField = () => { +export const useRelationField = < + T extends EntityForSelect | EntityForSelect[], +>() => { const { entityId, fieldDefinition, maxWidth } = useContext(FieldContext); const button = useGetButtonIcon(); @@ -24,11 +26,11 @@ export const useRelationField = () => { const fieldName = fieldDefinition.metadata.fieldName; - const [fieldValue, setFieldValue] = useRecoilState( + const [fieldValue, setFieldValue] = useRecoilState>( recordStoreFamilySelector({ recordId: entityId, fieldName }), ); - const { getDraftValueSelector } = useRecordFieldInput( + const { getDraftValueSelector } = useRecordFieldInput>( `${entityId}-${fieldName}`, ); const draftValue = useRecoilValue(getDraftValueSelector()); @@ -41,5 +43,6 @@ export const useRelationField = () => { initialSearchValue, setFieldValue, maxWidth: button && maxWidth ? maxWidth - 28 : maxWidth, + entityId, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFieldInput.tsx index ec80fd26c1df..ed752bef9130 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFieldInput.tsx @@ -24,7 +24,7 @@ export const RelationFieldInput = ({ onCancel, }: RelationFieldInputProps) => { const { fieldDefinition, initialSearchValue, fieldValue } = - useRelationField(); + useRelationField(); const persistField = usePersistField(); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationManyFieldInput.tsx new file mode 100644 index 000000000000..9e3629528463 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationManyFieldInput.tsx @@ -0,0 +1,82 @@ +import { useMemo } from 'react'; + +import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useUpdateRelationManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationManyFieldInput'; +import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell'; +import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect'; +import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker'; +import { useRelationPickerEntitiesOptions } from '@/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions'; +import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; + +import { useRelationField } from '../../hooks/useRelationField'; + +export const RelationManyFieldInput = ({ + relationPickerScopeId = 'relation-picker', +}: { + relationPickerScopeId?: string; +}) => { + const { closeInlineCell: closeEditableField } = useInlineCell(); + + const { fieldDefinition, fieldValue } = useRelationField(); + const { entities, relationPickerSearchFilter } = + useRelationPickerEntitiesOptions({ + relationObjectNameSingular: + fieldDefinition.metadata.relationObjectMetadataNameSingular, + relationPickerScopeId, + }); + + const { setRelationPickerSearchFilter } = useRelationPicker({ + relationPickerScopeId, + }); + + const { handleChange } = useUpdateRelationManyFieldInput({ entities }); + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: + fieldDefinition.metadata.relationObjectMetadataNameSingular, + }); + const allRecords = useMemo( + () => [ + ...entities.entitiesToSelect.map((entity) => { + const { record, ...recordIdentifier } = entity; + return { + objectMetadataItem: objectMetadataItem, + record: record, + recordIdentifier: recordIdentifier, + }; + }), + ], + [entities.entitiesToSelect, objectMetadataItem], + ); + + const selectedRecords = useMemo( + () => + allRecords.filter( + (entity) => + fieldValue?.some((f) => { + return f.id === entity.recordIdentifier.id; + }), + ), + [allRecords, fieldValue], + ); + + return ( + <> + + { + closeEditableField(); + }} + onChange={handleChange} + /> + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationManyFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationManyFieldInput.stories.tsx new file mode 100644 index 000000000000..b768056cb87f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationManyFieldInput.stories.tsx @@ -0,0 +1,86 @@ +import { useEffect } from 'react'; +import { Meta, StoryObj } from '@storybook/react'; +import { useSetRecoilState } from 'recoil'; + +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { RelationManyFieldInput } from '@/object-record/record-field/meta-types/input/components/RelationManyFieldInput'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { FieldMetadataType } from '~/generated/graphql'; +import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator'; +import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; +import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; +import { graphqlMocks } from '~/testing/graphqlMocks'; +import { + mockDefaultWorkspace, + mockedWorkspaceMemberData, +} from '~/testing/mock-data/users'; + +import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; + +const RelationWorkspaceSetterEffect = () => { + const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState); + const setCurrentWorkspaceMember = useSetRecoilState( + currentWorkspaceMemberState, + ); + + useEffect(() => { + setCurrentWorkspace(mockDefaultWorkspace); + setCurrentWorkspaceMember(mockedWorkspaceMemberData); + }, [setCurrentWorkspace, setCurrentWorkspaceMember]); + + return <>; +}; + +const RelationManyFieldInputWithContext = () => { + const setHotKeyScope = useSetHotkeyScope(); + + useEffect(() => { + setHotKeyScope('hotkey-scope'); + }, [setHotKeyScope]); + + return ( +
+ + + + +
+
+ ); +}; +const meta: Meta = { + title: 'UI/Data/Field/Input/RelationManyFieldInput', + component: RelationManyFieldInputWithContext, + args: {}, + decorators: [ObjectMetadataItemsDecorator, SnackBarDecorator], + parameters: { + clearMocks: true, + msw: graphqlMocks, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + decorators: [ComponentWithRecoilScopeDecorator], +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useUpdateRelationManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useUpdateRelationManyFieldInput.tsx new file mode 100644 index 000000000000..a0d274418af5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/hooks/useUpdateRelationManyFieldInput.tsx @@ -0,0 +1,52 @@ +import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; +import { useRelationField } from '@/object-record/record-field/meta-types/hooks/useRelationField'; +import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch'; +import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect'; +import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; +import { isDefined } from '~/utils/isDefined'; + +export const useUpdateRelationManyFieldInput = ({ + entities, +}: { + entities: EntitiesForMultipleEntitySelect; +}) => { + const { fieldDefinition, fieldValue, setFieldValue, entityId } = + useRelationField(); + + const { updateOneRecord } = useUpdateOneRecord({ + objectNameSingular: + fieldDefinition.metadata.relationObjectMetadataNameSingular, + }); + + const fieldName = fieldDefinition.metadata.targetFieldMetadataName; + + const handleChange = ( + objectRecord: ObjectRecordForSelect | null, + isSelected: boolean, + ) => { + const entityToAddOrRemove = entities.entitiesToSelect.find( + (entity) => entity.id === objectRecord?.recordIdentifier.id, + ); + + const updatedFieldValue = isSelected + ? [...(fieldValue ?? []), entityToAddOrRemove] + : (fieldValue ?? []).filter( + (value) => value.id !== objectRecord?.recordIdentifier.id, + ); + setFieldValue( + updatedFieldValue.filter((value) => + isDefined(value), + ) as EntityForSelect[], + ); + if (isDefined(objectRecord)) { + updateOneRecord({ + idToUpdate: objectRecord.record?.id, + updateOneRecordInput: { + [`${fieldName}Id`]: isSelected ? entityId : null, + }, + }); + } + }; + + return { handleChange }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputDraftValue.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputDraftValue.ts index 76a232a142b2..0282ccd02f86 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputDraftValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputDraftValue.ts @@ -18,6 +18,7 @@ import { FieldTextValue, FieldUUidValue, } from '@/object-record/record-field/types/FieldMetadata'; +import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; export type FieldTextDraftValue = string; export type FieldNumberDraftValue = string; @@ -78,7 +79,9 @@ export type FieldInputDraftValue = FieldValue extends FieldTextValue ? FieldSelectDraftValue : FieldValue extends FieldMultiSelectValue ? FieldMultiSelectDraftValue - : FieldValue extends FieldRelationValue + : FieldValue extends + | FieldRelationValue + | FieldRelationValue ? FieldRelationDraftValue : FieldValue extends FieldAddressValue ? FieldAddressDraftValue diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts index 8a287bd6af1a..f54794613652 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts @@ -106,6 +106,7 @@ export type FieldRelationMetadata = { relationObjectMetadataNamePlural: string; relationObjectMetadataNameSingular: string; relationType?: FieldDefinitionRelationType; + targetFieldMetadataName?: string; useEditButton?: boolean; }; @@ -173,7 +174,8 @@ export type FieldRatingValue = (typeof RATING_VALUES)[number]; export type FieldSelectValue = string | null; export type FieldMultiSelectValue = string[] | null; -export type FieldRelationValue = EntityForSelect | null; +export type FieldRelationValue = + T | null; // See https://zod.dev/?id=json-type type Literal = string | number | boolean | null; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRelationFromManyObjects.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRelationFromManyObjects.ts new file mode 100644 index 000000000000..03b56892afd6 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRelationFromManyObjects.ts @@ -0,0 +1,10 @@ +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +import { FieldDefinition } from '../FieldDefinition'; +import { FieldRelationMetadata } from '../FieldMetadata'; + +export const isFieldRelationFromManyObjects = ( + field: Pick, 'type' | 'metadata'>, +): field is FieldDefinition => + field.type === FieldMetadataType.Relation && + field.metadata.relationType === 'FROM_MANY_OBJECTS'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRelationValue.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRelationValue.ts index 879437f9164c..1919c493e327 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRelationValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRelationValue.ts @@ -1,9 +1,13 @@ import { isNull, isObject, isUndefined } from '@sniptt/guards'; +import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; + import { FieldRelationValue } from '../FieldMetadata'; // TODO: add zod -export const isFieldRelationValue = ( +export const isFieldRelationValue = < + T extends EntityForSelect | EntityForSelect[], +>( fieldValue: unknown, -): fieldValue is FieldRelationValue => +): fieldValue is FieldRelationValue => !isUndefined(fieldValue) && (isObject(fieldValue) || isNull(fieldValue)); diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx new file mode 100644 index 000000000000..049176e6cb08 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx @@ -0,0 +1,163 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import styled from '@emotion/styled'; +import { isNonEmptyString } from '@sniptt/guards'; +import { useDebouncedCallback } from 'use-debounce'; + +import { MultipleObjectRecordOnClickOutsideEffect } from '@/object-record/relation-picker/components/MultipleObjectRecordOnClickOutsideEffect'; +import { MultipleObjectRecordSelectItem } from '@/object-record/relation-picker/components/MultipleObjectRecordSelectItem'; +import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId'; +import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch'; +import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; +import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; +import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; +import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; +import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import { isDefined } from '~/utils/isDefined'; + +export const StyledSelectableItem = styled(SelectableItem)` + height: 100%; + width: 100%; +`; +export const MultiRecordSelect = ({ + onChange, + onSubmit, + selectedObjectRecords, + allRecords, + loading, + searchFilter, + setSearchFilter, +}: { + onChange?: ( + changedRecordForSelect: ObjectRecordForSelect, + newSelectedValue: boolean, + ) => void; + onSubmit?: (objectRecordsForSelect: ObjectRecordForSelect[]) => void; + selectedObjectRecords: ObjectRecordForSelect[]; + allRecords: ObjectRecordForSelect[]; + loading: boolean; + searchFilter: string; + setSearchFilter: (searchFilter: string) => void; +}) => { + const containerRef = useRef(null); + + const [internalSelectedRecords, setInternalSelectedRecords] = useState< + ObjectRecordForSelect[] + >([]); + + useEffect(() => { + if (!loading) { + setInternalSelectedRecords(selectedObjectRecords); + } + }, [selectedObjectRecords, loading]); + + const debouncedSetSearchFilter = useDebouncedCallback(setSearchFilter, 100, { + leading: true, + }); + + const handleFilterChange = (event: React.ChangeEvent) => { + debouncedSetSearchFilter(event.currentTarget.value); + }; + + const handleSelectChange = ( + changedRecordForSelect: ObjectRecordForSelect, + newSelectedValue: boolean, + ) => { + const newSelectedRecords = newSelectedValue + ? [...internalSelectedRecords, changedRecordForSelect] + : internalSelectedRecords.filter( + (selectedRecord) => + selectedRecord.record.id !== changedRecordForSelect.record.id, + ); + + setInternalSelectedRecords(newSelectedRecords); + + onChange?.(changedRecordForSelect, newSelectedValue); + }; + + const entitiesInDropdown = useMemo( + () => + [...(allRecords ?? [])].filter((entity) => + isNonEmptyString(entity.recordIdentifier.id), + ), + [allRecords], + ); + + const selectableItemIds = entitiesInDropdown.map( + (entity) => entity.record.id, + ); + + return ( + <> + { + onSubmit?.(internalSelectedRecords); + }} + /> + + + + + {loading ? ( + + ) : ( + <> + { + const recordIsSelected = internalSelectedRecords?.some( + (selectedRecord) => selectedRecord.record.id === recordId, + ); + + const correspondingRecordForSelect = entitiesInDropdown?.find( + (entity) => entity.record.id === recordId, + ); + + if (isDefined(correspondingRecordForSelect)) { + handleSelectChange( + correspondingRecordForSelect, + !recordIsSelected, + ); + } + }} + > + {entitiesInDropdown?.map((objectRecordForSelect) => ( + + handleSelectChange( + objectRecordForSelect, + newSelectedValue, + ) + } + selected={internalSelectedRecords?.some( + (selectedRecord) => { + return ( + selectedRecord.record.id === + objectRecordForSelect.record.id + ); + }, + )} + /> + ))} + + {entitiesInDropdown?.length === 0 && ( + + )} + + )} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelect.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelect.tsx index 44932a8ea774..659461a30d57 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultipleObjectRecordSelect.tsx @@ -1,37 +1,18 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useMemo, useState } from 'react'; import styled from '@emotion/styled'; -import { isNonEmptyString } from '@sniptt/guards'; -import { useDebouncedCallback } from 'use-debounce'; -import { MultipleObjectRecordOnClickOutsideEffect } from '@/object-record/relation-picker/components/MultipleObjectRecordOnClickOutsideEffect'; -import { MultipleObjectRecordSelectItem } from '@/object-record/relation-picker/components/MultipleObjectRecordSelectItem'; -import { MULTI_OBJECT_RECORD_SELECT_SELECTABLE_LIST_ID } from '@/object-record/relation-picker/constants/MultiObjectRecordSelectSelectableListId'; +import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect'; import { ObjectRecordForSelect, SelectedObjectRecordId, useMultiObjectSearch, } from '@/object-record/relation-picker/hooks/useMultiObjectSearch'; -import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; -import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; -import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; -import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; -import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; -import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; -import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; -import { isDefined } from '~/utils/isDefined'; export const StyledSelectableItem = styled(SelectableItem)` height: 100%; width: 100%; `; - -export type EntitiesForMultipleObjectRecordSelect = { - filteredSelectedObjectRecords: ObjectRecordForSelect[]; - objectRecordsToSelect: ObjectRecordForSelect[]; - loading: boolean; -}; - export const MultipleObjectRecordSelect = ({ onChange, onSubmit, @@ -41,12 +22,9 @@ export const MultipleObjectRecordSelect = ({ changedRecordForSelect: ObjectRecordForSelect, newSelectedValue: boolean, ) => void; - onCancel?: (objectRecordsForSelect: ObjectRecordForSelect[]) => void; onSubmit?: (objectRecordsForSelect: ObjectRecordForSelect[]) => void; selectedObjectRecordIds: SelectedObjectRecordId[]; }) => { - const containerRef = useRef(null); - const [searchFilter, setSearchFilter] = useState(''); const { @@ -73,122 +51,18 @@ export const MultipleObjectRecordSelect = ({ [selectedObjectRecords, selectedObjectRecordIds], ); - const [internalSelectedRecords, setInternalSelectedRecords] = useState< - ObjectRecordForSelect[] - >([]); - - useEffect(() => { - if (!loading) { - setInternalSelectedRecords(selectedObjectRecordsForSelect); - } - }, [selectedObjectRecordsForSelect, loading]); - - const debouncedSetSearchFilter = useDebouncedCallback(setSearchFilter, 100, { - leading: true, - }); - - const handleFilterChange = (event: React.ChangeEvent) => { - debouncedSetSearchFilter(event.currentTarget.value); - }; - - const handleSelectChange = ( - changedRecordForSelect: ObjectRecordForSelect, - newSelectedValue: boolean, - ) => { - const newSelectedRecords = newSelectedValue - ? [...internalSelectedRecords, changedRecordForSelect] - : internalSelectedRecords.filter( - (selectedRecord) => - selectedRecord.record.id !== changedRecordForSelect.record.id, - ); - - setInternalSelectedRecords(newSelectedRecords); - - onChange?.(changedRecordForSelect, newSelectedValue); - }; - - const entitiesInDropdown = useMemo( - () => - [ + return ( + isNonEmptyString(entity.recordIdentifier.id)), - [filteredSelectedObjectRecords, objectRecordsToSelect], - ); - - const selectableItemIds = entitiesInDropdown.map( - (entity) => entity.record.id, - ); - - return ( - <> - { - onSubmit?.(internalSelectedRecords); - }} - /> - - - - - {loading ? ( - - ) : ( - <> - { - const recordIsSelected = internalSelectedRecords?.some( - (selectedRecord) => selectedRecord.record.id === recordId, - ); - - const correspondingRecordForSelect = entitiesInDropdown?.find( - (entity) => entity.record.id === recordId, - ); - - if (isDefined(correspondingRecordForSelect)) { - handleSelectChange( - correspondingRecordForSelect, - !recordIsSelected, - ); - } - }} - > - {entitiesInDropdown?.map((objectRecordForSelect) => ( - - handleSelectChange( - objectRecordForSelect, - newSelectedValue, - ) - } - selected={internalSelectedRecords?.some( - (selectedRecord) => { - return ( - selectedRecord.record.id === - objectRecordForSelect.record.id - ); - }, - )} - /> - ))} - - {entitiesInDropdown?.length === 0 && ( - - )} - - )} - - - + ]} + loading={loading} + searchFilter={searchFilter} + setSearchFilter={setSearchFilter} + /> ); }; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx index 09752cef8b5e..70c777c7bfd3 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx @@ -1,12 +1,9 @@ -import { useRecoilValue } from 'recoil'; - import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect'; import { SingleEntitySelectMenuItems, SingleEntitySelectMenuItemsProps, } from '@/object-record/relation-picker/components/SingleEntitySelectMenuItems'; -import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates'; -import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery'; +import { useRelationPickerEntitiesOptions } from '@/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { isDefined } from '~/utils/isDefined'; @@ -44,32 +41,16 @@ export const SingleEntitySelectMenuItemsWithSearch = ({ relationPickerScopeId, }); - const { searchQueryState, relationPickerSearchFilterState } = - useRelationPickerScopedStates({ - relationPickerScopedId: relationPickerScopeId, + const { entities, relationPickerSearchFilter } = + useRelationPickerEntitiesOptions({ + relationObjectNameSingular, + relationPickerScopeId, + selectedRelationRecordIds, + excludedRelationRecordIds, }); - const searchQuery = useRecoilValue(searchQueryState); - const relationPickerSearchFilter = useRecoilValue( - relationPickerSearchFilterState, - ); - const showCreateButton = isDefined(onCreate); - const entities = useFilteredSearchEntityQuery({ - filters: [ - { - fieldNames: - searchQuery?.computeFilterFields?.(relationObjectNameSingular) ?? [], - filter: relationPickerSearchFilter, - }, - ], - orderByField: 'createdAt', - selectedIds: selectedRelationRecordIds, - excludeEntityIds: excludedRelationRecordIds, - objectNameSingular: relationObjectNameSingular, - }); - let onCreateWithInput = undefined; if (isDefined(onCreate)) { diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useRelationPicker.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useRelationPicker.ts index b327099bb099..3e74f0edafbf 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useRelationPicker.ts +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useRelationPicker.ts @@ -1,4 +1,4 @@ -import { useRecoilState, useSetRecoilState } from 'recoil'; +import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates'; import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext'; @@ -28,6 +28,10 @@ export const useRelationPicker = (props?: useRelationPickeProps) => { relationPickerSearchFilterState, ); + const relationPickerSearchFilter = useRecoilValue( + relationPickerSearchFilterState, + ); + const [relationPickerPreselectedId, setRelationPickerPreselectedId] = useRecoilState(relationPickerPreselectedIdState); @@ -37,5 +41,6 @@ export const useRelationPicker = (props?: useRelationPickeProps) => { setRelationPickerSearchFilter, relationPickerPreselectedId, setRelationPickerPreselectedId, + relationPickerSearchFilter, }; }; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions.ts new file mode 100644 index 000000000000..9eb5f990c622 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions.ts @@ -0,0 +1,41 @@ +import { useRecoilValue } from 'recoil'; + +import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates'; +import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery'; + +export const useRelationPickerEntitiesOptions = ({ + relationObjectNameSingular, + relationPickerScopeId = 'relation-picker', + selectedRelationRecordIds = [], + excludedRelationRecordIds = [], +}: { + relationObjectNameSingular: string; + relationPickerScopeId?: string; + selectedRelationRecordIds?: string[]; + excludedRelationRecordIds?: string[]; +}) => { + const { searchQueryState, relationPickerSearchFilterState } = + useRelationPickerScopedStates({ + relationPickerScopedId: relationPickerScopeId, + }); + const relationPickerSearchFilter = useRecoilValue( + relationPickerSearchFilterState, + ); + + const searchQuery = useRecoilValue(searchQueryState); + const entities = useFilteredSearchEntityQuery({ + filters: [ + { + fieldNames: + searchQuery?.computeFilterFields?.(relationObjectNameSingular) ?? [], + filter: relationPickerSearchFilter, + }, + ], + orderByField: 'createdAt', + selectedIds: selectedRelationRecordIds, + excludeEntityIds: excludedRelationRecordIds, + objectNameSingular: relationObjectNameSingular, + }); + + return { entities, relationPickerSearchFilter }; +}; diff --git a/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts b/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts index 57c0d193c2cf..fce402359687 100644 --- a/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts +++ b/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts @@ -3,6 +3,7 @@ import { isString } from '@sniptt/guards'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { isFieldRelationValue } from '@/object-record/record-field/types/guards/isFieldRelationValue'; +import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { FieldMetadataType } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; @@ -30,7 +31,7 @@ export const sanitizeRecordInput = ({ if ( fieldMetadataItem.type === FieldMetadataType.Relation && - isFieldRelationValue(fieldValue) + isFieldRelationValue(fieldValue) ) { const relationIdFieldName = `${fieldMetadataItem.name}Id`; const relationIdFieldMetadataItem = objectMetadataItem.fields.find(