Use search instead of findMany in relation pickers (#7798)
First step of #https://github.com/twentyhq/twenty/issues/3298. Here we update the search endpoint to allow for a filter argument, which we currently use in the relation pickers to restrict or exclude ids from search. In a future PR we will try to simplify the search logic in the FE
This commit is contained in:
@ -11,6 +11,7 @@ export const GotoHotkeys = () => {
|
||||
|
||||
return nonSystemActiveObjectMetadataItems.map((objectMetadataItem) => (
|
||||
<GoToHotkeyItemEffect
|
||||
key={`go-to-hokey-item-${objectMetadataItem.id}`}
|
||||
hotkey={objectMetadataItem.namePlural[0]}
|
||||
pathToNavigateTo={`/objects/${objectMetadataItem.namePlural}`}
|
||||
/>
|
||||
|
||||
@ -85,6 +85,7 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
|
||||
objectMetadataItemsForNavigationItems.map(
|
||||
(objectMetadataItem) => (
|
||||
<NavigationDrawerItemForObjectMetadataItem
|
||||
key={`navigation-drawer-item-${objectMetadataItem.id}`}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
),
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
||||
|
||||
export const ObjectMetadataItemsRelationPickerEffect = ({
|
||||
relationPickerScopeId,
|
||||
}: {
|
||||
relationPickerScopeId?: string;
|
||||
} = {}) => {
|
||||
const { setSearchQuery } = useRelationPicker({ relationPickerScopeId });
|
||||
|
||||
const computeFilterFields = (relationPickerType: string) => {
|
||||
if (relationPickerType === 'company') {
|
||||
return ['name'];
|
||||
}
|
||||
|
||||
if (['workspaceMember', 'person'].includes(relationPickerType)) {
|
||||
return ['name.firstName', 'name.lastName'];
|
||||
}
|
||||
|
||||
return ['name'];
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSearchQuery({ computeFilterFields });
|
||||
}, [setSearchQuery]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -13,10 +13,11 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useQuery, WatchQueryFetchPolicy } from '@apollo/client';
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { logError } from '~/utils/logError';
|
||||
|
||||
export type UseSearchRecordsParams = ObjectMetadataItemIdentifier &
|
||||
RecordGqlOperationVariables & {
|
||||
Pick<RecordGqlOperationVariables, 'filter' | 'limit'> & {
|
||||
onError?: (error?: Error) => void;
|
||||
skip?: boolean;
|
||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||
@ -29,6 +30,7 @@ export const useSearchRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
searchInput,
|
||||
limit,
|
||||
skip,
|
||||
filter,
|
||||
recordGqlFields,
|
||||
fetchPolicy,
|
||||
}: UseSearchRecordsParams) => {
|
||||
@ -45,10 +47,14 @@ export const useSearchRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
const { data, loading, error, previousData } =
|
||||
useQuery<RecordGqlOperationSearchResult>(searchRecordsQuery, {
|
||||
skip:
|
||||
skip || !objectMetadataItem || !currentWorkspaceMember || !searchInput,
|
||||
skip ||
|
||||
!objectMetadataItem ||
|
||||
!currentWorkspaceMember ||
|
||||
!isDefined(searchInput),
|
||||
variables: {
|
||||
search: searchInput,
|
||||
limit: limit,
|
||||
filter: filter,
|
||||
},
|
||||
fetchPolicy: fetchPolicy,
|
||||
onError: (error) => {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { RelationFromManyFieldInputMultiRecordsEffect } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect';
|
||||
@ -54,7 +53,6 @@ export const RelationFromManyFieldInput = ({
|
||||
return (
|
||||
<>
|
||||
<RelationPickerScope relationPickerScopeId={relationPickerScopeId}>
|
||||
<ObjectMetadataItemsRelationPickerEffect />
|
||||
<RelationFromManyFieldInputMultiRecordsEffect />
|
||||
<MultiRecordSelect
|
||||
onSubmit={handleSubmit}
|
||||
|
||||
@ -4,7 +4,6 @@ import { useCallback, useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { IconForbid, IconPencil, IconPlus } from 'twenty-ui';
|
||||
|
||||
import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
@ -209,7 +208,6 @@ export const RecordDetailRelationSection = ({
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<ObjectMetadataItemsRelationPickerEffect />
|
||||
<RelationFromManyFieldInputMultiRecordsEffect />
|
||||
<MultiRecordSelect
|
||||
onCreate={createNewRecordAndOpenRightDrawer}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect';
|
||||
import {
|
||||
SingleEntitySelectMenuItems,
|
||||
SingleEntitySelectMenuItemsProps,
|
||||
@ -65,9 +64,6 @@ export const SingleEntitySelectMenuItemsWithSearch = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<ObjectMetadataItemsRelationPickerEffect
|
||||
relationPickerScopeId={relationPickerScopeId}
|
||||
/>
|
||||
<DropdownMenuSearchInput onChange={handleSearchFilterChange} autoFocus />
|
||||
<DropdownMenuSeparator />
|
||||
<SingleEntitySelectMenuItems
|
||||
|
||||
@ -18,24 +18,15 @@ export const useRelationPickerEntitiesOptions = ({
|
||||
RelationPickerScopeInternalContext,
|
||||
);
|
||||
|
||||
const { searchQueryState, relationPickerSearchFilterState } =
|
||||
useRelationPickerScopedStates({
|
||||
relationPickerScopedId: scopeId,
|
||||
});
|
||||
const { relationPickerSearchFilterState } = useRelationPickerScopedStates({
|
||||
relationPickerScopedId: scopeId,
|
||||
});
|
||||
const relationPickerSearchFilter = useRecoilValue(
|
||||
relationPickerSearchFilterState,
|
||||
);
|
||||
|
||||
const searchQuery = useRecoilValue(searchQueryState);
|
||||
const entities = useFilteredSearchEntityQuery({
|
||||
filters: [
|
||||
{
|
||||
fieldNames:
|
||||
searchQuery?.computeFilterFields?.(relationObjectNameSingular) ?? [],
|
||||
filter: relationPickerSearchFilter,
|
||||
},
|
||||
],
|
||||
orderByField: 'createdAt',
|
||||
searchFilter: relationPickerSearchFilter,
|
||||
selectedIds: selectedRelationRecordIds,
|
||||
excludeRecordIds: excludedRelationRecordIds,
|
||||
objectNameSingular: relationObjectNameSingular,
|
||||
|
||||
@ -15,12 +15,14 @@ export const generateSearchRecordsQuery = ({
|
||||
computeReferences,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
objectMetadataItems: ObjectMetadataItem[]; // TODO - what is this used for?
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||
computeReferences?: boolean;
|
||||
}) => gql`
|
||||
query Search${capitalize(objectMetadataItem.namePlural)}($search: String, $limit: Int) {
|
||||
${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(searchInput: $search, limit: $limit){
|
||||
query Search${capitalize(objectMetadataItem.namePlural)}($search: String, $limit: Int, $filter: ${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}FilterInput) {
|
||||
${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(searchInput: $search, limit: $limit, filter: $filter){
|
||||
edges {
|
||||
node ${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems,
|
||||
|
||||
@ -80,13 +80,11 @@ describe('useFilteredSearchEntityQuery', () => {
|
||||
setMetadataItems(generatedMockObjectMetadataItems);
|
||||
|
||||
return useFilteredSearchEntityQuery({
|
||||
orderByField: 'name',
|
||||
filters: [{ fieldNames: ['name'], filter: 'Entity' }],
|
||||
sortOrder: 'AscNullsLast',
|
||||
selectedIds: ['1'],
|
||||
limit: 10,
|
||||
excludeRecordIds: ['2'],
|
||||
objectNameSingular: 'person',
|
||||
searchFilter: 'Entity',
|
||||
});
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
|
||||
@ -1,39 +1,26 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier';
|
||||
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit';
|
||||
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { useSearchRecords } from '@/object-record/hooks/useSearchRecords';
|
||||
import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect';
|
||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
|
||||
import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables';
|
||||
import { OrderBy } from '@/types/OrderBy';
|
||||
import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
type SearchFilter = { fieldNames: string[]; filter: string | number };
|
||||
|
||||
// TODO: use this for all search queries, because we need selectedEntities and entitiesToSelect each time we want to search
|
||||
// Filtered entities to select are
|
||||
|
||||
export const useFilteredSearchEntityQuery = ({
|
||||
orderByField,
|
||||
filters,
|
||||
sortOrder = 'AscNullsLast',
|
||||
selectedIds,
|
||||
limit,
|
||||
excludeRecordIds = [],
|
||||
objectNameSingular,
|
||||
searchFilter,
|
||||
}: {
|
||||
orderByField: string;
|
||||
filters: SearchFilter[];
|
||||
sortOrder?: OrderBy;
|
||||
selectedIds: string[];
|
||||
limit?: number;
|
||||
excludeRecordIds?: string[];
|
||||
objectNameSingular: string;
|
||||
searchFilter?: string;
|
||||
}): EntitiesForMultipleEntitySelect<EntityForSelect> => {
|
||||
const { mapToObjectRecordIdentifier } = useMapToObjectRecordIdentifier({
|
||||
objectNameSingular,
|
||||
@ -46,55 +33,21 @@ export const useFilteredSearchEntityQuery = ({
|
||||
const selectedIdsFilter = { id: { in: selectedIds } };
|
||||
|
||||
const { loading: selectedRecordsLoading, records: selectedRecords } =
|
||||
useFindManyRecords({
|
||||
useSearchRecords({
|
||||
objectNameSingular,
|
||||
filter: selectedIdsFilter,
|
||||
orderBy: [{ [orderByField]: sortOrder }],
|
||||
skip: !selectedIds.length,
|
||||
searchInput: searchFilter,
|
||||
});
|
||||
|
||||
const searchFilters = filters.map(({ fieldNames, filter }) => {
|
||||
if (!isNonEmptyString(filter)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const formattedFilters = fieldNames.reduce(
|
||||
(previousValue: RecordGqlOperationFilter[], fieldName) => {
|
||||
const [parentFieldName, subFieldName] = fieldName.split('.');
|
||||
|
||||
if (isNonEmptyString(subFieldName)) {
|
||||
// Composite field
|
||||
return [
|
||||
...previousValue,
|
||||
...generateILikeFiltersForCompositeFields(filter, parentFieldName, [
|
||||
subFieldName,
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
...previousValue,
|
||||
{
|
||||
[fieldName]: {
|
||||
ilike: `%${filter}%`,
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return makeOrFilterVariables(formattedFilters);
|
||||
});
|
||||
|
||||
const {
|
||||
loading: filteredSelectedRecordsLoading,
|
||||
records: filteredSelectedRecords,
|
||||
} = useFindManyRecords({
|
||||
} = useSearchRecords({
|
||||
objectNameSingular,
|
||||
filter: makeAndFilterVariables([...searchFilters, selectedIdsFilter]),
|
||||
orderBy: [{ [orderByField]: sortOrder }],
|
||||
filter: selectedIdsFilter,
|
||||
skip: !selectedIds.length,
|
||||
searchInput: searchFilter,
|
||||
});
|
||||
|
||||
const notFilterIds = [...selectedIds, ...excludeRecordIds];
|
||||
@ -102,11 +55,11 @@ export const useFilteredSearchEntityQuery = ({
|
||||
? { not: { id: { in: notFilterIds } } }
|
||||
: undefined;
|
||||
const { loading: recordsToSelectLoading, records: recordsToSelect } =
|
||||
useFindManyRecords({
|
||||
useSearchRecords({
|
||||
objectNameSingular,
|
||||
filter: makeAndFilterVariables([...searchFilters, notFilter]),
|
||||
filter: notFilter,
|
||||
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
|
||||
orderBy: [{ [orderByField]: sortOrder }],
|
||||
searchInput: searchFilter,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user