mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-26 13:42:44 +03:00
Use search in multi object pickers (#7909)
Fixes https://github.com/twentyhq/twenty/issues/3298. We still have some existing glitches in the picker yet to fix. --------- Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
parent
67fb750ef6
commit
c7bc301dba
@ -0,0 +1,96 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { isUndefined } from '@sniptt/guards';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
|
||||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
||||
import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField';
|
||||
import { isObjectMetadataItemSearchable } from '@/object-record/utils/isObjectMetadataItemSearchable';
|
||||
import { isNonEmptyArray } from '~/utils/isNonEmptyArray';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useGenerateCombinedSearchRecordsQuery = ({
|
||||
operationSignatures,
|
||||
}: {
|
||||
operationSignatures: RecordGqlOperationSignature[];
|
||||
}) => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
if (!isNonEmptyArray(operationSignatures)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filterPerMetadataItemArray = operationSignatures
|
||||
.map(
|
||||
({ objectNameSingular }) =>
|
||||
`$filter${capitalize(objectNameSingular)}: ${capitalize(
|
||||
objectNameSingular,
|
||||
)}FilterInput`,
|
||||
)
|
||||
.join(', ');
|
||||
|
||||
const limitPerMetadataItemArray = operationSignatures
|
||||
.map(
|
||||
({ objectNameSingular }) =>
|
||||
`$limit${capitalize(objectNameSingular)}: Int`,
|
||||
)
|
||||
.join(', ');
|
||||
|
||||
const queryKeyWithObjectMetadataItemArray = operationSignatures.map(
|
||||
(queryKey) => {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.nameSingular === queryKey.objectNameSingular,
|
||||
);
|
||||
|
||||
if (isUndefined(objectMetadataItem)) {
|
||||
throw new Error(
|
||||
`Object metadata item not found for object name singular: ${queryKey.objectNameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
return { ...queryKey, objectMetadataItem };
|
||||
},
|
||||
);
|
||||
|
||||
const filteredQueryKeyWithObjectMetadataItemArray =
|
||||
queryKeyWithObjectMetadataItemArray.filter(({ objectMetadataItem }) =>
|
||||
isObjectMetadataItemSearchable(objectMetadataItem),
|
||||
);
|
||||
|
||||
return gql`
|
||||
query CombinedSearchRecords(
|
||||
${filterPerMetadataItemArray},
|
||||
${limitPerMetadataItemArray},
|
||||
$search: String,
|
||||
) {
|
||||
${filteredQueryKeyWithObjectMetadataItemArray
|
||||
.map(
|
||||
({ objectMetadataItem, fields }) =>
|
||||
`${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(filter: $filter${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)},
|
||||
limit: $limit${capitalize(objectMetadataItem.nameSingular)},
|
||||
searchInput: $search
|
||||
){
|
||||
edges {
|
||||
node ${mapObjectMetadataToGraphQLQuery({
|
||||
objectMetadataItems: objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
recordGqlFields:
|
||||
fields ??
|
||||
generateDepthOneRecordGqlFields({
|
||||
objectMetadataItem,
|
||||
}),
|
||||
})}
|
||||
cursor
|
||||
}
|
||||
totalCount
|
||||
}`,
|
||||
)
|
||||
.join('\n')}
|
||||
}
|
||||
`;
|
||||
};
|
@ -4,18 +4,32 @@ import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery';
|
||||
import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery';
|
||||
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 { useOrderByFieldPerMetadataItem } from '@/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem';
|
||||
import { useSearchFilterPerMetadataItem } from '@/object-record/relation-picker/hooks/useSearchFilterPerMetadataItem';
|
||||
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,
|
||||
@ -27,18 +41,14 @@ export const useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery = ({
|
||||
}) => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const { searchFilterPerMetadataItemNameSingular } =
|
||||
useSearchFilterPerMetadataItem({
|
||||
objectMetadataItems,
|
||||
searchFilterValue,
|
||||
});
|
||||
|
||||
const objectMetadataItemsUsedInSelectedIdsQuery = objectMetadataItems.filter(
|
||||
({ nameSingular }) => {
|
||||
return selectedObjectRecordIds.some(({ objectNameSingular }) => {
|
||||
return objectNameSingular === nameSingular;
|
||||
});
|
||||
},
|
||||
const objectMetadataItemsUsedInSelectedIdsQuery = useMemo(
|
||||
() =>
|
||||
objectMetadataItems.filter(({ nameSingular }) => {
|
||||
return selectedObjectRecordIds.some(({ objectNameSingular }) => {
|
||||
return objectNameSingular === nameSingular;
|
||||
});
|
||||
}),
|
||||
[objectMetadataItems, selectedObjectRecordIds],
|
||||
);
|
||||
|
||||
const selectedAndMatchesSearchFilterTextFilterPerMetadataItem =
|
||||
@ -53,38 +63,25 @@ export const useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery = ({
|
||||
|
||||
if (!isNonEmptyArray(selectedIds)) return null;
|
||||
|
||||
const searchFilter =
|
||||
searchFilterPerMetadataItemNameSingular[nameSingular] ?? {};
|
||||
return [
|
||||
`filter${capitalize(nameSingular)}`,
|
||||
{
|
||||
and: [
|
||||
{
|
||||
...searchFilter,
|
||||
},
|
||||
{
|
||||
id: {
|
||||
in: selectedIds,
|
||||
},
|
||||
},
|
||||
],
|
||||
id: {
|
||||
in: selectedIds,
|
||||
},
|
||||
},
|
||||
];
|
||||
})
|
||||
.filter(isDefined),
|
||||
);
|
||||
|
||||
const { orderByFieldPerMetadataItem } = useOrderByFieldPerMetadataItem({
|
||||
objectMetadataItems: objectMetadataItemsUsedInSelectedIdsQuery,
|
||||
});
|
||||
|
||||
const { limitPerMetadataItem } = useLimitPerMetadataItem({
|
||||
objectMetadataItems: objectMetadataItemsUsedInSelectedIdsQuery,
|
||||
limit,
|
||||
});
|
||||
|
||||
const multiSelectQueryForSelectedIds =
|
||||
useGenerateCombinedFindManyRecordsQuery({
|
||||
const multiSelectSearchQueryForSelectedIds =
|
||||
useGenerateCombinedSearchRecordsQuery({
|
||||
operationSignatures: objectMetadataItemsUsedInSelectedIdsQuery.map(
|
||||
(objectMetadataItem) => ({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
@ -97,22 +94,23 @@ export const useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery = ({
|
||||
loading: selectedAndMatchesSearchFilterObjectRecordsLoading,
|
||||
data: selectedAndMatchesSearchFilterObjectRecordsQueryResult,
|
||||
} = useQuery<MultiObjectRecordQueryResult>(
|
||||
multiSelectQueryForSelectedIds ?? EMPTY_QUERY,
|
||||
multiSelectSearchQueryForSelectedIds ?? EMPTY_QUERY,
|
||||
{
|
||||
variables: {
|
||||
search: searchFilterValue,
|
||||
...selectedAndMatchesSearchFilterTextFilterPerMetadataItem,
|
||||
...orderByFieldPerMetadataItem,
|
||||
...limitPerMetadataItem,
|
||||
},
|
||||
skip: !isDefined(multiSelectQueryForSelectedIds),
|
||||
skip: !isDefined(multiSelectSearchQueryForSelectedIds),
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
objectRecordForSelectArray: selectedAndMatchesSearchFilterObjectRecords,
|
||||
} = useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({
|
||||
multiObjectRecordsQueryResult:
|
||||
multiObjectRecordsQueryResult: formatSearchResults(
|
||||
selectedAndMatchesSearchFilterObjectRecordsQueryResult,
|
||||
),
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -4,15 +4,15 @@ 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 { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery';
|
||||
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 { useOrderByFieldPerMetadataItem } from '@/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem';
|
||||
import { useSearchFilterPerMetadataItem } from '@/object-record/relation-picker/hooks/useSearchFilterPerMetadataItem';
|
||||
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';
|
||||
@ -36,13 +36,10 @@ export const useMultiObjectSearchMatchesSearchFilterAndToSelectQuery = ({
|
||||
.filter(({ isSystem, isRemote }) => !isSystem && !isRemote)
|
||||
.filter(({ nameSingular }) => {
|
||||
return !excludedObjects?.includes(nameSingular as CoreObjectNameSingular);
|
||||
});
|
||||
|
||||
const { searchFilterPerMetadataItemNameSingular } =
|
||||
useSearchFilterPerMetadataItem({
|
||||
objectMetadataItems: selectableObjectMetadataItems,
|
||||
searchFilterValue,
|
||||
});
|
||||
})
|
||||
.filter((object) =>
|
||||
isObjectMetadataItemSearchableInCombinedRequest(object),
|
||||
);
|
||||
|
||||
const objectRecordsToSelectAndMatchesSearchFilterTextFilterPerMetadataItem =
|
||||
Object.fromEntries(
|
||||
@ -65,29 +62,19 @@ export const useMultiObjectSearchMatchesSearchFilterAndToSelectQuery = ({
|
||||
? { not: { id: { in: excludedIdsUnion } } }
|
||||
: undefined;
|
||||
|
||||
const searchFilters = [
|
||||
searchFilterPerMetadataItemNameSingular[nameSingular],
|
||||
excludedIdsFilter,
|
||||
];
|
||||
|
||||
return [
|
||||
`filter${capitalize(nameSingular)}`,
|
||||
makeAndFilterVariables(searchFilters),
|
||||
makeAndFilterVariables([excludedIdsFilter]),
|
||||
];
|
||||
})
|
||||
.filter(isDefined),
|
||||
);
|
||||
|
||||
const { orderByFieldPerMetadataItem } = useOrderByFieldPerMetadataItem({
|
||||
objectMetadataItems: selectableObjectMetadataItems,
|
||||
});
|
||||
|
||||
const { limitPerMetadataItem } = useLimitPerMetadataItem({
|
||||
objectMetadataItems: selectableObjectMetadataItems,
|
||||
limit,
|
||||
});
|
||||
|
||||
const multiSelectQuery = useGenerateCombinedFindManyRecordsQuery({
|
||||
const multiSelectQuery = useGenerateCombinedSearchRecordsQuery({
|
||||
operationSignatures: selectableObjectMetadataItems.map(
|
||||
(objectMetadataItem) => ({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
@ -101,8 +88,8 @@ export const useMultiObjectSearchMatchesSearchFilterAndToSelectQuery = ({
|
||||
data: toSelectAndMatchesSearchFilterObjectRecordsQueryResult,
|
||||
} = useQuery<MultiObjectRecordQueryResult>(multiSelectQuery ?? EMPTY_QUERY, {
|
||||
variables: {
|
||||
search: searchFilterValue,
|
||||
...objectRecordsToSelectAndMatchesSearchFilterTextFilterPerMetadataItem,
|
||||
...orderByFieldPerMetadataItem,
|
||||
...limitPerMetadataItem,
|
||||
},
|
||||
skip: !isDefined(multiSelectQuery),
|
||||
@ -111,8 +98,9 @@ export const useMultiObjectSearchMatchesSearchFilterAndToSelectQuery = ({
|
||||
const {
|
||||
objectRecordForSelectArray: toSelectAndMatchesSearchFilterObjectRecords,
|
||||
} = useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({
|
||||
multiObjectRecordsQueryResult:
|
||||
multiObjectRecordsQueryResult: formatSearchResults(
|
||||
toSelectAndMatchesSearchFilterObjectRecordsQueryResult,
|
||||
),
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -1,67 +0,0 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||
import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables';
|
||||
import { FieldMetadataType } from '~/generated/graphql';
|
||||
import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useSearchFilterPerMetadataItem = ({
|
||||
objectMetadataItems,
|
||||
searchFilterValue,
|
||||
}: {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
searchFilterValue: string;
|
||||
}) => {
|
||||
const searchFilterPerMetadataItemNameSingular =
|
||||
Object.fromEntries<RecordGqlOperationFilter>(
|
||||
objectMetadataItems
|
||||
.map((objectMetadataItem) => {
|
||||
if (searchFilterValue === '') return null;
|
||||
|
||||
const labelIdentifierFieldMetadataItem =
|
||||
getLabelIdentifierFieldMetadataItem(objectMetadataItem);
|
||||
|
||||
let searchFilter: RecordGqlOperationFilter = {};
|
||||
|
||||
if (isDefined(labelIdentifierFieldMetadataItem)) {
|
||||
switch (labelIdentifierFieldMetadataItem.type) {
|
||||
case FieldMetadataType.FullName: {
|
||||
if (isNonEmptyString(searchFilterValue)) {
|
||||
const compositeFilter = makeOrFilterVariables(
|
||||
generateILikeFiltersForCompositeFields(
|
||||
searchFilterValue,
|
||||
labelIdentifierFieldMetadataItem.name,
|
||||
['firstName', 'lastName'],
|
||||
),
|
||||
);
|
||||
|
||||
if (isDefined(compositeFilter)) {
|
||||
searchFilter = compositeFilter;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
if (isNonEmptyString(searchFilterValue)) {
|
||||
searchFilter = {
|
||||
[labelIdentifierFieldMetadataItem.name]: {
|
||||
ilike: `%${searchFilterValue}%`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [objectMetadataItem.nameSingular, searchFilter] as const;
|
||||
})
|
||||
.filter(isDefined),
|
||||
);
|
||||
|
||||
return {
|
||||
searchFilterPerMetadataItemNameSingular,
|
||||
};
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
const SEARCHABLE_STANDARD_OBJECTS_NAMES_PLURAL = [
|
||||
'companies',
|
||||
'people',
|
||||
'opportunities',
|
||||
];
|
||||
export const isObjectMetadataItemSearchable = (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
) => {
|
||||
return (
|
||||
objectMetadataItem.isCustom ||
|
||||
SEARCHABLE_STANDARD_OBJECTS_NAMES_PLURAL.includes(
|
||||
objectMetadataItem.namePlural,
|
||||
)
|
||||
);
|
||||
};
|
@ -0,0 +1,17 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
const SEARCHABLE_STANDARD_OBJECTS_IN_COMBINED_REQUEST_NAMES_PLURAL = [
|
||||
'companies',
|
||||
'people',
|
||||
'opportunities',
|
||||
];
|
||||
export const isObjectMetadataItemSearchableInCombinedRequest = (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
) => {
|
||||
return (
|
||||
objectMetadataItem.isCustom ||
|
||||
SEARCHABLE_STANDARD_OBJECTS_IN_COMBINED_REQUEST_NAMES_PLURAL.includes(
|
||||
objectMetadataItem.namePlural,
|
||||
)
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user