add new globalSearch resolver + update useSearchRecords hook (#10457)
# Context To enable search records sorting by ts_rank_cd / ts_rank, we have decided to add a new search resolver serving `GlobalSearchRecordDTO`. ----- - [x] Test to add - work in progress closes https://github.com/twentyhq/core-team-issues/issues/357
This commit is contained in:
@ -1,156 +1,29 @@
|
||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { Company } from '@/companies/types/Company';
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainName';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { useMultiObjectSearch } from '@/object-record/record-picker/hooks/useMultiObjectSearch';
|
||||
import { useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap } from '@/object-record/record-picker/hooks/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap';
|
||||
import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Avatar, IconCheckbox, IconNotes } from 'twenty-ui';
|
||||
import { capitalize } from 'twenty-shared';
|
||||
import { Avatar } from 'twenty-ui';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
import { getLogoUrlFromDomainName } from '~/utils';
|
||||
import { useGlobalSearchQuery } from '~/generated/graphql';
|
||||
|
||||
const MAX_SEARCH_RESULTS_PER_OBJECT = 8;
|
||||
const MAX_SEARCH_RESULTS = 30;
|
||||
|
||||
export const useSearchRecords = () => {
|
||||
const commandMenuSearch = useRecoilValue(commandMenuSearchState);
|
||||
|
||||
const isRichTextV2Enabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsRichTextV2Enabled,
|
||||
);
|
||||
|
||||
const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300);
|
||||
|
||||
const {
|
||||
matchesSearchFilterObjectRecordsQueryResult,
|
||||
matchesSearchFilterObjectRecordsLoading: loading,
|
||||
} = useMultiObjectSearch({
|
||||
excludedObjects: [CoreObjectNameSingular.Task, CoreObjectNameSingular.Note],
|
||||
searchFilterValue: deferredCommandMenuSearch ?? undefined,
|
||||
limit: MAX_SEARCH_RESULTS_PER_OBJECT,
|
||||
const { data: globalSearchData, loading } = useGlobalSearchQuery({
|
||||
variables: {
|
||||
searchInput: deferredCommandMenuSearch ?? '',
|
||||
limit: MAX_SEARCH_RESULTS,
|
||||
excludedObjectNameSingulars: [],
|
||||
},
|
||||
});
|
||||
|
||||
const { objectRecordsMap: matchesSearchFilterObjectRecords } =
|
||||
useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap({
|
||||
multiObjectRecordsQueryResult:
|
||||
matchesSearchFilterObjectRecordsQueryResult,
|
||||
});
|
||||
|
||||
const { loading: isNotesLoading, records: notes } = useFindManyRecords<Note>({
|
||||
objectNameSingular: CoreObjectNameSingular.Note,
|
||||
filter: deferredCommandMenuSearch
|
||||
? makeOrFilterVariables([
|
||||
{ title: { ilike: `%${deferredCommandMenuSearch}%` } },
|
||||
isRichTextV2Enabled
|
||||
? {
|
||||
bodyV2: {
|
||||
markdown: { ilike: `%${deferredCommandMenuSearch}%` },
|
||||
},
|
||||
}
|
||||
: { body: { ilike: `%${deferredCommandMenuSearch}%` } },
|
||||
])
|
||||
: undefined,
|
||||
limit: MAX_SEARCH_RESULTS_PER_OBJECT,
|
||||
});
|
||||
|
||||
const { loading: isTasksLoading, records: tasks } = useFindManyRecords<Task>({
|
||||
objectNameSingular: CoreObjectNameSingular.Task,
|
||||
filter: deferredCommandMenuSearch
|
||||
? makeOrFilterVariables([
|
||||
{ title: { ilike: `%${deferredCommandMenuSearch}%` } },
|
||||
isRichTextV2Enabled
|
||||
? {
|
||||
bodyV2: {
|
||||
markdown: { ilike: `%${deferredCommandMenuSearch}%` },
|
||||
},
|
||||
}
|
||||
: { body: { ilike: `%${deferredCommandMenuSearch}%` } },
|
||||
])
|
||||
: undefined,
|
||||
limit: MAX_SEARCH_RESULTS_PER_OBJECT,
|
||||
});
|
||||
|
||||
const people = matchesSearchFilterObjectRecords.people?.map(
|
||||
(people) => people.record,
|
||||
);
|
||||
const companies = matchesSearchFilterObjectRecords.companies?.map(
|
||||
(companies) => companies.record,
|
||||
);
|
||||
const opportunities = matchesSearchFilterObjectRecords.opportunities?.map(
|
||||
(opportunities) => opportunities.record,
|
||||
);
|
||||
|
||||
const peopleCommands = useMemo(
|
||||
() =>
|
||||
people?.map(({ id, name: { firstName, lastName }, avatarUrl }) => ({
|
||||
id,
|
||||
label: `${firstName} ${lastName}`,
|
||||
description: 'Person',
|
||||
to: `object/person/${id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
type="rounded"
|
||||
avatarUrl={avatarUrl}
|
||||
placeholderColorSeed={id}
|
||||
placeholder={`${firstName} ${lastName}`}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
[people],
|
||||
);
|
||||
|
||||
const companyCommands = useMemo(
|
||||
() =>
|
||||
companies?.map((company) => ({
|
||||
id: company.id,
|
||||
label: company.name ?? '',
|
||||
description: 'Company',
|
||||
to: `object/company/${company.id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
placeholderColorSeed={company.id}
|
||||
placeholder={company.name}
|
||||
avatarUrl={getLogoUrlFromDomainName(
|
||||
getCompanyDomainName(company as Company),
|
||||
)}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
[companies],
|
||||
);
|
||||
|
||||
const opportunityCommands = useMemo(
|
||||
() =>
|
||||
opportunities?.map(({ id, name }) => ({
|
||||
id,
|
||||
label: name ?? '',
|
||||
description: 'Opportunity',
|
||||
to: `object/opportunity/${id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
type="rounded"
|
||||
avatarUrl={null}
|
||||
placeholderColorSeed={id}
|
||||
placeholder={name ?? ''}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
[opportunities],
|
||||
);
|
||||
|
||||
const openNoteRightDrawer = useOpenActivityRightDrawer({
|
||||
objectNameSingular: CoreObjectNameSingular.Note,
|
||||
});
|
||||
@ -159,87 +32,49 @@ export const useSearchRecords = () => {
|
||||
objectNameSingular: CoreObjectNameSingular.Task,
|
||||
});
|
||||
|
||||
const noteCommands = useMemo(
|
||||
() =>
|
||||
notes?.map((note) => ({
|
||||
id: note.id,
|
||||
label: note.title ?? '',
|
||||
description: 'Note',
|
||||
to: '',
|
||||
onCommandClick: () => openNoteRightDrawer(note.id),
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: IconNotes,
|
||||
})),
|
||||
[notes, openNoteRightDrawer],
|
||||
);
|
||||
|
||||
const tasksCommands = useMemo(
|
||||
() =>
|
||||
tasks?.map((task) => ({
|
||||
id: task.id,
|
||||
label: task.title ?? '',
|
||||
description: 'Task',
|
||||
to: '',
|
||||
onCommandClick: () => openTaskRightDrawer(task.id),
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: IconCheckbox,
|
||||
})),
|
||||
[tasks, openTaskRightDrawer],
|
||||
);
|
||||
|
||||
const customObjectRecordsMap = useMemo(() => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(matchesSearchFilterObjectRecords).filter(
|
||||
([namePlural, records]) =>
|
||||
![
|
||||
CoreObjectNamePlural.Person,
|
||||
CoreObjectNamePlural.Opportunity,
|
||||
CoreObjectNamePlural.Company,
|
||||
].includes(namePlural as CoreObjectNamePlural) && !isEmpty(records),
|
||||
),
|
||||
);
|
||||
}, [matchesSearchFilterObjectRecords]);
|
||||
|
||||
const customObjectCommands = useMemo(() => {
|
||||
return Object.values(customObjectRecordsMap).flatMap((objectRecords) =>
|
||||
objectRecords.map((objectRecord) => ({
|
||||
id: objectRecord.record.id,
|
||||
label: objectRecord.recordIdentifier.name,
|
||||
description: objectRecord.objectMetadataItem.labelSingular,
|
||||
to: `object/${objectRecord.objectMetadataItem.nameSingular}/${objectRecord.record.id}`,
|
||||
const commands = useMemo(() => {
|
||||
return (globalSearchData?.globalSearch ?? []).map((searchRecord) => {
|
||||
const command = {
|
||||
id: searchRecord.recordId,
|
||||
label: searchRecord.label,
|
||||
description: capitalize(searchRecord.objectSingularName),
|
||||
to: `object/${searchRecord.objectSingularName}/${searchRecord.recordId}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
type="rounded"
|
||||
avatarUrl={objectRecord.record.avatarUrl}
|
||||
placeholderColorSeed={objectRecord.record.id}
|
||||
placeholder={objectRecord.recordIdentifier.name ?? ''}
|
||||
type={
|
||||
searchRecord.objectSingularName === 'company'
|
||||
? 'squared'
|
||||
: 'rounded'
|
||||
}
|
||||
avatarUrl={searchRecord.imageUrl}
|
||||
placeholderColorSeed={searchRecord.recordId}
|
||||
placeholder={searchRecord.label}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
);
|
||||
}, [customObjectRecordsMap]);
|
||||
|
||||
const commands = [
|
||||
...(peopleCommands ?? []),
|
||||
...(companyCommands ?? []),
|
||||
...(opportunityCommands ?? []),
|
||||
...(noteCommands ?? []),
|
||||
...(tasksCommands ?? []),
|
||||
...(customObjectCommands ?? []),
|
||||
];
|
||||
|
||||
const noResults =
|
||||
!peopleCommands?.length &&
|
||||
!companyCommands?.length &&
|
||||
!opportunityCommands?.length &&
|
||||
!noteCommands?.length &&
|
||||
!tasksCommands?.length &&
|
||||
!customObjectCommands?.length;
|
||||
};
|
||||
if (
|
||||
[CoreObjectNameSingular.Task, CoreObjectNameSingular.Note].includes(
|
||||
searchRecord.objectSingularName as CoreObjectNameSingular,
|
||||
)
|
||||
) {
|
||||
return {
|
||||
...command,
|
||||
to: '',
|
||||
onCommandClick: () => {
|
||||
searchRecord.objectSingularName === 'task'
|
||||
? openTaskRightDrawer(searchRecord.recordId)
|
||||
: openNoteRightDrawer(searchRecord.recordId);
|
||||
},
|
||||
};
|
||||
}
|
||||
return command;
|
||||
});
|
||||
}, [globalSearchData, openTaskRightDrawer, openNoteRightDrawer]);
|
||||
|
||||
return {
|
||||
loading: loading || isNotesLoading || isTasksLoading,
|
||||
noResults,
|
||||
loading,
|
||||
noResults: !commands?.length,
|
||||
commandGroups: [
|
||||
{
|
||||
heading: t`Results`,
|
||||
|
||||
Reference in New Issue
Block a user