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 <bordeau.lucas@gmail.com>
This commit is contained in:
Marie
2024-11-07 17:09:19 +01:00
committed by GitHub
parent 2ab4aa1377
commit ac233b771c
16 changed files with 214 additions and 514 deletions

View File

@ -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 = ({
<ActivityTargetInlineCellEditModeMultiRecordsEffect
selectedObjectRecordIds={selectedTargetObjectIds}
/>
<ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect />
<MultiRecordSelect onSubmit={handleSubmit} onChange={handleChange} />
</RelationPickerScope>
</StyledSelectContainer>

View File

@ -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';

View File

@ -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 & {

View File

@ -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<ObjectRecordForSelect[]>({
key: 'objectRecordMultiSelectMatchesFilterRecordsIdsComponentState',
defaultValue: [],
});

View File

@ -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 setObjectRecordMultiSelectCheckedRecordsIds = useSetRecoilState(
objectRecordMultiSelectCheckedRecordsIdsState,
);
const relationPickerScopedId = useAvailableScopeIdOrThrow(
RelationPickerScopeInternalContext,
);
const { relationPickerSearchFilterState } = useRelationPickerScopedStates({
relationPickerScopedId,
});
const relationPickerSearchFilter = useRecoilValue(
relationPickerSearchFilterState,
);
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 <></>;
};

View File

@ -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 <></>;
};

View File

@ -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);
});
});

View File

@ -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,

View File

@ -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,
};
};

View File

@ -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<MultiObjectRecordQueryResult>(
multiSelectSearchQueryForSelectedIds ?? EMPTY_QUERY,
{
variables: {
search: searchFilterValue,
...selectedAndMatchesSearchFilterTextFilterPerMetadataItem,
...limitPerMetadataItem,
},
skip: !isDefined(multiSelectSearchQueryForSelectedIds),
},
);
const {
objectRecordForSelectArray: selectedAndMatchesSearchFilterObjectRecords,
} = useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({
multiObjectRecordsQueryResult: formatSearchResults(
selectedAndMatchesSearchFilterObjectRecordsQueryResult,
),
});
return {
selectedAndMatchesSearchFilterObjectRecordsLoading,
selectedAndMatchesSearchFilterObjectRecords,
};
};

View File

@ -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<MultiObjectRecordQueryResult>(multiSelectQuery ?? EMPTY_QUERY, {
variables: {
search: searchFilterValue,
...objectRecordsToSelectAndMatchesSearchFilterTextFilterPerMetadataItem,
...limitPerMetadataItem,
},
skip: !isDefined(multiSelectQuery),
});
const {
objectRecordForSelectArray: toSelectAndMatchesSearchFilterObjectRecords,
} = useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({
multiObjectRecordsQueryResult: formatSearchResults(
toSelectAndMatchesSearchFilterObjectRecordsQueryResult,
),
});
return {
toSelectAndMatchesSearchFilterObjectRecordsLoading,
toSelectAndMatchesSearchFilterObjectRecords,
};
};

View File

@ -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<MultiObjectRecordQueryResult>(
multiSelectSearchQueryForSelectedIds ?? EMPTY_QUERY,
{
variables: {
search: searchFilterValue,
...limitPerMetadataItem,
},
skip: !isDefined(multiSelectSearchQueryForSelectedIds),
},
);
const { objectRecordForSelectArray: matchesSearchFilterObjectRecords } =
useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({
multiObjectRecordsQueryResult:
matchesSearchFilterObjectRecordsQueryResult,
});
return {
matchesSearchFilterObjectRecordsLoading,
matchesSearchFilterObjectRecords,
};
};

View File

@ -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';

View File

@ -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);
};

View File

@ -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;
};

View File

@ -0,0 +1,4 @@
export type SelectedObjectRecordId = {
objectNameSingular: string;
id: string;
};