From 84f1559832b7ead981dbf7cd810db6e6d889446f Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Tue, 7 Oct 2025 20:07:27 +0200 Subject: [PATCH] Refactor SingleRecordPicker to handle morphs (#14956) ## Scope - Refactor SingleRecordPicker to handle morphItem and match Multiple one - fix query generation to take morph into account Left: - double check SingleRecordPicker in Workflows / Form - Fix tests / stories --- .../hooks/useUpdateActivityTargetFromCell.ts | 10 +- ...tSearchRecordAsSingleRecordPickerRecord.ts | 23 --- .../cache/utils/updateRecordFromCache.ts | 3 +- ...omputeDepthOneRecordGqlFieldsFromRecord.ts | 5 +- .../utils/generateDepthOneRecordGqlFields.ts | 86 ++++++--- .../hooks/useObjectRecordSearchRecords.ts | 4 + .../hooks/useUpdateOneRecordV2.ts | 3 +- ...cordDetailRelationSectionDropdownToOne.tsx | 11 +- .../components/FormSingleRecordPicker.tsx | 15 +- .../MorphRelationManyToOneFieldInput.tsx | 46 ++++- .../RelationManyToOneFieldInput.tsx | 15 +- .../input/hooks/useMorphPersistManyToOne.ts | 106 +++++++++++ ...RecordAndObjectMetadataItemFromRecordId.ts | 8 +- .../useMultipleRecordPickerPerformSearch.ts | 21 +-- .../searchRecordStoreComponentFamilyState.ts | 8 +- .../types/MultipleRecordPickerRecords.ts | 10 - .../components/SingleRecordPicker.tsx | 12 +- .../components/SingleRecordPickerMenuItem.tsx | 52 ++++-- .../SingleRecordPickerMenuItems.tsx | 47 +++-- .../SingleRecordPickerMenuItemsWithSearch.tsx | 24 +-- .../SingleRecordPicker.stories.tsx | 43 ++--- .../useSingleRecordPickerPerformSearch.ts | 119 ++++++++++++ .../hooks/useSingleRecordPickerRecords.ts | 6 +- .../types/SingleRecordPickerRecord.ts | 6 - .../__mocks__/useFilteredSearchRecordQuery.ts | 176 ------------------ .../useFilteredSearchRecordQuery.test.tsx | 106 ----------- .../hooks/useFilteredSearchRecordQuery.ts | 73 -------- .../src/modules/search/types/interface.ts | 7 - 28 files changed, 460 insertions(+), 585 deletions(-) delete mode 100644 packages/twenty-front/src/modules/object-metadata/utils/formatSearchRecordAsSingleRecordPickerRecord.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useMorphPersistManyToOne.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerRecords.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerPerformSearch.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord.ts delete mode 100644 packages/twenty-front/src/modules/search/hooks/__mocks__/useFilteredSearchRecordQuery.ts delete mode 100644 packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchRecordQuery.test.tsx delete mode 100644 packages/twenty-front/src/modules/search/hooks/useFilteredSearchRecordQuery.ts delete mode 100644 packages/twenty-front/src/modules/search/types/interface.ts diff --git a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromCell.ts b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromCell.ts index 6e66947137..6bd45fea58 100644 --- a/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromCell.ts +++ b/packages/twenty-front/src/modules/activities/inline-cell/hooks/useUpdateActivityTargetFromCell.ts @@ -6,7 +6,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; -import { searchRecordStoreComponentFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState'; +import { searchRecordStoreFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState'; import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { useRecoilCallback, useSetRecoilState } from 'recoil'; @@ -58,7 +58,6 @@ export const useUpdateActivityTargetFromCell = ({ async ({ morphItem, activityTargetWithTargetRecords, - recordPickerInstanceId, }: UpdateActivityTargetFromCellProps) => { const targetObjectName = activityObjectNameSingular === CoreObjectNameSingular.Task @@ -105,12 +104,7 @@ export const useUpdateActivityTargetFromCell = ({ } } else { const searchRecord = snapshot - .getLoadable( - searchRecordStoreComponentFamilyState.atomFamily({ - instanceId: recordPickerInstanceId, - familyKey: morphItem.recordId, - }), - ) + .getLoadable(searchRecordStoreFamilyState(morphItem.recordId)) .getValue(); if (!isDefined(searchRecord) || !isDefined(searchRecord?.record)) { diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatSearchRecordAsSingleRecordPickerRecord.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatSearchRecordAsSingleRecordPickerRecord.ts deleted file mode 100644 index 59a0e6577c..0000000000 --- a/packages/twenty-front/src/modules/object-metadata/utils/formatSearchRecordAsSingleRecordPickerRecord.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { getAvatarType } from '@/object-metadata/utils/getAvatarType'; -import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage'; -import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; -import { type SearchRecord } from '~/generated/graphql'; - -export const formatSearchRecordAsSingleRecordPickerRecord = ( - searchRecord: SearchRecord, -): SingleRecordPickerRecord => { - return { - id: searchRecord.recordId, - name: searchRecord.label, - avatarUrl: searchRecord.imageUrl ?? undefined, - avatarType: getAvatarType(searchRecord.objectNameSingular), - linkToShowPage: - getBasePathToShowPage({ - objectNameSingular: searchRecord.objectNameSingular, - }) + searchRecord.recordId, - record: { - id: searchRecord.recordId, - __typename: searchRecord.objectNameSingular, - }, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts b/packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts index 01ea8875b8..f9783cdaf9 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/updateRecordFromCache.ts @@ -4,6 +4,7 @@ import gql from 'graphql-tag'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; +import { type RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; import { type RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; import { type ObjectPermissions } from 'twenty-shared/types'; @@ -21,7 +22,7 @@ export const updateRecordFromCache = ({ objectMetadataItems: ObjectMetadataItem[]; objectMetadataItem: ObjectMetadataItem; cache: ApolloCache; - recordGqlFields: Record; + recordGqlFields: RecordGqlFields; record: T; objectPermissionsByObjectMetadataId: Record< string, diff --git a/packages/twenty-front/src/modules/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord.ts b/packages/twenty-front/src/modules/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord.ts index adc8bcc2cb..6d5c40e4c6 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord.ts @@ -1,3 +1,4 @@ +import { type RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; import { type GenerateDepthOneRecordGqlFields, generateDepthOneRecordGqlFields, @@ -11,13 +12,13 @@ type ComputeDepthOneRecordGqlFieldsFromRecordArgs = export const computeDepthOneRecordGqlFieldsFromRecord = ({ objectMetadataItem, record, -}: ComputeDepthOneRecordGqlFieldsFromRecordArgs) => { +}: ComputeDepthOneRecordGqlFieldsFromRecordArgs): RecordGqlFields => { const depthOneRecordGqlFields = generateDepthOneRecordGqlFields({ objectMetadataItem, }); const recordKeys = Object.keys(record); - return Object.keys(depthOneRecordGqlFields).reduce>( + return Object.keys(depthOneRecordGqlFields).reduce( (acc, key) => { return { ...acc, diff --git a/packages/twenty-front/src/modules/object-record/graphql/utils/generateDepthOneRecordGqlFields.ts b/packages/twenty-front/src/modules/object-record/graphql/utils/generateDepthOneRecordGqlFields.ts index c4f1baaef8..6e6700abc5 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/utils/generateDepthOneRecordGqlFields.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/utils/generateDepthOneRecordGqlFields.ts @@ -1,6 +1,8 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { isDefined } from 'twenty-shared/utils'; +import { type RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; +import { FieldMetadataType, RelationType } from 'twenty-shared/types'; +import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; export type GenerateDepthOneRecordGqlFields = { objectMetadataItem: ObjectMetadataItem; @@ -8,28 +10,64 @@ export type GenerateDepthOneRecordGqlFields = { export const generateDepthOneRecordGqlFields = ({ objectMetadataItem, -}: GenerateDepthOneRecordGqlFields) => - objectMetadataItem.readableFields.reduce>( - (acc, field) => { - return { - ...acc, - ...(isDefined(field.settings?.joinColumnName) - ? { - [field.settings.joinColumnName]: true, - } - : {}), - [field.name]: - // TODO: Remove once we have made the workflows lighter - (objectMetadataItem.nameSingular === - CoreObjectNameSingular.Workflow || - objectMetadataItem.nameSingular === - CoreObjectNameSingular.WorkflowVersion || - objectMetadataItem.nameSingular === - CoreObjectNameSingular.WorkflowRun) && - (field.name === 'versions' || field.name === 'runs') - ? { id: true, name: true } - : true, - }; +}: GenerateDepthOneRecordGqlFields) => { + const gqlFields = objectMetadataItem.readableFields.flatMap( + (fieldMetadata) => { + const isManyToOneMorphRelation = + fieldMetadata.type === FieldMetadataType.MORPH_RELATION && + fieldMetadata.settings.relationType === RelationType.MANY_TO_ONE; + + const isManyToOneRelation = + fieldMetadata.type === FieldMetadataType.RELATION && + fieldMetadata.settings.relationType === RelationType.MANY_TO_ONE; + + if (isManyToOneRelation) { + return [`${fieldMetadata.name}Id`, `${fieldMetadata.name}`]; + } + + if (isManyToOneMorphRelation) { + if (!isDefined(fieldMetadata.morphRelations)) { + throw new Error( + `Field ${fieldMetadata.name} is missing, please refresh the page. If the problem persists, please contact support.`, + ); + } + + const morphGqlFields = fieldMetadata.morphRelations.map( + (morphRelation) => ({ + gqlField: computeMorphRelationFieldName({ + fieldName: fieldMetadata.name, + relationType: morphRelation.type, + targetObjectMetadataNameSingular: + morphRelation.targetObjectMetadata.nameSingular, + targetObjectMetadataNamePlural: + morphRelation.targetObjectMetadata.namePlural, + }), + fieldMetadata, + }), + ); + return morphGqlFields.flatMap((morphGqlField) => [ + morphGqlField.gqlField, + `${morphGqlField.gqlField}Id`, + ]); + } + + return [fieldMetadata.name]; }, - {}, ); + + return gqlFields.reduce((acc, field) => { + return { + ...acc, + [field]: + // TODO: Remove once we have made the workflows lighter + (objectMetadataItem.nameSingular === CoreObjectNameSingular.Workflow || + objectMetadataItem.nameSingular === + CoreObjectNameSingular.WorkflowVersion || + objectMetadataItem.nameSingular === + CoreObjectNameSingular.WorkflowRun) && + (field === 'versions' || field === 'runs') + ? { id: true, name: true } + : true, + }; + }, {}); +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useObjectRecordSearchRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useObjectRecordSearchRecords.ts index 196ae5773c..05276bf86f 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useObjectRecordSearchRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useObjectRecordSearchRecords.ts @@ -9,6 +9,7 @@ import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { type ObjectRecordFilterInput, + type SearchQuery, useSearchQuery, } from '~/generated/graphql'; import { logError } from '~/utils/logError'; @@ -18,6 +19,7 @@ export type UseSearchRecordsParams = { objectNameSingulars: string[]; limit?: number; onError?: (error?: Error) => void; + onCompleted?: (data: SearchQuery) => void; skip?: boolean; fetchPolicy?: WatchQueryFetchPolicy; searchInput?: string; @@ -28,6 +30,7 @@ export const useObjectRecordSearchRecords = ({ objectNameSingulars, searchInput, limit, + onCompleted, skip, filter, fetchPolicy, @@ -49,6 +52,7 @@ export const useObjectRecordSearchRecords = ({ }, fetchPolicy: fetchPolicy, client: apolloCoreClient, + onCompleted: onCompleted, onError: (error) => { logError( `useSearchRecords for "${objectNameSingulars.join(', ')}" error : ` + diff --git a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordV2.ts b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordV2.ts index 44daa93475..2f210a5a01 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordV2.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecordV2.ts @@ -7,6 +7,7 @@ import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache'; import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; +import { type RecordGqlFields } from '@/object-record/graphql/types/RecordGqlFields'; import { computeDepthOneRecordGqlFieldsFromRecord } from '@/object-record/graphql/utils/computeDepthOneRecordGqlFieldsFromRecord'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; @@ -24,7 +25,7 @@ type UpdateOneRecordArgs = { updateOneRecordInput: Partial>; optimisticRecord?: Partial; objectNameSingular: string; - recordGqlFields?: Record; + recordGqlFields?: RecordGqlFields; }; export const useUpdateOneRecordV2 = () => { const apolloCoreClient = useApolloCoreClient(); diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToOne.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToOne.tsx index 2a03d3f918..4793222ddf 100644 --- a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToOne.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToOne.tsx @@ -11,7 +11,7 @@ import { SingleRecordPicker } from '@/object-record/record-picker/single-record- import { useSingleRecordPickerOpen } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerOpen'; import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState'; import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState'; -import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; +import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; import { getRecordFieldCardRelationPickerDropdownId } from '@/object-record/record-show/utils/getRecordFieldCardRelationPickerDropdownId'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; @@ -77,13 +77,14 @@ export const RecordDetailRelationSectionDropdownToOne = () => { const { onSubmit } = useContext(FieldInputEventContext); const handleRelationPickerEntitySelected = ( - selectedRelationEntity?: SingleRecordPickerRecord, + selectedMorphItem?: RecordPickerPickableMorphItem, ) => { closeDropdown(dropdownId); - if (!selectedRelationEntity?.id || !relationFieldMetadataItem?.name) return; + if (!selectedMorphItem?.recordId || !relationFieldMetadataItem?.name) + return; - onSubmit?.({ newValue: selectedRelationEntity.record }); + onSubmit?.({ newValue: selectedMorphItem.recordId }); }; const { createNewRecordAndOpenRightDrawer } = @@ -133,7 +134,7 @@ export const RecordDetailRelationSectionDropdownToOne = () => { focusId={dropdownId} componentInstanceId={dropdownId} EmptyIcon={IconForbid} - onRecordSelected={handleRelationPickerEntitySelected} + onMorphItemSelected={handleRelationPickerEntitySelected} objectNameSingulars={[relationObjectMetadataNameSingular]} recordPickerInstanceId={dropdownId} onCancel={() => closeDropdown(dropdownId)} diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx index a33ea86f4d..ede6d1ed82 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/form-types/components/FormSingleRecordPicker.tsx @@ -7,7 +7,7 @@ import { type VariablePickerComponent } from '@/object-record/record-field/ui/fo import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker'; import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState'; import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState'; -import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; +import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; import { InputLabel } from '@/ui/input/components/InputLabel'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; @@ -122,19 +122,16 @@ export const FormSingleRecordPicker = ({ setRecordPickerSearchFilter(''); }, [setRecordPickerSearchFilter]); - const handleRecordSelected = ( - selectedEntity: SingleRecordPickerRecord | null | undefined, + const handleMorphItemSelected = ( + selectedMorphItem: RecordPickerPickableMorphItem | null | undefined, ) => { - if ( - !isDefined(selectedEntity?.record?.id) || - !isNonEmptyString(selectedEntity.record?.id) - ) { + if (!isNonEmptyString(selectedMorphItem?.recordId)) { onChange(null); return; } - onChange(selectedEntity.record.id); + onChange(selectedMorphItem.recordId); closeDropdown(dropdownId); }; @@ -216,7 +213,7 @@ export const FormSingleRecordPicker = ({ EmptyIcon={IconForbid} emptyLabel={'No ' + objectNameSingulars.join(' or ')} onCancel={() => closeDropdown(dropdownId)} - onRecordSelected={handleRecordSelected} + onMorphItemSelected={handleMorphItemSelected} objectNameSingulars={objectNameSingulars} recordPickerInstanceId={dropdownId} dropdownWidth={GenericDropdownContentWidth.ExtraLarge} diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/MorphRelationManyToOneFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/MorphRelationManyToOneFieldInput.tsx index 02a5715412..d1f1901ccf 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/MorphRelationManyToOneFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/MorphRelationManyToOneFieldInput.tsx @@ -1,33 +1,59 @@ import { FieldInputEventContext } from '@/object-record/record-field/ui/contexts/FieldInputEventContext'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext'; +import { useMorphPersistManyToOne } from '@/object-record/record-field/ui/meta-types/input/hooks/useMorphPersistManyToOne'; import { RecordFieldComponentInstanceContext } from '@/object-record/record-field/ui/states/contexts/RecordFieldComponentInstanceContext'; import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionComponentState'; import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionLoadingComponentState'; import { isFieldMorphRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelationManyToOne'; import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker'; -import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; -import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext'; +import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; import { useContext } from 'react'; +import { isDefined } from 'twenty-shared/utils'; import { IconForbid } from 'twenty-ui/display'; export const MorphRelationManyToOneFieldInput = () => { - const { fieldDefinition } = useContext(FieldContext); + const { fieldDefinition, recordId } = useContext(FieldContext); - const { onCancel, onSubmit } = useContext(FieldInputEventContext); - const { onCloseTableCell } = useRecordTableBodyContextOrThrow(); + const { onCancel } = useContext(FieldInputEventContext); + const { objectMetadataItems } = useObjectMetadataItems(); const instanceId = useAvailableComponentInstanceIdOrThrow( RecordFieldComponentInstanceContext, ); - const handleRecordSelected = ( - selectedRecord: SingleRecordPickerRecord | null | undefined, + const { persistMorphManyToOne } = useMorphPersistManyToOne({ + objectMetadataNameSingular: + fieldDefinition.metadata.objectMetadataNameSingular ?? '', + }); + + const handleMorphItemSelected = async ( + selectedMorphItem: RecordPickerPickableMorphItem | null | undefined, ) => { - onSubmit?.({ newValue: selectedRecord?.record ?? null }); - onCloseTableCell(); + if (!isDefined(selectedMorphItem)) { + // Handle detach + return; + } + + const targetObjectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.id === selectedMorphItem.objectMetadataId, + ); + + if (!isDefined(targetObjectMetadataItem)) { + throw new Error('TargetObjectMetadataItem is required'); + } + + await persistMorphManyToOne({ + recordId: recordId, + fieldDefinition, + valueToPersist: selectedMorphItem.recordId, + targetObjectMetadataNameSingular: targetObjectMetadataItem.nameSingular, + }); + onCancel?.(); }; const layoutDirection = useRecoilComponentValue( @@ -56,7 +82,7 @@ export const MorphRelationManyToOneFieldInput = () => { EmptyIcon={IconForbid} emptyLabel={'No ' + fieldDefinition.label} onCancel={onCancel} - onRecordSelected={handleRecordSelected} + onMorphItemSelected={handleMorphItemSelected} objectNameSingulars={objectNameSingulars} recordPickerInstanceId={instanceId} layoutDirection={ diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationManyToOneFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationManyToOneFieldInput.tsx index 9d4075d015..622723430c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationManyToOneFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationManyToOneFieldInput.tsx @@ -8,7 +8,7 @@ import { recordFieldInputLayoutDirectionComponentState } from '@/object-record/r import { recordFieldInputLayoutDirectionLoadingComponentState } from '@/object-record/record-field/ui/states/recordFieldInputLayoutDirectionLoadingComponentState'; import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker'; import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState'; -import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; +import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; @@ -28,9 +28,14 @@ export const RelationManyToOneFieldInput = () => { RecordFieldComponentInstanceContext, ); - const handleRecordSelected = ( - selectedRecord: SingleRecordPickerRecord | null | undefined, - ) => onSubmit?.({ newValue: selectedRecord?.record ?? null }); + const handleMorphItemSelected = ( + selectedMorphItem: RecordPickerPickableMorphItem | null | undefined, + ) => + onSubmit?.({ + newValue: isDefined(selectedMorphItem) + ? { id: selectedMorphItem.recordId } + : null, + }); const { objectMetadataItem: relationObjectMetadataItem } = useObjectMetadataItem({ @@ -85,7 +90,7 @@ export const RelationManyToOneFieldInput = () => { emptyLabel={t`No ${fieldLabel}`} onCancel={onCancel} onCreate={handleCreateNew} - onRecordSelected={handleRecordSelected} + onMorphItemSelected={handleMorphItemSelected} objectNameSingulars={[ fieldDefinition.metadata.relationObjectMetadataNameSingular, ]} diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useMorphPersistManyToOne.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useMorphPersistManyToOne.ts new file mode 100644 index 0000000000..61c35d923c --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/hooks/useMorphPersistManyToOne.ts @@ -0,0 +1,106 @@ +import { useRecoilCallback } from 'recoil'; + +import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition'; +import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; +import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; + +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; +import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata'; +import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation'; +import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils'; + +export type MorphPersistManyToOneProps = { + objectMetadataNameSingular: string; +}; + +export const useMorphPersistManyToOne = ({ + objectMetadataNameSingular, +}: MorphPersistManyToOneProps) => { + const { objectMetadataItems } = useObjectMetadataItems(); + + const { updateOneRecord } = useUpdateOneRecord({ + objectNameSingular: objectMetadataNameSingular, + }); + + const persistMorphManyToOne = useRecoilCallback( + ({ set, snapshot }) => + async ({ + recordId, + fieldDefinition, + valueToPersist, + targetObjectMetadataNameSingular, + }: { + recordId: string; + fieldDefinition: FieldDefinition; + valueToPersist: string | null | undefined; + targetObjectMetadataNameSingular: string; + }) => { + assertFieldMetadata( + FieldMetadataType.MORPH_RELATION, + isFieldMorphRelation, + fieldDefinition, + ); + const targetObjectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.nameSingular === + targetObjectMetadataNameSingular, + ); + + if (!isDefined(targetObjectMetadataItem)) { + throw new Error('Object metadata item not found'); + } + + const fieldName = fieldDefinition.metadata.fieldName; + + if (!isDefined(valueToPersist)) { + // Handle detach + return; + } + + const computedFieldName = computeMorphRelationFieldName({ + fieldName, + relationType: fieldDefinition.metadata.relationType, + targetObjectMetadataNameSingular: + targetObjectMetadataItem.nameSingular, + targetObjectMetadataNamePlural: targetObjectMetadataItem.namePlural, + }); + + const currentValue: unknown = snapshot + .getLoadable( + recordStoreFamilySelector({ + recordId, + fieldName: computedFieldName, + }), + ) + .getValue(); + + if ( + isDefined(currentValue) && + (currentValue as ObjectRecord).id === valueToPersist + ) { + return; + } + + updateOneRecord?.({ + idToUpdate: recordId, + updateOneRecordInput: { + [`${computedFieldName}Id`]: valueToPersist, + }, + }); + set( + recordStoreFamilySelector({ + recordId, + fieldName: computedFieldName, + }), + valueToPersist, + ); + return; + }, + [updateOneRecord, objectMetadataItems], + ); + + return { persistMorphManyToOne }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/hooks/useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId.ts b/packages/twenty-front/src/modules/object-record/record-picker/hooks/useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId.ts index 17bc17451e..5f6e1b65f1 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/hooks/useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId.ts +++ b/packages/twenty-front/src/modules/object-record/record-picker/hooks/useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId.ts @@ -1,7 +1,8 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { searchRecordStoreComponentFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState'; +import { searchRecordStoreFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState'; import { multipleRecordPickerSinglePickableMorphItemComponentFamilySelector } from '@/object-record/record-picker/multiple-record-picker/states/selectors/multipleRecordPickerSinglePickableMorphItemComponentFamilySelector'; import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue'; +import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; type UseRecordPickerGetRecordAndObjectMetadataItemFromRecordIdProps = { @@ -19,10 +20,7 @@ export const useRecordPickerGetSearchRecordAndObjectMetadataItemFromRecordId = recordId, ); - const searchRecord = useRecoilComponentFamilyValue( - searchRecordStoreComponentFamilyState, - recordId, - ); + const searchRecord = useRecoilValue(searchRecordStoreFamilyState(recordId)); if (!isDefined(pickableMorphItem) || !isDefined(searchRecord)) { return { searchRecord: null, objectMetadataItem: null }; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch.ts index 584f7aeb29..2e1d9f823a 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch.ts +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch.ts @@ -8,7 +8,7 @@ import { multipleRecordPickerPaginationState } from '@/object-record/record-pick import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState'; import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState'; import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState'; -import { searchRecordStoreComponentFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState'; +import { searchRecordStoreFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState'; import { sortMorphItems } from '@/object-record/record-picker/multiple-record-picker/utils/sortMorphItems'; import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId'; @@ -280,10 +280,7 @@ export const useMultipleRecordPickerPerformSearch = () => { searchRecords.forEach((searchRecord) => { set( - searchRecordStoreComponentFamilyState.atomFamily({ - instanceId: multipleRecordPickerInstanceId, - familyKey: searchRecord.recordId, - }), + searchRecordStoreFamilyState(searchRecord.recordId), searchRecord, ); }); @@ -346,16 +343,10 @@ export const useMultipleRecordPickerPerformSearch = () => { return; } - set( - searchRecordStoreComponentFamilyState.atomFamily({ - instanceId: multipleRecordPickerInstanceId, - familyKey: objectRecord.id, - }), - { - ...searchRecord, - record: objectRecord, - }, - ); + set(searchRecordStoreFamilyState(objectRecord.id), { + ...searchRecord, + record: objectRecord, + }); }); }, ); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState.ts index d07b930d73..d524798e2c 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState.ts +++ b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState.ts @@ -1,13 +1,11 @@ -import { MultipleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/multiple-record-picker/states/contexts/MultipleRecordPickerComponentInstanceContext'; import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState'; +import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState'; import { type SearchRecord } from '~/generated-metadata/graphql'; -export const searchRecordStoreComponentFamilyState = createComponentFamilyState< +export const searchRecordStoreFamilyState = createFamilyState< (SearchRecord & { record?: ObjectRecord }) | undefined, string >({ - key: 'searchRecordStoreComponentFamilyState', + key: 'searchRecordStoreFamilyState', defaultValue: undefined, - componentInstanceContext: MultipleRecordPickerComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerRecords.ts b/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerRecords.ts deleted file mode 100644 index 0281367f67..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerRecords.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; - -export type MultipleRecordPickerRecords< - CustomRecordForRecordPicker extends SingleRecordPickerRecord, -> = { - selectedRecords: CustomRecordForRecordPicker[]; - filteredSelectedRecords: CustomRecordForRecordPicker[]; - recordsToSelect: CustomRecordForRecordPicker[]; - loading: boolean; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPicker.tsx b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPicker.tsx index 165fefb6f9..0b573c3092 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPicker.tsx +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPicker.tsx @@ -6,7 +6,7 @@ import { } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItemsWithSearch'; import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext'; import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState'; -import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; +import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState'; @@ -24,7 +24,7 @@ export const SingleRecordPicker = ({ excludedRecordIds, onCancel, onCreate, - onRecordSelected, + onMorphItemSelected, objectNameSingulars, componentInstanceId, layoutDirection, @@ -44,12 +44,12 @@ export const SingleRecordPicker = ({ onCancel?.(); }; - const handleRecordSelected = ( - selectedRecord?: SingleRecordPickerRecord | undefined, + const handleMorphItemSelected = ( + selectedMorphItem?: RecordPickerPickableMorphItem | undefined, ) => { setRecordPickerSearchFilter(''); - onRecordSelected?.(selectedRecord); + onMorphItemSelected?.(selectedMorphItem); }; useListenClickOutside({ @@ -83,7 +83,7 @@ export const SingleRecordPicker = ({ excludedRecordIds, onCancel: handleCancel, onCreate, - onRecordSelected: handleRecordSelected, + onMorphItemSelected: handleMorphItemSelected, objectNameSingulars, layoutDirection, }} diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItem.tsx b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItem.tsx index c39fdb3586..a87b5c0def 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItem.tsx +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItem.tsx @@ -1,19 +1,23 @@ import styled from '@emotion/styled'; +import { getAvatarType } from '@/object-metadata/utils/getAvatarType'; +import { searchRecordStoreFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState'; import { SingleRecordPickerComponentInstanceContext } from '@/object-record/record-picker/single-record-picker/states/contexts/SingleRecordPickerComponentInstanceContext'; -import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; import { getSingleRecordPickerSelectableListId } from '@/object-record/record-picker/single-record-picker/utils/getSingleRecordPickerSelectableListId'; +import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-shared/utils'; import { Avatar } from 'twenty-ui/display'; import { MenuItemSelectAvatar } from 'twenty-ui/navigation'; type SingleRecordPickerMenuItemProps = { - record: SingleRecordPickerRecord; - onRecordSelected: (recordSelected?: SingleRecordPickerRecord) => void; - selectedRecord?: SingleRecordPickerRecord; + morphItem: RecordPickerPickableMorphItem; + onMorphItemSelected: (morphItem?: RecordPickerPickableMorphItem) => void; + isRecordSelected: boolean; }; const StyledSelectableItem = styled(SelectableListItem)` @@ -21,9 +25,9 @@ const StyledSelectableItem = styled(SelectableListItem)` `; export const SingleRecordPickerMenuItem = ({ - record, - onRecordSelected, - selectedRecord, + morphItem, + onMorphItemSelected, + isRecordSelected, }: SingleRecordPickerMenuItemProps) => { const recordPickerComponentInstanceId = useAvailableComponentInstanceIdOrThrow( @@ -33,33 +37,41 @@ export const SingleRecordPickerMenuItem = ({ const selectableListComponentInstanceId = getSingleRecordPickerSelectableListId(recordPickerComponentInstanceId); - const isSelectedItemId = useRecoilComponentFamilyValue( + const isSelectedByKeyboard = useRecoilComponentFamilyValue( isSelectedItemIdComponentFamilySelector, - record.id, + morphItem.recordId, selectableListComponentInstanceId, ); + const searchRecord = useRecoilValue( + searchRecordStoreFamilyState(morphItem.recordId), + ); + + if (!isDefined(searchRecord)) { + return null; + } + return ( { - onRecordSelected(record); + onMorphItemSelected(morphItem); }} > onRecordSelected(record)} - text={record.name} - selected={selectedRecord?.id === record.id} - focused={isSelectedItemId} + onClick={() => onMorphItemSelected(morphItem)} + text={searchRecord.label} + selected={isRecordSelected} + focused={isSelectedByKeyboard} avatar={ } /> diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItems.tsx b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItems.tsx index cd0c50e71f..f4e54ab3b3 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItems.tsx +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItems.tsx @@ -1,4 +1,4 @@ -import { isNonEmptyString, isUndefined } from '@sniptt/guards'; +import { isUndefined } from '@sniptt/guards'; import { Key } from 'ts-key-enum'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; @@ -12,8 +12,8 @@ import { SingleRecordPickerComponentInstanceContext } from '@/object-record/reco import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState'; import { singleRecordPickerShouldShowInitialLoadingComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerShouldShowInitialLoadingComponentState'; import { singleRecordPickerShouldShowSkeletonComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerShouldShowSkeletonComponentState'; -import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; import { getSingleRecordPickerSelectableListId } from '@/object-record/record-picker/single-record-picker/utils/getSingleRecordPickerSelectableListId'; +import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; import { isSelectedItemIdComponentFamilySelector } from '@/ui/layout/selectable-list/states/selectors/isSelectedItemIdComponentFamilySelector'; import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement'; @@ -21,36 +21,26 @@ import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/com import { useRecoilComponentFamilyValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValue'; import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; -import { isDefined } from 'twenty-shared/utils'; import { type IconComponent } from 'twenty-ui/display'; import { MenuItemSelect } from 'twenty-ui/navigation'; export type SingleRecordPickerMenuItemsProps = { EmptyIcon?: IconComponent; emptyLabel?: string; - recordsToSelect: SingleRecordPickerRecord[]; + pickableMorphItems: RecordPickerPickableMorphItem[]; onCancel?: () => void; - onRecordSelected: (entity?: SingleRecordPickerRecord) => void; - selectedRecord?: SingleRecordPickerRecord; + onMorphItemSelected: (morphItem?: RecordPickerPickableMorphItem) => void; focusId: string; - filteredSelectedRecords: SingleRecordPickerRecord[]; }; export const SingleRecordPickerMenuItems = ({ EmptyIcon, emptyLabel, - recordsToSelect, + pickableMorphItems, onCancel, - onRecordSelected, - filteredSelectedRecords, - selectedRecord, + onMorphItemSelected, focusId, }: SingleRecordPickerMenuItemsProps) => { - const recordsInDropdown = [selectedRecord, ...recordsToSelect].filter( - (entity): entity is SingleRecordPickerRecord => - isDefined(entity) && isNonEmptyString(entity.name), - ); - const recordPickerComponentInstanceId = useAvailableComponentInstanceIdOrThrow( SingleRecordPickerComponentInstanceContext, @@ -79,7 +69,9 @@ export const SingleRecordPickerMenuItems = ({ dependencies: [onCancel, resetSelectedItem], }); - const selectableItemIds = recordsInDropdown.map((entity) => entity.id); + const selectableItemIds = pickableMorphItems.map( + (morphItem) => morphItem.recordId, + ); const [selectedRecordId, setSelectedRecordId] = useRecoilComponentState( singleRecordPickerSelectedIdComponentState, ); @@ -92,8 +84,11 @@ export const SingleRecordPickerMenuItems = ({ singleRecordPickerShouldShowInitialLoadingComponentState, ); - const searchHasNoResults = - recordsToSelect.length === 0 && filteredSelectedRecords?.length === 0; + const itemsMatchingSearchFilter = pickableMorphItems.filter( + (morphItem) => morphItem.isMatchingSearchFilter, + ); + + const searchHasNoResults = itemsMatchingSearchFilter.length === 0; return ( { setSelectedRecordId(undefined); - onRecordSelected(); + onMorphItemSelected(); }} > { setSelectedRecordId(undefined); - onRecordSelected(); + onMorphItemSelected(); }} LeftIcon={EmptyIcon} text={emptyLabel} @@ -127,12 +122,12 @@ export const SingleRecordPickerMenuItems = ({ ) : singleRecordPickerShouldShowSkeleton ? ( ) : ( - recordsInDropdown?.map((record) => ( + itemsMatchingSearchFilter.map((morphItem) => ( )) )} diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItemsWithSearch.tsx b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItemsWithSearch.tsx index 8f1e44357d..0383af5b32 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItemsWithSearch.tsx +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/SingleRecordPickerMenuItemsWithSearch.tsx @@ -28,11 +28,7 @@ export type SingleRecordPickerMenuItemsWithSearchProps = { focusId: string; } & Pick< SingleRecordPickerMenuItemsProps, - | 'EmptyIcon' - | 'emptyLabel' - | 'onCancel' - | 'onRecordSelected' - | 'selectedRecord' + 'EmptyIcon' | 'emptyLabel' | 'onCancel' | 'onMorphItemSelected' >; export const SingleRecordPickerMenuItemsWithSearch = ({ @@ -41,7 +37,7 @@ export const SingleRecordPickerMenuItemsWithSearch = ({ excludedRecordIds, onCancel, onCreate, - onRecordSelected, + onMorphItemSelected, objectNameSingulars, layoutDirection = 'search-bar-on-top', focusId, @@ -57,7 +53,7 @@ export const SingleRecordPickerMenuItemsWithSearch = ({ recordPickerInstanceId, ); - const { records } = useSingleRecordPickerRecords({ + const { pickableMorphItems, loading } = useSingleRecordPickerRecords({ objectNameSingulars, excludedRecordIds, }); @@ -83,7 +79,7 @@ export const SingleRecordPickerMenuItemsWithSearch = ({ return ( <> - + {layoutDirection === 'search-bar-on-bottom' && ( <> {isDefined(onCreate) && hasUpdatePermissions && ( @@ -102,14 +98,12 @@ export const SingleRecordPickerMenuItemsWithSearch = ({ @@ -127,14 +121,12 @@ export const SingleRecordPickerMenuItemsWithSearch = ({ diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/__stories__/SingleRecordPicker.stories.tsx b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/__stories__/SingleRecordPicker.stories.tsx index 732b66d5c7..6edc493ad7 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/__stories__/SingleRecordPicker.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/components/__stories__/SingleRecordPicker.stories.tsx @@ -5,24 +5,31 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { graphqlMocks } from '~/testing/graphqlMocks'; -import { allMockPersonRecords } from '~/testing/mock-data/people'; import { sleep } from '~/utils/sleep'; import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker'; -import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; import { IconUserCircle } from 'twenty-ui/display'; import { ComponentDecorator } from 'twenty-ui/testing'; import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator'; -const records = allMockPersonRecords.map( - (person) => ({ - id: person.id, - name: person.name.firstName + ' ' + person.name.lastName, - avatarUrl: 'https://picsum.photos/200', - avatarType: 'rounded', - record: { ...person, __typename: 'Person' }, - }), -); +// const records = allMockPersonRecords.map((person) => ({ +// id: person.id, +// label: person.name.firstName + ' ' + person.name.lastName, +// imageUrl: 'https://picsum.photos/200', +// objectNameSingular: 'Person', +// recordId: person.id, +// tsRank: 0, +// tsRankCD: 0, +// })); + +// const pickableMorphItems = records.map( +// (record) => ({ +// recordId: record.recordId, +// objectMetadataId: record.objectNameSingular, +// isSelected: false, +// isMatchingSearchFilter: true, +// }), +// ); const meta: Meta = { title: 'UI/RecordPicker/SingleRecordPicker', @@ -37,15 +44,7 @@ const meta: Meta = { objectNameSingulars: [CoreObjectNameSingular.WorkspaceMember], componentInstanceId: 'single-record-picker', }, - argTypes: { - selectedRecord: { - options: records.map(({ name }) => name), - mapping: records.reduce( - (result, entity) => ({ ...result, [entity.name]: entity }), - {}, - ), - }, - }, + argTypes: {}, parameters: { msw: graphqlMocks, }, @@ -56,9 +55,7 @@ type Story = StoryObj; export const Default: Story = {}; -export const WithSelectedRecord: Story = { - args: { selectedRecord: records[2] }, -}; +export const WithSelectedRecord: Story = {}; export const WithEmptyOption: Story = { args: { diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerPerformSearch.ts b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerPerformSearch.ts new file mode 100644 index 0000000000..f7f311a50d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerPerformSearch.ts @@ -0,0 +1,119 @@ +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit'; +import { useObjectRecordSearchRecords } from '@/object-record/hooks/useObjectRecordSearchRecords'; +import { searchRecordStoreFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState'; +import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem'; +import { useRecoilCallback } from 'recoil'; +import { CustomError, isDefined } from 'twenty-shared/utils'; +import { type SearchQuery } from '~/generated/graphql'; + +export const useSingleRecordPickerPerformSearch = ({ + selectedIds, + limit, + excludedRecordIds = [], + objectNameSingulars, + searchFilter, +}: { + selectedIds: string[]; + limit?: number; + excludedRecordIds?: string[]; + objectNameSingulars: string[]; + searchFilter?: string; +}): { + pickableMorphItems: RecordPickerPickableMorphItem[]; + loading: boolean; +} => { + const onSearchRecordsCompleted = useRecoilCallback( + ({ set }) => + (data: SearchQuery) => { + const searchRecords = data.search.edges.map((edge) => edge.node); + + searchRecords.forEach((searchRecord) => { + set(searchRecordStoreFamilyState(searchRecord.recordId), { + ...searchRecord, + record: undefined, + }); + }); + }, + [], + ); + + const { objectMetadataItems } = useObjectMetadataItems(); + const selectedIdsFilter = { id: { in: selectedIds } }; + + const { loading: selectedRecordsLoading, searchRecords: selectedRecords } = + useObjectRecordSearchRecords({ + objectNameSingulars, + filter: selectedIdsFilter, + skip: !selectedIds.length, + searchInput: '', + onCompleted: onSearchRecordsCompleted, + }); + + const { + loading: filteredSelectedRecordsLoading, + searchRecords: filteredSelectedRecords, + } = useObjectRecordSearchRecords({ + objectNameSingulars, + filter: selectedIdsFilter, + skip: !selectedIds.length, + searchInput: searchFilter, + onCompleted: onSearchRecordsCompleted, + }); + + const notFilterIds = [...selectedIds, ...excludedRecordIds]; + const notFilter = notFilterIds.length + ? { not: { id: { in: notFilterIds } } } + : undefined; + const { loading: recordsToSelectLoading, searchRecords: recordsToSelect } = + useObjectRecordSearchRecords({ + objectNameSingulars, + filter: notFilter, + limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT, + searchInput: searchFilter, + fetchPolicy: 'cache-and-network', + onCompleted: onSearchRecordsCompleted, + }); + + const pickableMorphItems = [...selectedRecords, ...recordsToSelect].map( + (record) => { + const objectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.nameSingular === record.objectNameSingular, + ); + + if (!isDefined(objectMetadataItem)) { + throw new CustomError( + 'Object metadata item not found for object name singular: ' + + record.objectNameSingular, + 'OBJECT_METADATA_ITEM_NOT_FOUND', + ); + } + + return { + recordId: record.recordId, + objectMetadataId: objectMetadataItem.id, + isSelected: selectedRecords.some( + (selectedRecord) => selectedRecord.recordId === record.recordId, + ), + isMatchingSearchFilter: + recordsToSelect.some( + (recordsToSelectRecord) => + recordsToSelectRecord.recordId === record.recordId, + ) || + filteredSelectedRecords.some( + (filteredSelectedRecord) => + filteredSelectedRecord.recordId === record.recordId, + ), + }; + }, + ); + + return { + pickableMorphItems, + loading: + recordsToSelectLoading || + filteredSelectedRecordsLoading || + selectedRecordsLoading, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerRecords.ts b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerRecords.ts index 62e64b4ad5..4605511e7b 100644 --- a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerRecords.ts +++ b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerRecords.ts @@ -1,6 +1,6 @@ +import { useSingleRecordPickerPerformSearch } from '@/object-record/record-picker/single-record-picker/hooks/useSingleRecordPickerPerformSearch'; import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState'; import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState'; -import { useFilteredSearchRecordQuery } from '@/search/hooks/useFilteredSearchRecordQuery'; import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; export const useSingleRecordPickerRecords = ({ @@ -17,12 +17,12 @@ export const useSingleRecordPickerRecords = ({ const selectedRecordId = useRecoilComponentValue( singleRecordPickerSelectedIdComponentState, ); - const records = useFilteredSearchRecordQuery({ + const { pickableMorphItems, loading } = useSingleRecordPickerPerformSearch({ searchFilter: recordPickerSearchFilter, selectedIds: selectedRecordId ? [selectedRecordId] : [], excludedRecordIds: excludedRecordIds, objectNameSingulars, }); - return { records }; + return { pickableMorphItems, loading }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord.ts b/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord.ts deleted file mode 100644 index 0e6b2a154d..0000000000 --- a/packages/twenty-front/src/modules/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier'; - -export type SingleRecordPickerRecord = ObjectRecordIdentifier & { - record: ObjectRecord; -}; diff --git a/packages/twenty-front/src/modules/search/hooks/__mocks__/useFilteredSearchRecordQuery.ts b/packages/twenty-front/src/modules/search/hooks/__mocks__/useFilteredSearchRecordQuery.ts deleted file mode 100644 index 89e4afc95b..0000000000 --- a/packages/twenty-front/src/modules/search/hooks/__mocks__/useFilteredSearchRecordQuery.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { gql } from '@apollo/client'; - -export const query = gql` - query FindManyPeople( - $filter: PersonFilterInput - $orderBy: [PersonOrderByInput] - $lastCursor: String - $limit: Int = 60 - ) { - people( - filter: $filter - orderBy: $orderBy - first: $limit - after: $lastCursor - ) { - edges { - node { - id - opportunities { - edges { - node { - id - personId - pointOfContactId - updatedAt - companyId - stage - closeDate - amount { - amountMicros - currencyCode - } - id - createdAt - } - } - } - xLink { - primaryLinkLabel - primaryLinkUrl - } - id - pointOfContactForOpportunities { - edges { - node { - id - personId - pointOfContactId - updatedAt - companyId - stage - closeDate - amount { - amountMicros - currencyCode - } - id - createdAt - } - } - } - createdAt - company { - id - xLink { - primaryLinkLabel - primaryLinkUrl - } - linkedinLink { - primaryLinkLabel - primaryLinkUrl - } - domainName - annualRecurringRevenue { - amountMicros - currencyCode - } - createdAt - address { - adressCity - } - updatedAt - name - accountOwnerId - employees - id - idealCustomerProfile - } - city - email - jobTitle - favorites { - edges { - node { - id - id - companyId - createdAt - forWorkspaceMemberId - personId - position - updatedAt - } - } - } - attachments { - edges { - node { - id - updatedAt - createdAt - name - personId - companyId - id - authorId - type - fullPath - } - } - } - name { - firstName - lastName - } - phone - linkedinLink { - primaryLinkLabel - primaryLinkUrl - } - updatedAt - avatarUrl - companyId - } - cursor - } - pageInfo { - hasNextPage - startCursor - endCursor - } - } - } -`; - -export const variables = { - recordsToSelect: { - limit: 10, - filter: { - and: [ - { and: [{ or: [{ name: { ilike: '%Entity%' } }] }] }, - { not: { id: { in: ['1', '2'] } } }, - ], - }, - orderBy: [{ name: 'AscNullsLast' }], - }, - filteredSelectedRecords: { - limit: 60, - filter: { - and: [ - { and: [{ or: [{ name: { ilike: '%Entity%' } }] }] }, - { id: { in: ['1'] } }, - ], - }, - orderBy: [{ name: 'AscNullsLast' }], - }, - selectedEntities: { - limit: 60, - filter: { id: { in: ['1'] } }, - orderBy: [{ name: 'AscNullsLast' }], - }, -}; - -export const responseData = { - edges: [], -}; diff --git a/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchRecordQuery.test.tsx b/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchRecordQuery.test.tsx deleted file mode 100644 index 99c15ce60b..0000000000 --- a/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchRecordQuery.test.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { MockedProvider } from '@apollo/client/testing'; -import { renderHook } from '@testing-library/react'; -import { type ReactNode } from 'react'; -import { RecoilRoot, useSetRecoilState } from 'recoil'; - -import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; - -import { type MultipleRecordPickerRecords } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerRecords'; -import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext'; -import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems'; -import { - query, - responseData, - variables, -} from '../__mocks__/useFilteredSearchRecordQuery'; -import { useFilteredSearchRecordQuery } from '../useFilteredSearchRecordQuery'; - -const mocks = [ - { - request: { - query, - variables: variables.recordsToSelect, - }, - result: jest.fn(() => ({ - data: { - people: responseData, - }, - })), - }, - { - request: { - query, - variables: variables.filteredSelectedRecords, - }, - result: jest.fn(() => ({ - data: { - people: responseData, - }, - })), - }, - { - request: { - query, - variables: variables.selectedEntities, - }, - result: jest.fn(() => ({ - data: { - people: responseData, - }, - })), - }, -]; - -const Wrapper = ({ children }: { children: ReactNode }) => ( - - - - {children} - - - -); - -describe('useFilteredSearchRecordQuery', () => { - it('returns the correct result when everything is provided', async () => { - const { result } = renderHook( - () => { - const setCurrentWorkspaceMember = useSetRecoilState( - currentWorkspaceMemberState, - ); - setCurrentWorkspaceMember({ - id: '32219445-f587-4c40-b2b1-6d3205ed96da', - name: { firstName: 'John', lastName: 'Connor' }, - locale: 'en', - colorScheme: 'Light', - userEmail: 'userEmail', - }); - - const setMetadataItems = useSetRecoilState(objectMetadataItemsState); - - setMetadataItems(generatedMockObjectMetadataItems); - - return useFilteredSearchRecordQuery({ - selectedIds: ['1'], - limit: 10, - excludedRecordIds: ['2'], - objectNameSingulars: ['person'], - searchFilter: 'Entity', - }); - }, - { wrapper: Wrapper }, - ); - - const expectedResult: MultipleRecordPickerRecords = { - selectedRecords: [], - filteredSelectedRecords: [], - recordsToSelect: [], - loading: true, - }; - - expect(result.current).toEqual(expectedResult); - }); -}); diff --git a/packages/twenty-front/src/modules/search/hooks/useFilteredSearchRecordQuery.ts b/packages/twenty-front/src/modules/search/hooks/useFilteredSearchRecordQuery.ts deleted file mode 100644 index 49c0c6a741..0000000000 --- a/packages/twenty-front/src/modules/search/hooks/useFilteredSearchRecordQuery.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { formatSearchRecordAsSingleRecordPickerRecord } from '@/object-metadata/utils/formatSearchRecordAsSingleRecordPickerRecord'; -import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit'; -import { useObjectRecordSearchRecords } from '@/object-record/hooks/useObjectRecordSearchRecords'; -import { type SingleRecordPickerRecord } from '@/object-record/record-picker/single-record-picker/types/SingleRecordPickerRecord'; -import { isDefined } from 'twenty-shared/utils'; - -export const useFilteredSearchRecordQuery = ({ - selectedIds, - limit, - excludedRecordIds = [], - objectNameSingulars, - searchFilter, -}: { - selectedIds: string[]; - limit?: number; - excludedRecordIds?: string[]; - objectNameSingulars: string[]; - searchFilter?: string; -}): { - selectedRecords: SingleRecordPickerRecord[]; - filteredSelectedRecords: SingleRecordPickerRecord[]; - recordsToSelect: SingleRecordPickerRecord[]; - loading: boolean; -} => { - const selectedIdsFilter = { id: { in: selectedIds } }; - - const { loading: selectedRecordsLoading, searchRecords: selectedRecords } = - useObjectRecordSearchRecords({ - objectNameSingulars, - filter: selectedIdsFilter, - skip: !selectedIds.length, - searchInput: '', - }); - - const { - loading: filteredSelectedRecordsLoading, - searchRecords: filteredSelectedRecords, - } = useObjectRecordSearchRecords({ - objectNameSingulars, - filter: selectedIdsFilter, - skip: !selectedIds.length, - searchInput: searchFilter, - }); - - const notFilterIds = [...selectedIds, ...excludedRecordIds]; - const notFilter = notFilterIds.length - ? { not: { id: { in: notFilterIds } } } - : undefined; - const { loading: recordsToSelectLoading, searchRecords: recordsToSelect } = - useObjectRecordSearchRecords({ - objectNameSingulars, - filter: notFilter, - limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT, - searchInput: searchFilter, - fetchPolicy: 'cache-and-network', - }); - - return { - selectedRecords: selectedRecords - .map(formatSearchRecordAsSingleRecordPickerRecord) - .filter(isDefined), - filteredSelectedRecords: filteredSelectedRecords - .map(formatSearchRecordAsSingleRecordPickerRecord) - .filter(isDefined), - recordsToSelect: recordsToSelect - .map(formatSearchRecordAsSingleRecordPickerRecord) - .filter(isDefined), - loading: - recordsToSelectLoading || - filteredSelectedRecordsLoading || - selectedRecordsLoading, - }; -}; diff --git a/packages/twenty-front/src/modules/search/types/interface.ts b/packages/twenty-front/src/modules/search/types/interface.ts deleted file mode 100644 index bfaeb9e64c..0000000000 --- a/packages/twenty-front/src/modules/search/types/interface.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { type DocumentNode } from 'graphql'; - -export type SearchConfigType = { - query: DocumentNode; - template: (searchInput: string, currentSelectedId?: string) => any; - resultMapper: (data: any) => any; -};