From ac233b771c509da6f448a622547542dc46497e3d Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Thu, 7 Nov 2024 17:09:19 +0100 Subject: [PATCH] Simplify multi-object picker logic with search (#8010) Simplifying the logic around multi-object pickers and search by getting rid of the behaviour that keeped selected elements even when they did not match the search filter (eg keeping selected record "Brian Chesky" in dropdown even when search input is "Qonto"). This allows us to simplify the fetch queries around the search to only do one query. --------- Co-authored-by: Lucas Bordeau --- .../ActivityTargetInlineCellEditMode.tsx | 2 + ...onFromManyFieldInputMultiRecordsEffect.tsx | 2 +- ...ctRecordMultiSelectComponentFamilyState.ts | 2 +- ...ctMatchesFilterRecordsIdsComponentState.ts | 8 + ...etInlineCellEditModeMultiRecordsEffect.tsx | 67 +++------ ...EditModeMultiRecordsSearchFilterEffect.tsx | 54 +++++++ .../__tests__/useMultiObjectSearch.test.tsx | 141 ------------------ ...ltFormattedAsObjectRecordForSelectArray.ts | 40 +++-- .../hooks/useMultiObjectSearch.ts | 76 ---------- ...atchesSearchFilterAndSelectedItemsQuery.ts | 120 --------------- ...archMatchesSearchFilterAndToSelectQuery.ts | 110 -------------- ...ltiObjectSearchMatchesSearchFilterQuery.ts | 75 ++++++++++ .../useMultiObjectSearchSelectedItemsQuery.ts | 2 +- .../formatMultiObjectRecordSearchResults.ts | 16 ++ .../types/ObjectRecordForSelect.ts | 9 ++ .../types/SelectedObjectRecordId.ts | 4 + 16 files changed, 214 insertions(+), 514 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-field/states/objectRecordMultiSelectMatchesFilterRecordsIdsComponentState.ts create mode 100644 packages/twenty-front/src/modules/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect.tsx delete mode 100644 packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectSearch.test.tsx delete mode 100644 packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearch.ts delete mode 100644 packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery.ts delete mode 100644 packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery.ts create mode 100644 packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterQuery.ts create mode 100644 packages/twenty-front/src/modules/object-record/relation-picker/utils/formatMultiObjectRecordSearchResults.ts create mode 100644 packages/twenty-front/src/modules/object-record/types/ObjectRecordForSelect.ts create mode 100644 packages/twenty-front/src/modules/object-record/types/SelectedObjectRecordId.ts 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 5b9d62b6cd7a..7719f4576bed 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 @@ -27,6 +27,7 @@ import { import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlineCell'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { ActivityTargetInlineCellEditModeMultiRecordsEffect } from '@/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsEffect'; +import { ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect } from '@/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect'; import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect'; import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope'; import { prefillRecord } from '@/object-record/utils/prefillRecord'; @@ -287,6 +288,7 @@ export const ActivityTargetInlineCellEditMode = ({ + diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect.tsx index 6f42a7fbad2e..866edfdada99 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect.tsx @@ -5,10 +5,10 @@ import { useObjectRecordMultiSelectScopedStates } from '@/activities/hooks/useOb import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useRelationField } from '@/object-record/record-field/meta-types/hooks/useRelationField'; import { objectRecordMultiSelectComponentFamilyState } from '@/object-record/record-field/states/objectRecordMultiSelectComponentFamilyState'; -import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch'; import { useRelationPickerEntitiesOptions } from '@/object-record/relation-picker/hooks/useRelationPickerEntitiesOptions'; import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext'; import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; +import { ObjectRecordForSelect } from '@/object-record/types/ObjectRecordForSelect'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/states/objectRecordMultiSelectComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-field/states/objectRecordMultiSelectComponentFamilyState.ts index 0e15c962e11b..e4150ae029c6 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/states/objectRecordMultiSelectComponentFamilyState.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/states/objectRecordMultiSelectComponentFamilyState.ts @@ -1,4 +1,4 @@ -import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch'; +import { ObjectRecordForSelect } from '@/object-record/types/ObjectRecordForSelect'; import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState'; export type ObjectRecordAndSelected = ObjectRecordForSelect & { diff --git a/packages/twenty-front/src/modules/object-record/record-field/states/objectRecordMultiSelectMatchesFilterRecordsIdsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-field/states/objectRecordMultiSelectMatchesFilterRecordsIdsComponentState.ts new file mode 100644 index 000000000000..bfaebeaa86fa --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/states/objectRecordMultiSelectMatchesFilterRecordsIdsComponentState.ts @@ -0,0 +1,8 @@ +import { ObjectRecordForSelect } from '@/object-record/types/ObjectRecordForSelect'; +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +export const objectRecordMultiSelectMatchesFilterRecordsIdsComponentState = + createComponentState({ + key: 'objectRecordMultiSelectMatchesFilterRecordsIdsComponentState', + defaultValue: [], + }); diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsEffect.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsEffect.tsx index 425877c20e15..26981f3c0396 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsEffect.tsx @@ -7,15 +7,11 @@ import { } from 'recoil'; import { useObjectRecordMultiSelectScopedStates } from '@/activities/hooks/useObjectRecordMultiSelectScopedStates'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { objectRecordMultiSelectComponentFamilyState } from '@/object-record/record-field/states/objectRecordMultiSelectComponentFamilyState'; -import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates'; -import { - ObjectRecordForSelect, - SelectedObjectRecordId, - useMultiObjectSearch, -} from '@/object-record/relation-picker/hooks/useMultiObjectSearch'; +import { objectRecordMultiSelectMatchesFilterRecordsIdsComponentState } from '@/object-record/record-field/states/objectRecordMultiSelectMatchesFilterRecordsIdsComponentState'; import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext'; +import { ObjectRecordForSelect } from '@/object-record/types/ObjectRecordForSelect'; +import { SelectedObjectRecordId } from '@/object-record/types/SelectedObjectRecordId'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; @@ -30,43 +26,14 @@ export const ActivityTargetInlineCellEditModeMultiRecordsEffect = ({ const { objectRecordsIdsMultiSelectState, objectRecordMultiSelectCheckedRecordsIdsState, - recordMultiSelectIsLoadingState, } = useObjectRecordMultiSelectScopedStates(scopeId); const [objectRecordsIdsMultiSelect, setObjectRecordsIdsMultiSelect] = useRecoilState(objectRecordsIdsMultiSelectState); - const setRecordMultiSelectIsLoading = useSetRecoilState( - recordMultiSelectIsLoadingState, - ); - - const relationPickerScopedId = useAvailableScopeIdOrThrow( - RelationPickerScopeInternalContext, - ); - - const { relationPickerSearchFilterState } = useRelationPickerScopedStates({ - relationPickerScopedId, - }); - const relationPickerSearchFilter = useRecoilValue( - relationPickerSearchFilterState, + const setObjectRecordMultiSelectCheckedRecordsIds = useSetRecoilState( + objectRecordMultiSelectCheckedRecordsIdsState, ); - const { filteredSelectedObjectRecords, loading, objectRecordsToSelect } = - useMultiObjectSearch({ - searchFilterValue: relationPickerSearchFilter, - excludedObjects: [ - CoreObjectNameSingular.Task, - CoreObjectNameSingular.Note, - ], - selectedObjectRecordIds, - excludedObjectRecordIds: [], - limit: 10, - }); - - const [ - objectRecordMultiSelectCheckedRecordsIds, - setObjectRecordMultiSelectCheckedRecordsIds, - ] = useRecoilState(objectRecordMultiSelectCheckedRecordsIdsState); - const updateRecords = useRecoilCallback( ({ snapshot, set }) => (newRecords: ObjectRecordForSelect[]) => { @@ -80,6 +47,10 @@ export const ActivityTargetInlineCellEditModeMultiRecordsEffect = ({ ) .getValue(); + const objectRecordMultiSelectCheckedRecordsIds = snapshot + .getLoadable(objectRecordMultiSelectCheckedRecordsIdsState) + .getValue(); + const newRecordWithSelected = { ...newRecord, selected: objectRecordMultiSelectCheckedRecordsIds.some( @@ -103,23 +74,25 @@ export const ActivityTargetInlineCellEditModeMultiRecordsEffect = ({ } } }, - [objectRecordMultiSelectCheckedRecordsIds, scopeId], + [objectRecordMultiSelectCheckedRecordsIdsState, scopeId], + ); + + const matchesSearchFilterObjectRecords = useRecoilValue( + objectRecordMultiSelectMatchesFilterRecordsIdsComponentState({ + scopeId, + }), ); useEffect(() => { - const allRecords = [ - ...(filteredSelectedObjectRecords ?? []), - ...(objectRecordsToSelect ?? []), - ]; + const allRecords = matchesSearchFilterObjectRecords ?? []; updateRecords(allRecords); const allRecordsIds = allRecords.map((record) => record.record.id); if (!isDeeplyEqual(allRecordsIds, objectRecordsIdsMultiSelect)) { setObjectRecordsIdsMultiSelect(allRecordsIds); } }, [ - filteredSelectedObjectRecords, + matchesSearchFilterObjectRecords, objectRecordsIdsMultiSelect, - objectRecordsToSelect, setObjectRecordsIdsMultiSelect, updateRecords, ]); @@ -130,9 +103,5 @@ export const ActivityTargetInlineCellEditModeMultiRecordsEffect = ({ ); }, [selectedObjectRecordIds, setObjectRecordMultiSelectCheckedRecordsIds]); - useEffect(() => { - setRecordMultiSelectIsLoading(loading); - }, [loading, setRecordMultiSelectIsLoading]); - return <>; }; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect.tsx new file mode 100644 index 000000000000..c216122c148c --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect.tsx @@ -0,0 +1,54 @@ +import { useEffect } from 'react'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; + +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { objectRecordMultiSelectMatchesFilterRecordsIdsComponentState } from '@/object-record/record-field/states/objectRecordMultiSelectMatchesFilterRecordsIdsComponentState'; +import { useRelationPickerScopedStates } from '@/object-record/relation-picker/hooks/internal/useRelationPickerScopedStates'; +import { useMultiObjectSearchMatchesSearchFilterQuery } from '@/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterQuery'; +import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext'; +import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; + +export const ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect = + () => { + const scopeId = useAvailableScopeIdOrThrow( + RelationPickerScopeInternalContext, + ); + + const setRecordMultiSelectMatchesFilterRecords = useSetRecoilState( + objectRecordMultiSelectMatchesFilterRecordsIdsComponentState({ + scopeId, + }), + ); + + const relationPickerScopedId = useAvailableScopeIdOrThrow( + RelationPickerScopeInternalContext, + ); + + const { relationPickerSearchFilterState } = useRelationPickerScopedStates({ + relationPickerScopedId, + }); + const relationPickerSearchFilter = useRecoilValue( + relationPickerSearchFilterState, + ); + + const { matchesSearchFilterObjectRecords } = + useMultiObjectSearchMatchesSearchFilterQuery({ + excludedObjects: [ + CoreObjectNameSingular.Task, + CoreObjectNameSingular.Note, + ], + searchFilterValue: relationPickerSearchFilter, + limit: 10, + }); + + useEffect(() => { + setRecordMultiSelectMatchesFilterRecords( + matchesSearchFilterObjectRecords, + ); + }, [ + setRecordMultiSelectMatchesFilterRecords, + matchesSearchFilterObjectRecords, + ]); + + return <>; + }; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectSearch.test.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectSearch.test.tsx deleted file mode 100644 index 0f27bd796e54..000000000000 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectSearch.test.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { - MultiObjectSearch, - ObjectRecordForSelect, - SelectedObjectRecordId, - useMultiObjectSearch, -} from '@/object-record/relation-picker/hooks/useMultiObjectSearch'; -import { useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery } from '@/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery'; -import { useMultiObjectSearchMatchesSearchFilterAndToSelectQuery } from '@/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery'; -import { useMultiObjectSearchSelectedItemsQuery } from '@/object-record/relation-picker/hooks/useMultiObjectSearchSelectedItemsQuery'; -import { renderHook } from '@testing-library/react'; -import { FieldMetadataType } from '~/generated/graphql'; - -jest.mock( - '@/object-record/relation-picker/hooks/useMultiObjectSearchSelectedItemsQuery', -); -jest.mock( - '@/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery', -); -jest.mock( - '@/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery', -); - -const objectData: ObjectMetadataItem[] = [ - { - createdAt: 'createdAt', - id: 'id', - isActive: true, - isCustom: true, - isSystem: false, - isRemote: false, - labelPlural: 'labelPlural', - labelSingular: 'labelSingular', - namePlural: 'namePlural', - nameSingular: 'nameSingular', - isLabelSyncedWithName: false, - updatedAt: 'updatedAt', - fields: [ - { - id: 'f6a0a73a-5ee6-442e-b764-39b682471240', - name: 'id', - label: 'id', - type: FieldMetadataType.Uuid, - createdAt: '2024-01-01T00:00:00.000Z', - updatedAt: '2024-01-01T00:00:00.000Z', - isActive: true, - }, - ], - indexMetadatas: [], - }, -]; - -describe('useMultiObjectSearch', () => { - const selectedObjectRecordIds: SelectedObjectRecordId[] = [ - { objectNameSingular: 'object1', id: '1' }, - { objectNameSingular: 'object2', id: '2' }, - ]; - const searchFilterValue = 'searchValue'; - const limit = 5; - const excludedObjectRecordIds: SelectedObjectRecordId[] = [ - { objectNameSingular: 'object3', id: '3' }, - { objectNameSingular: 'object4', id: '4' }, - ]; - const excludedObjects: CoreObjectNameSingular[] = []; - - const selectedObjectRecords: ObjectRecordForSelect[] = [ - { - objectMetadataItem: objectData[0], - record: { - __typename: 'ObjectRecord', - id: '1', - createdAt: 'createdAt', - updatedAt: 'updatedAt', - }, - recordIdentifier: { - id: '1', - name: 'name', - }, - }, - ]; - const selectedObjectRecordsLoading = false; - - const selectedAndMatchesSearchFilterObjectRecords: ObjectRecordForSelect[] = - []; - const selectedAndMatchesSearchFilterObjectRecordsLoading = false; - - const toSelectAndMatchesSearchFilterObjectRecords: ObjectRecordForSelect[] = - []; - const toSelectAndMatchesSearchFilterObjectRecordsLoading = false; - - beforeEach(() => { - (useMultiObjectSearchSelectedItemsQuery as jest.Mock).mockReturnValue({ - selectedObjectRecords, - selectedObjectRecordsLoading, - }); - - ( - useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery as jest.Mock - ).mockReturnValue({ - selectedAndMatchesSearchFilterObjectRecords, - selectedAndMatchesSearchFilterObjectRecordsLoading, - }); - - ( - useMultiObjectSearchMatchesSearchFilterAndToSelectQuery as jest.Mock - ).mockReturnValue({ - toSelectAndMatchesSearchFilterObjectRecords, - toSelectAndMatchesSearchFilterObjectRecordsLoading, - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should return the correct object records and loading state', () => { - const { result } = renderHook(() => - useMultiObjectSearch({ - searchFilterValue, - selectedObjectRecordIds, - limit, - excludedObjectRecordIds, - excludedObjects, - }), - ); - - const expected: MultiObjectSearch = { - selectedObjectRecords, - filteredSelectedObjectRecords: - selectedAndMatchesSearchFilterObjectRecords, - objectRecordsToSelect: toSelectAndMatchesSearchFilterObjectRecords, - loading: - selectedAndMatchesSearchFilterObjectRecordsLoading || - toSelectAndMatchesSearchFilterObjectRecordsLoading || - selectedObjectRecordsLoading, - }; - - expect(result.current).toEqual(expected); - }); -}); diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray.ts index a1047478a7ba..ebac778c44f9 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray.ts +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray.ts @@ -4,7 +4,8 @@ import { useRecoilValue } from 'recoil'; import { objectMetadataItemsByNamePluralMapSelector } from '@/object-metadata/states/objectMetadataItemsByNamePluralMapSelector'; import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier'; import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection'; -import { ObjectRecordForSelect } from '@/object-record/relation-picker/hooks/useMultiObjectSearch'; +import { formatMultiObjectRecordSearchResults } from '@/object-record/relation-picker/utils/formatMultiObjectRecordSearchResults'; +import { ObjectRecordForSelect } from '@/object-record/types/ObjectRecordForSelect'; import { isDefined } from '~/utils/isDefined'; export type MultiObjectRecordQueryResult = { @@ -24,25 +25,34 @@ export const useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArr objectMetadataItemsByNamePluralMapSelector, ); + const formattedMultiObjectRecordsQueryResult = useMemo(() => { + return formatMultiObjectRecordSearchResults( + multiObjectRecordsQueryResult, + ); + }, [multiObjectRecordsQueryResult]); + const objectRecordForSelectArray = useMemo(() => { - return Object.entries(multiObjectRecordsQueryResult ?? {}).flatMap( - ([namePlural, objectRecordConnection]) => { - const objectMetadataItem = - objectMetadataItemsByNamePluralMap.get(namePlural); + return Object.entries( + formattedMultiObjectRecordsQueryResult ?? {}, + ).flatMap(([namePlural, objectRecordConnection]) => { + const objectMetadataItem = + objectMetadataItemsByNamePluralMap.get(namePlural); - if (!isDefined(objectMetadataItem)) return []; + if (!isDefined(objectMetadataItem)) return []; - return objectRecordConnection.edges.map(({ node }) => ({ + return objectRecordConnection.edges.map(({ node }) => ({ + objectMetadataItem, + record: node, + recordIdentifier: getObjectRecordIdentifier({ objectMetadataItem, record: node, - recordIdentifier: getObjectRecordIdentifier({ - objectMetadataItem, - record: node, - }), - })) as ObjectRecordForSelect[]; - }, - ); - }, [multiObjectRecordsQueryResult, objectMetadataItemsByNamePluralMap]); + }), + })) as ObjectRecordForSelect[]; + }); + }, [ + formattedMultiObjectRecordsQueryResult, + objectMetadataItemsByNamePluralMap, + ]); return { objectRecordForSelectArray, diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearch.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearch.ts deleted file mode 100644 index 8651e7f428f8..000000000000 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearch.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery } from '@/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery'; -import { useMultiObjectSearchMatchesSearchFilterAndToSelectQuery } from '@/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery'; -import { useMultiObjectSearchSelectedItemsQuery } from '@/object-record/relation-picker/hooks/useMultiObjectSearchSelectedItemsQuery'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier'; - -export const MULTI_OBJECT_SEARCH_REQUEST_LIMIT = 5; - -export type ObjectRecordForSelect = { - objectMetadataItem: ObjectMetadataItem; - record: ObjectRecord; - recordIdentifier: ObjectRecordIdentifier; -}; - -export type SelectedObjectRecordId = { - objectNameSingular: string; - id: string; -}; - -export type MultiObjectSearch = { - selectedObjectRecords: ObjectRecordForSelect[]; - filteredSelectedObjectRecords: ObjectRecordForSelect[]; - objectRecordsToSelect: ObjectRecordForSelect[]; - loading: boolean; -}; - -export const useMultiObjectSearch = ({ - searchFilterValue, - selectedObjectRecordIds, - limit, - excludedObjectRecordIds = [], - excludedObjects, -}: { - searchFilterValue: string; - selectedObjectRecordIds: SelectedObjectRecordId[]; - limit?: number; - excludedObjectRecordIds?: SelectedObjectRecordId[]; - excludedObjects?: CoreObjectNameSingular[]; -}): MultiObjectSearch => { - const { selectedObjectRecords, selectedObjectRecordsLoading } = - useMultiObjectSearchSelectedItemsQuery({ - selectedObjectRecordIds, - }); - - const { - selectedAndMatchesSearchFilterObjectRecords, - selectedAndMatchesSearchFilterObjectRecordsLoading, - } = useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery({ - searchFilterValue, - selectedObjectRecordIds, - limit, - }); - - const { - toSelectAndMatchesSearchFilterObjectRecords, - toSelectAndMatchesSearchFilterObjectRecordsLoading, - } = useMultiObjectSearchMatchesSearchFilterAndToSelectQuery({ - excludedObjects, - excludedObjectRecordIds, - searchFilterValue, - selectedObjectRecordIds, - limit, - }); - - return { - selectedObjectRecords, - filteredSelectedObjectRecords: selectedAndMatchesSearchFilterObjectRecords, - objectRecordsToSelect: toSelectAndMatchesSearchFilterObjectRecords, - loading: - selectedAndMatchesSearchFilterObjectRecordsLoading || - toSelectAndMatchesSearchFilterObjectRecordsLoading || - selectedObjectRecordsLoading, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery.ts deleted file mode 100644 index b69ef1f40c6d..000000000000 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { useQuery } from '@apollo/client'; -import { isNonEmptyArray } from '@sniptt/guards'; -import { useRecoilValue } from 'recoil'; - -import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery'; -import { useGenerateCombinedSearchRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedSearchRecordsQuery'; -import { useLimitPerMetadataItem } from '@/object-record/relation-picker/hooks/useLimitPerMetadataItem'; -import { - MultiObjectRecordQueryResult, - useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray, -} from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray'; -import { SelectedObjectRecordId } from '@/object-record/relation-picker/hooks/useMultiObjectSearch'; -import { useMemo } from 'react'; -import { isDefined } from '~/utils/isDefined'; -import { capitalize } from '~/utils/string/capitalize'; - -export const formatSearchResults = ( - searchResults: MultiObjectRecordQueryResult | undefined, -): MultiObjectRecordQueryResult => { - if (!searchResults) { - return {}; - } - - return Object.entries(searchResults).reduce((acc, [key, value]) => { - let newKey = key.replace(/^search/, ''); - newKey = newKey.charAt(0).toLowerCase() + newKey.slice(1); - acc[newKey] = value; - return acc; - }, {} as MultiObjectRecordQueryResult); -}; - -export const useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery = ({ - selectedObjectRecordIds, - searchFilterValue, - limit, -}: { - selectedObjectRecordIds: SelectedObjectRecordId[]; - searchFilterValue: string; - limit?: number; -}) => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState); - - const objectMetadataItemsUsedInSelectedIdsQuery = useMemo( - () => - objectMetadataItems.filter(({ nameSingular }) => { - return selectedObjectRecordIds.some(({ objectNameSingular }) => { - return objectNameSingular === nameSingular; - }); - }), - [objectMetadataItems, selectedObjectRecordIds], - ); - - const selectedAndMatchesSearchFilterTextFilterPerMetadataItem = - Object.fromEntries( - objectMetadataItems - .map(({ nameSingular }) => { - const selectedIds = selectedObjectRecordIds - .filter( - ({ objectNameSingular }) => objectNameSingular === nameSingular, - ) - .map(({ id }) => id); - - if (!isNonEmptyArray(selectedIds)) return null; - - return [ - `filter${capitalize(nameSingular)}`, - { - id: { - in: selectedIds, - }, - }, - ]; - }) - .filter(isDefined), - ); - - const { limitPerMetadataItem } = useLimitPerMetadataItem({ - objectMetadataItems: objectMetadataItemsUsedInSelectedIdsQuery, - limit, - }); - - const multiSelectSearchQueryForSelectedIds = - useGenerateCombinedSearchRecordsQuery({ - operationSignatures: objectMetadataItemsUsedInSelectedIdsQuery.map( - (objectMetadataItem) => ({ - objectNameSingular: objectMetadataItem.nameSingular, - variables: {}, - }), - ), - }); - - const { - loading: selectedAndMatchesSearchFilterObjectRecordsLoading, - data: selectedAndMatchesSearchFilterObjectRecordsQueryResult, - } = useQuery( - multiSelectSearchQueryForSelectedIds ?? EMPTY_QUERY, - { - variables: { - search: searchFilterValue, - ...selectedAndMatchesSearchFilterTextFilterPerMetadataItem, - ...limitPerMetadataItem, - }, - skip: !isDefined(multiSelectSearchQueryForSelectedIds), - }, - ); - - const { - objectRecordForSelectArray: selectedAndMatchesSearchFilterObjectRecords, - } = useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({ - multiObjectRecordsQueryResult: formatSearchResults( - selectedAndMatchesSearchFilterObjectRecordsQueryResult, - ), - }); - - return { - selectedAndMatchesSearchFilterObjectRecordsLoading, - selectedAndMatchesSearchFilterObjectRecords, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery.ts deleted file mode 100644 index c3150cd44ca8..000000000000 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { useQuery } from '@apollo/client'; -import { useRecoilValue } from 'recoil'; - -import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery'; -import { useGenerateCombinedSearchRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedSearchRecordsQuery'; -import { useLimitPerMetadataItem } from '@/object-record/relation-picker/hooks/useLimitPerMetadataItem'; -import { - MultiObjectRecordQueryResult, - useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray, -} from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray'; -import { SelectedObjectRecordId } from '@/object-record/relation-picker/hooks/useMultiObjectSearch'; -import { formatSearchResults } from '@/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery'; -import { isObjectMetadataItemSearchableInCombinedRequest } from '@/object-record/utils/isObjectMetadataItemSearchableInCombinedRequest'; -import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; -import { isDefined } from '~/utils/isDefined'; -import { capitalize } from '~/utils/string/capitalize'; - -export const useMultiObjectSearchMatchesSearchFilterAndToSelectQuery = ({ - selectedObjectRecordIds, - excludedObjectRecordIds, - searchFilterValue, - limit, - excludedObjects, -}: { - selectedObjectRecordIds: SelectedObjectRecordId[]; - excludedObjectRecordIds: SelectedObjectRecordId[]; - searchFilterValue: string; - limit?: number; - excludedObjects?: CoreObjectNameSingular[]; -}) => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState); - - const selectableObjectMetadataItems = objectMetadataItems - .filter(({ isSystem, isRemote }) => !isSystem && !isRemote) - .filter(({ nameSingular }) => { - return !excludedObjects?.includes(nameSingular as CoreObjectNameSingular); - }) - .filter((object) => - isObjectMetadataItemSearchableInCombinedRequest(object), - ); - - const objectRecordsToSelectAndMatchesSearchFilterTextFilterPerMetadataItem = - Object.fromEntries( - selectableObjectMetadataItems - .map(({ nameSingular }) => { - const selectedIds = selectedObjectRecordIds - .filter( - ({ objectNameSingular }) => objectNameSingular === nameSingular, - ) - .map(({ id }) => id); - - const excludedIds = excludedObjectRecordIds - .filter( - ({ objectNameSingular }) => objectNameSingular === nameSingular, - ) - .map(({ id }) => id); - - const excludedIdsUnion = [...selectedIds, ...excludedIds]; - const excludedIdsFilter = excludedIdsUnion.length - ? { not: { id: { in: excludedIdsUnion } } } - : undefined; - - return [ - `filter${capitalize(nameSingular)}`, - makeAndFilterVariables([excludedIdsFilter]), - ]; - }) - .filter(isDefined), - ); - const { limitPerMetadataItem } = useLimitPerMetadataItem({ - objectMetadataItems: selectableObjectMetadataItems, - limit, - }); - - const multiSelectQuery = useGenerateCombinedSearchRecordsQuery({ - operationSignatures: selectableObjectMetadataItems.map( - (objectMetadataItem) => ({ - objectNameSingular: objectMetadataItem.nameSingular, - variables: {}, - }), - ), - }); - - const { - loading: toSelectAndMatchesSearchFilterObjectRecordsLoading, - data: toSelectAndMatchesSearchFilterObjectRecordsQueryResult, - } = useQuery(multiSelectQuery ?? EMPTY_QUERY, { - variables: { - search: searchFilterValue, - ...objectRecordsToSelectAndMatchesSearchFilterTextFilterPerMetadataItem, - ...limitPerMetadataItem, - }, - skip: !isDefined(multiSelectQuery), - }); - - const { - objectRecordForSelectArray: toSelectAndMatchesSearchFilterObjectRecords, - } = useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({ - multiObjectRecordsQueryResult: formatSearchResults( - toSelectAndMatchesSearchFilterObjectRecordsQueryResult, - ), - }); - - return { - toSelectAndMatchesSearchFilterObjectRecordsLoading, - toSelectAndMatchesSearchFilterObjectRecords, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterQuery.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterQuery.ts new file mode 100644 index 000000000000..a8bdfec35419 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterQuery.ts @@ -0,0 +1,75 @@ +import { useQuery } from '@apollo/client'; +import { useRecoilValue } from 'recoil'; + +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery'; +import { useGenerateCombinedSearchRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedSearchRecordsQuery'; +import { useLimitPerMetadataItem } from '@/object-record/relation-picker/hooks/useLimitPerMetadataItem'; +import { + MultiObjectRecordQueryResult, + useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray, +} from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray'; +import { isObjectMetadataItemSearchableInCombinedRequest } from '@/object-record/utils/isObjectMetadataItemSearchableInCombinedRequest'; +import { isDefined } from '~/utils/isDefined'; + +export const useMultiObjectSearchMatchesSearchFilterQuery = ({ + searchFilterValue, + limit, + excludedObjects, +}: { + searchFilterValue: string; + limit?: number; + excludedObjects?: CoreObjectNameSingular[]; +}) => { + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + + const selectableObjectMetadataItems = objectMetadataItems + .filter(({ isSystem, isRemote }) => !isSystem && !isRemote) + .filter(({ nameSingular }) => { + return !excludedObjects?.includes(nameSingular as CoreObjectNameSingular); + }) + .filter((objectMetadataItem) => + isObjectMetadataItemSearchableInCombinedRequest(objectMetadataItem), + ); + + const { limitPerMetadataItem } = useLimitPerMetadataItem({ + objectMetadataItems, + limit, + }); + + const multiSelectSearchQueryForSelectedIds = + useGenerateCombinedSearchRecordsQuery({ + operationSignatures: selectableObjectMetadataItems.map( + (objectMetadataItem) => ({ + objectNameSingular: objectMetadataItem.nameSingular, + variables: {}, + }), + ), + }); + + const { + loading: matchesSearchFilterObjectRecordsLoading, + data: matchesSearchFilterObjectRecordsQueryResult, + } = useQuery( + multiSelectSearchQueryForSelectedIds ?? EMPTY_QUERY, + { + variables: { + search: searchFilterValue, + ...limitPerMetadataItem, + }, + skip: !isDefined(multiSelectSearchQueryForSelectedIds), + }, + ); + + const { objectRecordForSelectArray: matchesSearchFilterObjectRecords } = + useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({ + multiObjectRecordsQueryResult: + matchesSearchFilterObjectRecordsQueryResult, + }); + + return { + matchesSearchFilterObjectRecordsLoading, + matchesSearchFilterObjectRecords, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchSelectedItemsQuery.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchSelectedItemsQuery.ts index 5656cb77f5b9..a4fd29ddcbb5 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchSelectedItemsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchSelectedItemsQuery.ts @@ -9,8 +9,8 @@ import { MultiObjectRecordQueryResult, useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray, } from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray'; -import { SelectedObjectRecordId } from '@/object-record/relation-picker/hooks/useMultiObjectSearch'; import { useOrderByFieldPerMetadataItem } from '@/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem'; +import { SelectedObjectRecordId } from '@/object-record/types/SelectedObjectRecordId'; import { isDefined } from '~/utils/isDefined'; import { capitalize } from '~/utils/string/capitalize'; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/utils/formatMultiObjectRecordSearchResults.ts b/packages/twenty-front/src/modules/object-record/relation-picker/utils/formatMultiObjectRecordSearchResults.ts new file mode 100644 index 000000000000..318f12ab7548 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/relation-picker/utils/formatMultiObjectRecordSearchResults.ts @@ -0,0 +1,16 @@ +import { MultiObjectRecordQueryResult } from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray'; + +export const formatMultiObjectRecordSearchResults = ( + searchResults: MultiObjectRecordQueryResult | undefined | null, +): MultiObjectRecordQueryResult => { + if (!searchResults) { + return {}; + } + + return Object.entries(searchResults).reduce((acc, [key, value]) => { + let newKey = key.replace(/^search/, ''); + newKey = newKey.charAt(0).toLowerCase() + newKey.slice(1); + acc[newKey] = value; + return acc; + }, {} as MultiObjectRecordQueryResult); +}; diff --git a/packages/twenty-front/src/modules/object-record/types/ObjectRecordForSelect.ts b/packages/twenty-front/src/modules/object-record/types/ObjectRecordForSelect.ts new file mode 100644 index 000000000000..a1266fb6f702 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/types/ObjectRecordForSelect.ts @@ -0,0 +1,9 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier'; + +export type ObjectRecordForSelect = { + objectMetadataItem: ObjectMetadataItem; + record: ObjectRecord; + recordIdentifier: ObjectRecordIdentifier; +}; diff --git a/packages/twenty-front/src/modules/object-record/types/SelectedObjectRecordId.ts b/packages/twenty-front/src/modules/object-record/types/SelectedObjectRecordId.ts new file mode 100644 index 000000000000..2c9bb2353892 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/types/SelectedObjectRecordId.ts @@ -0,0 +1,4 @@ +export type SelectedObjectRecordId = { + objectNameSingular: string; + id: string; +};