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