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:
@ -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';
|
||||
|
||||
|
||||
@ -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 & {
|
||||
|
||||
@ -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: [],
|
||||
});
|
||||
@ -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 <></>;
|
||||
};
|
||||
|
||||
@ -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 <></>;
|
||||
};
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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);
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -0,0 +1,4 @@
|
||||
export type SelectedObjectRecordId = {
|
||||
objectNameSingular: string;
|
||||
id: string;
|
||||
};
|
||||
Reference in New Issue
Block a user