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) => (
|
return nonSystemActiveObjectMetadataItems.map((objectMetadataItem) => (
|
||||||
<GoToHotkeyItemEffect
|
<GoToHotkeyItemEffect
|
||||||
|
key={`go-to-hokey-item-${objectMetadataItem.id}`}
|
||||||
hotkey={objectMetadataItem.namePlural[0]}
|
hotkey={objectMetadataItem.namePlural[0]}
|
||||||
pathToNavigateTo={`/objects/${objectMetadataItem.namePlural}`}
|
pathToNavigateTo={`/objects/${objectMetadataItem.namePlural}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -85,6 +85,7 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
|
|||||||
objectMetadataItemsForNavigationItems.map(
|
objectMetadataItemsForNavigationItems.map(
|
||||||
(objectMetadataItem) => (
|
(objectMetadataItem) => (
|
||||||
<NavigationDrawerItemForObjectMetadataItem
|
<NavigationDrawerItemForObjectMetadataItem
|
||||||
|
key={`navigation-drawer-item-${objectMetadataItem.id}`}
|
||||||
objectMetadataItem={objectMetadataItem}
|
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 { useQuery, WatchQueryFetchPolicy } from '@apollo/client';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { logError } from '~/utils/logError';
|
import { logError } from '~/utils/logError';
|
||||||
|
|
||||||
export type UseSearchRecordsParams = ObjectMetadataItemIdentifier &
|
export type UseSearchRecordsParams = ObjectMetadataItemIdentifier &
|
||||||
RecordGqlOperationVariables & {
|
Pick<RecordGqlOperationVariables, 'filter' | 'limit'> & {
|
||||||
onError?: (error?: Error) => void;
|
onError?: (error?: Error) => void;
|
||||||
skip?: boolean;
|
skip?: boolean;
|
||||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||||
@ -29,6 +30,7 @@ export const useSearchRecords = <T extends ObjectRecord = ObjectRecord>({
|
|||||||
searchInput,
|
searchInput,
|
||||||
limit,
|
limit,
|
||||||
skip,
|
skip,
|
||||||
|
filter,
|
||||||
recordGqlFields,
|
recordGqlFields,
|
||||||
fetchPolicy,
|
fetchPolicy,
|
||||||
}: UseSearchRecordsParams) => {
|
}: UseSearchRecordsParams) => {
|
||||||
@ -45,10 +47,14 @@ export const useSearchRecords = <T extends ObjectRecord = ObjectRecord>({
|
|||||||
const { data, loading, error, previousData } =
|
const { data, loading, error, previousData } =
|
||||||
useQuery<RecordGqlOperationSearchResult>(searchRecordsQuery, {
|
useQuery<RecordGqlOperationSearchResult>(searchRecordsQuery, {
|
||||||
skip:
|
skip:
|
||||||
skip || !objectMetadataItem || !currentWorkspaceMember || !searchInput,
|
skip ||
|
||||||
|
!objectMetadataItem ||
|
||||||
|
!currentWorkspaceMember ||
|
||||||
|
!isDefined(searchInput),
|
||||||
variables: {
|
variables: {
|
||||||
search: searchInput,
|
search: searchInput,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
|
filter: filter,
|
||||||
},
|
},
|
||||||
fetchPolicy: fetchPolicy,
|
fetchPolicy: fetchPolicy,
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
|
||||||
import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect';
|
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||||
import { RelationFromManyFieldInputMultiRecordsEffect } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect';
|
import { RelationFromManyFieldInputMultiRecordsEffect } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect';
|
||||||
@ -54,7 +53,6 @@ export const RelationFromManyFieldInput = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RelationPickerScope relationPickerScopeId={relationPickerScopeId}>
|
<RelationPickerScope relationPickerScopeId={relationPickerScopeId}>
|
||||||
<ObjectMetadataItemsRelationPickerEffect />
|
|
||||||
<RelationFromManyFieldInputMultiRecordsEffect />
|
<RelationFromManyFieldInputMultiRecordsEffect />
|
||||||
<MultiRecordSelect
|
<MultiRecordSelect
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { useCallback, useContext } from 'react';
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { IconForbid, IconPencil, IconPlus } from 'twenty-ui';
|
import { IconForbid, IconPencil, IconPlus } from 'twenty-ui';
|
||||||
|
|
||||||
import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect';
|
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||||
@ -209,7 +208,6 @@ export const RecordDetailRelationSection = ({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ObjectMetadataItemsRelationPickerEffect />
|
|
||||||
<RelationFromManyFieldInputMultiRecordsEffect />
|
<RelationFromManyFieldInputMultiRecordsEffect />
|
||||||
<MultiRecordSelect
|
<MultiRecordSelect
|
||||||
onCreate={createNewRecordAndOpenRightDrawer}
|
onCreate={createNewRecordAndOpenRightDrawer}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect';
|
|
||||||
import {
|
import {
|
||||||
SingleEntitySelectMenuItems,
|
SingleEntitySelectMenuItems,
|
||||||
SingleEntitySelectMenuItemsProps,
|
SingleEntitySelectMenuItemsProps,
|
||||||
@ -65,9 +64,6 @@ export const SingleEntitySelectMenuItemsWithSearch = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ObjectMetadataItemsRelationPickerEffect
|
|
||||||
relationPickerScopeId={relationPickerScopeId}
|
|
||||||
/>
|
|
||||||
<DropdownMenuSearchInput onChange={handleSearchFilterChange} autoFocus />
|
<DropdownMenuSearchInput onChange={handleSearchFilterChange} autoFocus />
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<SingleEntitySelectMenuItems
|
<SingleEntitySelectMenuItems
|
||||||
|
|||||||
@ -18,24 +18,15 @@ export const useRelationPickerEntitiesOptions = ({
|
|||||||
RelationPickerScopeInternalContext,
|
RelationPickerScopeInternalContext,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { searchQueryState, relationPickerSearchFilterState } =
|
const { relationPickerSearchFilterState } = useRelationPickerScopedStates({
|
||||||
useRelationPickerScopedStates({
|
relationPickerScopedId: scopeId,
|
||||||
relationPickerScopedId: scopeId,
|
});
|
||||||
});
|
|
||||||
const relationPickerSearchFilter = useRecoilValue(
|
const relationPickerSearchFilter = useRecoilValue(
|
||||||
relationPickerSearchFilterState,
|
relationPickerSearchFilterState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const searchQuery = useRecoilValue(searchQueryState);
|
|
||||||
const entities = useFilteredSearchEntityQuery({
|
const entities = useFilteredSearchEntityQuery({
|
||||||
filters: [
|
searchFilter: relationPickerSearchFilter,
|
||||||
{
|
|
||||||
fieldNames:
|
|
||||||
searchQuery?.computeFilterFields?.(relationObjectNameSingular) ?? [],
|
|
||||||
filter: relationPickerSearchFilter,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
orderByField: 'createdAt',
|
|
||||||
selectedIds: selectedRelationRecordIds,
|
selectedIds: selectedRelationRecordIds,
|
||||||
excludeRecordIds: excludedRelationRecordIds,
|
excludeRecordIds: excludedRelationRecordIds,
|
||||||
objectNameSingular: relationObjectNameSingular,
|
objectNameSingular: relationObjectNameSingular,
|
||||||
|
|||||||
@ -15,12 +15,14 @@ export const generateSearchRecordsQuery = ({
|
|||||||
computeReferences,
|
computeReferences,
|
||||||
}: {
|
}: {
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
objectMetadataItems: ObjectMetadataItem[]; // TODO - what is this used for?
|
objectMetadataItems: ObjectMetadataItem[];
|
||||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||||
computeReferences?: boolean;
|
computeReferences?: boolean;
|
||||||
}) => gql`
|
}) => gql`
|
||||||
query Search${capitalize(objectMetadataItem.namePlural)}($search: String, $limit: Int) {
|
query Search${capitalize(objectMetadataItem.namePlural)}($search: String, $limit: Int, $filter: ${capitalize(
|
||||||
${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(searchInput: $search, limit: $limit){
|
objectMetadataItem.nameSingular,
|
||||||
|
)}FilterInput) {
|
||||||
|
${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(searchInput: $search, limit: $limit, filter: $filter){
|
||||||
edges {
|
edges {
|
||||||
node ${mapObjectMetadataToGraphQLQuery({
|
node ${mapObjectMetadataToGraphQLQuery({
|
||||||
objectMetadataItems,
|
objectMetadataItems,
|
||||||
|
|||||||
@ -80,13 +80,11 @@ describe('useFilteredSearchEntityQuery', () => {
|
|||||||
setMetadataItems(generatedMockObjectMetadataItems);
|
setMetadataItems(generatedMockObjectMetadataItems);
|
||||||
|
|
||||||
return useFilteredSearchEntityQuery({
|
return useFilteredSearchEntityQuery({
|
||||||
orderByField: 'name',
|
|
||||||
filters: [{ fieldNames: ['name'], filter: 'Entity' }],
|
|
||||||
sortOrder: 'AscNullsLast',
|
|
||||||
selectedIds: ['1'],
|
selectedIds: ['1'],
|
||||||
limit: 10,
|
limit: 10,
|
||||||
excludeRecordIds: ['2'],
|
excludeRecordIds: ['2'],
|
||||||
objectNameSingular: 'person',
|
objectNameSingular: 'person',
|
||||||
|
searchFilter: 'Entity',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ wrapper: Wrapper },
|
{ wrapper: Wrapper },
|
||||||
|
|||||||
@ -1,39 +1,26 @@
|
|||||||
import { isNonEmptyString } from '@sniptt/guards';
|
|
||||||
|
|
||||||
import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier';
|
import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier';
|
||||||
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit';
|
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit';
|
||||||
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
import { useSearchRecords } from '@/object-record/hooks/useSearchRecords';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
|
||||||
import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect';
|
import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect';
|
||||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
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';
|
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
|
// TODO: use this for all search queries, because we need selectedEntities and entitiesToSelect each time we want to search
|
||||||
// Filtered entities to select are
|
// Filtered entities to select are
|
||||||
|
|
||||||
export const useFilteredSearchEntityQuery = ({
|
export const useFilteredSearchEntityQuery = ({
|
||||||
orderByField,
|
|
||||||
filters,
|
|
||||||
sortOrder = 'AscNullsLast',
|
|
||||||
selectedIds,
|
selectedIds,
|
||||||
limit,
|
limit,
|
||||||
excludeRecordIds = [],
|
excludeRecordIds = [],
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
|
searchFilter,
|
||||||
}: {
|
}: {
|
||||||
orderByField: string;
|
|
||||||
filters: SearchFilter[];
|
|
||||||
sortOrder?: OrderBy;
|
|
||||||
selectedIds: string[];
|
selectedIds: string[];
|
||||||
limit?: number;
|
limit?: number;
|
||||||
excludeRecordIds?: string[];
|
excludeRecordIds?: string[];
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
|
searchFilter?: string;
|
||||||
}): EntitiesForMultipleEntitySelect<EntityForSelect> => {
|
}): EntitiesForMultipleEntitySelect<EntityForSelect> => {
|
||||||
const { mapToObjectRecordIdentifier } = useMapToObjectRecordIdentifier({
|
const { mapToObjectRecordIdentifier } = useMapToObjectRecordIdentifier({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
@ -46,55 +33,21 @@ export const useFilteredSearchEntityQuery = ({
|
|||||||
const selectedIdsFilter = { id: { in: selectedIds } };
|
const selectedIdsFilter = { id: { in: selectedIds } };
|
||||||
|
|
||||||
const { loading: selectedRecordsLoading, records: selectedRecords } =
|
const { loading: selectedRecordsLoading, records: selectedRecords } =
|
||||||
useFindManyRecords({
|
useSearchRecords({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
filter: selectedIdsFilter,
|
filter: selectedIdsFilter,
|
||||||
orderBy: [{ [orderByField]: sortOrder }],
|
|
||||||
skip: !selectedIds.length,
|
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 {
|
const {
|
||||||
loading: filteredSelectedRecordsLoading,
|
loading: filteredSelectedRecordsLoading,
|
||||||
records: filteredSelectedRecords,
|
records: filteredSelectedRecords,
|
||||||
} = useFindManyRecords({
|
} = useSearchRecords({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
filter: makeAndFilterVariables([...searchFilters, selectedIdsFilter]),
|
filter: selectedIdsFilter,
|
||||||
orderBy: [{ [orderByField]: sortOrder }],
|
|
||||||
skip: !selectedIds.length,
|
skip: !selectedIds.length,
|
||||||
|
searchInput: searchFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
const notFilterIds = [...selectedIds, ...excludeRecordIds];
|
const notFilterIds = [...selectedIds, ...excludeRecordIds];
|
||||||
@ -102,11 +55,11 @@ export const useFilteredSearchEntityQuery = ({
|
|||||||
? { not: { id: { in: notFilterIds } } }
|
? { not: { id: { in: notFilterIds } } }
|
||||||
: undefined;
|
: undefined;
|
||||||
const { loading: recordsToSelectLoading, records: recordsToSelect } =
|
const { loading: recordsToSelectLoading, records: recordsToSelect } =
|
||||||
useFindManyRecords({
|
useSearchRecords({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
filter: makeAndFilterVariables([...searchFilters, notFilter]),
|
filter: notFilter,
|
||||||
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
|
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
|
||||||
orderBy: [{ [orderByField]: sortOrder }],
|
searchInput: searchFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -113,6 +113,52 @@ export const graphqlMocks = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
graphql.query('SearchWorkspaceMembers', () => {
|
||||||
|
return HttpResponse.json({
|
||||||
|
data: {
|
||||||
|
searchWorkspaceMembers: {
|
||||||
|
edges: mockWorkspaceMembers.map((member) => ({
|
||||||
|
node: {
|
||||||
|
...member,
|
||||||
|
messageParticipants: {
|
||||||
|
edges: [],
|
||||||
|
__typename: 'MessageParticipantConnection',
|
||||||
|
},
|
||||||
|
authoredAttachments: {
|
||||||
|
edges: [],
|
||||||
|
__typename: 'AttachmentConnection',
|
||||||
|
},
|
||||||
|
authoredComments: {
|
||||||
|
edges: [],
|
||||||
|
__typename: 'CommentConnection',
|
||||||
|
},
|
||||||
|
accountOwnerForCompanies: {
|
||||||
|
edges: [],
|
||||||
|
__typename: 'CompanyConnection',
|
||||||
|
},
|
||||||
|
authoredActivities: {
|
||||||
|
edges: [],
|
||||||
|
__typename: 'ActivityConnection',
|
||||||
|
},
|
||||||
|
favorites: {
|
||||||
|
edges: [],
|
||||||
|
__typename: 'FavoriteConnection',
|
||||||
|
},
|
||||||
|
connectedAccounts: {
|
||||||
|
edges: [],
|
||||||
|
__typename: 'ConnectedAccountConnection',
|
||||||
|
},
|
||||||
|
assignedActivities: {
|
||||||
|
edges: [],
|
||||||
|
__typename: 'ActivityConnection',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cursor: null,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
graphql.query('FindManyViewFields', ({ variables }) => {
|
graphql.query('FindManyViewFields', ({ variables }) => {
|
||||||
const viewId = variables.filter.view.eq;
|
const viewId = variables.filter.view.eq;
|
||||||
|
|
||||||
|
|||||||
@ -4,16 +4,19 @@ import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/int
|
|||||||
import {
|
import {
|
||||||
Record as IRecord,
|
Record as IRecord,
|
||||||
OrderByDirection,
|
OrderByDirection,
|
||||||
|
RecordFilter,
|
||||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||||
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
|
import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface';
|
||||||
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
|
||||||
import { SearchResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
import { SearchResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||||
|
|
||||||
import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant';
|
import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant';
|
||||||
|
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
||||||
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper';
|
||||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||||
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants';
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
|
import { isDefined } from 'src/utils/is-defined';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GraphqlQuerySearchResolverService
|
export class GraphqlQuerySearchResolverService
|
||||||
@ -24,11 +27,19 @@ export class GraphqlQuerySearchResolverService
|
|||||||
private readonly featureFlagService: FeatureFlagService,
|
private readonly featureFlagService: FeatureFlagService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async resolve<ObjectRecord extends IRecord = IRecord>(
|
async resolve<
|
||||||
|
ObjectRecord extends IRecord = IRecord,
|
||||||
|
Filter extends RecordFilter = RecordFilter,
|
||||||
|
>(
|
||||||
args: SearchResolverArgs,
|
args: SearchResolverArgs,
|
||||||
options: WorkspaceQueryRunnerOptions,
|
options: WorkspaceQueryRunnerOptions,
|
||||||
): Promise<IConnection<ObjectRecord>> {
|
): Promise<IConnection<ObjectRecord>> {
|
||||||
const { authContext, objectMetadataItem, objectMetadataMap } = options;
|
const {
|
||||||
|
authContext,
|
||||||
|
objectMetadataItem,
|
||||||
|
objectMetadataMapItem,
|
||||||
|
objectMetadataMap,
|
||||||
|
} = options;
|
||||||
|
|
||||||
const repository =
|
const repository =
|
||||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||||
@ -39,7 +50,7 @@ export class GraphqlQuerySearchResolverService
|
|||||||
const typeORMObjectRecordsParser =
|
const typeORMObjectRecordsParser =
|
||||||
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
|
new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap);
|
||||||
|
|
||||||
if (!args.searchInput) {
|
if (!isDefined(args.searchInput)) {
|
||||||
return typeORMObjectRecordsParser.createConnection({
|
return typeORMObjectRecordsParser.createConnection({
|
||||||
objectRecords: [],
|
objectRecords: [],
|
||||||
objectName: objectMetadataItem.nameSingular,
|
objectName: objectMetadataItem.nameSingular,
|
||||||
@ -54,11 +65,27 @@ export class GraphqlQuerySearchResolverService
|
|||||||
|
|
||||||
const limit = args?.limit ?? QUERY_MAX_RECORDS;
|
const limit = args?.limit ?? QUERY_MAX_RECORDS;
|
||||||
|
|
||||||
const resultsWithTsVector = (await repository
|
const queryBuilder = repository.createQueryBuilder(
|
||||||
.createQueryBuilder()
|
objectMetadataItem.nameSingular,
|
||||||
.where(`"${SEARCH_VECTOR_FIELD.name}" @@ to_tsquery(:searchTerms)`, {
|
);
|
||||||
searchTerms,
|
const graphqlQueryParser = new GraphqlQueryParser(
|
||||||
})
|
objectMetadataMapItem.fields,
|
||||||
|
objectMetadataMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
const queryBuilderWithFilter = graphqlQueryParser.applyFilterToBuilder(
|
||||||
|
queryBuilder,
|
||||||
|
objectMetadataMapItem.nameSingular,
|
||||||
|
args.filter ?? ({} as Filter),
|
||||||
|
);
|
||||||
|
|
||||||
|
const resultsWithTsVector = (await queryBuilderWithFilter
|
||||||
|
.andWhere(
|
||||||
|
searchTerms === ''
|
||||||
|
? `"${SEARCH_VECTOR_FIELD.name}" IS NOT NULL`
|
||||||
|
: `"${SEARCH_VECTOR_FIELD.name}" @@ to_tsquery(:searchTerms)`,
|
||||||
|
searchTerms === '' ? {} : { searchTerms },
|
||||||
|
)
|
||||||
.orderBy(
|
.orderBy(
|
||||||
`ts_rank("${SEARCH_VECTOR_FIELD.name}", to_tsquery(:searchTerms))`,
|
`ts_rank("${SEARCH_VECTOR_FIELD.name}", to_tsquery(:searchTerms))`,
|
||||||
'DESC',
|
'DESC',
|
||||||
@ -84,6 +111,9 @@ export class GraphqlQuerySearchResolverService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private formatSearchTerms(searchTerm: string) {
|
private formatSearchTerms(searchTerm: string) {
|
||||||
|
if (searchTerm === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
const words = searchTerm.trim().split(/\s+/);
|
const words = searchTerm.trim().split(/\s+/);
|
||||||
const formattedWords = words.map((word) => {
|
const formattedWords = words.map((word) => {
|
||||||
const escapedWord = word.replace(/[\\:'&|!()]/g, '\\$&');
|
const escapedWord = word.replace(/[\\:'&|!()]/g, '\\$&');
|
||||||
|
|||||||
@ -48,8 +48,11 @@ export interface FindDuplicatesResolverArgs<
|
|||||||
data?: Data[];
|
data?: Data[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchResolverArgs {
|
export interface SearchResolverArgs<
|
||||||
|
Filter extends RecordFilter = RecordFilter,
|
||||||
|
> {
|
||||||
searchInput?: string;
|
searchInput?: string;
|
||||||
|
filter?: Filter;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -147,6 +147,10 @@ export const getResolverArgs = (
|
|||||||
type: GraphQLInt,
|
type: GraphQLInt,
|
||||||
isNullable: true,
|
isNullable: true,
|
||||||
},
|
},
|
||||||
|
filter: {
|
||||||
|
kind: InputTypeDefinitionKind.Filter,
|
||||||
|
isNullable: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown resolver type: ${type}`);
|
throw new Error(`Unknown resolver type: ${type}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user