Add search records actions to the command menu (#9892)
Closes https://github.com/twentyhq/core-team-issues/issues/253 and https://github.com/twentyhq/core-team-issues/issues/256. - Created `CommandMenuList`, a component used at the root level of the command menu and inside the search page of the command menu - Refactored record agnostic actions - Added shortcuts to the action menu entries (`/` key for the search) and updated the design of the shortcuts - Reordered actions at the root level of the command menu https://github.com/user-attachments/assets/e1339cc4-ef5d-45c5-a159-6817a54b92e9
This commit is contained in:
@ -5,11 +5,11 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
||||
|
||||
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
|
||||
import { useCopyContextStoreStates } from '@/command-menu/hooks/useCopyContextStoreAndActionMenuStates';
|
||||
import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
@ -19,6 +19,7 @@ import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
|
||||
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
|
||||
import { IconSearch } from 'twenty-ui';
|
||||
import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState';
|
||||
|
||||
export const useCommandMenu = () => {
|
||||
@ -71,6 +72,7 @@ export const useCommandMenu = () => {
|
||||
Icon: undefined,
|
||||
});
|
||||
set(isCommandMenuOpenedState, false);
|
||||
set(commandMenuSearchState, '');
|
||||
resetSelectedItem();
|
||||
goBackToPreviousHotkeyScope();
|
||||
|
||||
@ -110,6 +112,20 @@ export const useCommandMenu = () => {
|
||||
[openCommandMenu],
|
||||
);
|
||||
|
||||
const openRecordsSearchPage = useRecoilCallback(
|
||||
({ set }) => {
|
||||
return () => {
|
||||
set(commandMenuPageState, CommandMenuPages.SearchRecords);
|
||||
set(commandMenuPageInfoState, {
|
||||
title: 'Search',
|
||||
Icon: IconSearch,
|
||||
});
|
||||
openCommandMenu();
|
||||
};
|
||||
},
|
||||
[openCommandMenu],
|
||||
);
|
||||
|
||||
const setGlobalCommandMenuContext = useRecoilCallback(
|
||||
({ set }) => {
|
||||
return () => {
|
||||
@ -161,6 +177,7 @@ export const useCommandMenu = () => {
|
||||
return {
|
||||
openCommandMenu,
|
||||
closeCommandMenu,
|
||||
openRecordsSearchPage,
|
||||
openRecordInCommandMenu,
|
||||
toggleCommandMenu,
|
||||
setGlobalCommandMenuContext,
|
||||
|
||||
@ -5,43 +5,25 @@ import {
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { useOpenCopilotRightDrawer } from '@/activities/copilot/right-drawer/hooks/useOpenCopilotRightDrawer';
|
||||
import { copilotQueryState } from '@/activities/copilot/right-drawer/states/copilotQueryState';
|
||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { COMMAND_MENU_NAVIGATE_COMMANDS } from '@/command-menu/constants/CommandMenuNavigateCommands';
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import {
|
||||
Command,
|
||||
CommandScope,
|
||||
CommandType,
|
||||
} from '@/command-menu/types/Command';
|
||||
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/relation-picker/hooks/useMultiObjectSearch';
|
||||
import { useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap } from '@/object-record/relation-picker/hooks/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap';
|
||||
import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { Avatar, IconCheckbox, IconNotes, IconSparkles } from 'twenty-ui';
|
||||
import { IconSparkles } from 'twenty-ui';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
import { getLogoUrlFromDomainName } from '~/utils';
|
||||
|
||||
export const useCommandMenuCommands = () => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentSelector,
|
||||
);
|
||||
const openActivityRightDrawer = useOpenActivityRightDrawer({
|
||||
objectNameSingular: CoreObjectNameSingular.Note,
|
||||
});
|
||||
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
|
||||
|
||||
const commandMenuSearch = useRecoilValue(commandMenuSearchState);
|
||||
const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300); // 200ms - 500ms
|
||||
|
||||
@ -78,6 +60,23 @@ export const useCommandMenuCommands = () => {
|
||||
onCommandClick: actionMenuEntry.onClick,
|
||||
type: CommandType.StandardAction,
|
||||
scope: CommandScope.RecordSelection,
|
||||
hotKeys: actionMenuEntry.hotKeys,
|
||||
}));
|
||||
|
||||
const actionObjectCommands: Command[] = actionMenuEntries
|
||||
?.filter(
|
||||
(actionMenuEntry) =>
|
||||
actionMenuEntry.type === ActionMenuEntryType.Standard &&
|
||||
actionMenuEntry.scope === ActionMenuEntryScope.Object,
|
||||
)
|
||||
?.map((actionMenuEntry) => ({
|
||||
id: actionMenuEntry.key,
|
||||
label: actionMenuEntry.label,
|
||||
Icon: actionMenuEntry.Icon,
|
||||
onCommandClick: actionMenuEntry.onClick,
|
||||
type: CommandType.StandardAction,
|
||||
scope: CommandScope.Object,
|
||||
hotKeys: actionMenuEntry.hotKeys,
|
||||
}));
|
||||
|
||||
const actionGlobalCommands: Command[] = actionMenuEntries
|
||||
@ -93,6 +92,7 @@ export const useCommandMenuCommands = () => {
|
||||
onCommandClick: actionMenuEntry.onClick,
|
||||
type: CommandType.StandardAction,
|
||||
scope: CommandScope.Global,
|
||||
hotKeys: actionMenuEntry.hotKeys,
|
||||
}));
|
||||
|
||||
const workflowRunRecordSelectionCommands: Command[] = actionMenuEntries
|
||||
@ -108,6 +108,7 @@ export const useCommandMenuCommands = () => {
|
||||
onCommandClick: actionMenuEntry.onClick,
|
||||
type: CommandType.WorkflowRun,
|
||||
scope: CommandScope.RecordSelection,
|
||||
hotKeys: actionMenuEntry.hotKeys,
|
||||
}));
|
||||
|
||||
const workflowRunGlobalCommands: Command[] = actionMenuEntries
|
||||
@ -123,193 +124,16 @@ export const useCommandMenuCommands = () => {
|
||||
onCommandClick: actionMenuEntry.onClick,
|
||||
type: CommandType.WorkflowRun,
|
||||
scope: CommandScope.Global,
|
||||
hotKeys: actionMenuEntry.hotKeys,
|
||||
}));
|
||||
|
||||
const {
|
||||
matchesSearchFilterObjectRecordsQueryResult,
|
||||
matchesSearchFilterObjectRecordsLoading: loading,
|
||||
} = useMultiObjectSearch({
|
||||
excludedObjects: [CoreObjectNameSingular.Task, CoreObjectNameSingular.Note],
|
||||
searchFilterValue: deferredCommandMenuSearch ?? undefined,
|
||||
limit: 3,
|
||||
});
|
||||
|
||||
const { objectRecordsMap: matchesSearchFilterObjectRecords } =
|
||||
useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap({
|
||||
multiObjectRecordsQueryResult:
|
||||
matchesSearchFilterObjectRecordsQueryResult,
|
||||
});
|
||||
|
||||
const { loading: isNotesLoading, records: notes } = useFindManyRecords<Note>({
|
||||
skip: !isCommandMenuOpened,
|
||||
objectNameSingular: CoreObjectNameSingular.Note,
|
||||
filter: deferredCommandMenuSearch
|
||||
? makeOrFilterVariables([
|
||||
{ title: { ilike: `%${deferredCommandMenuSearch}%` } },
|
||||
{ body: { ilike: `%${deferredCommandMenuSearch}%` } },
|
||||
])
|
||||
: undefined,
|
||||
limit: 3,
|
||||
});
|
||||
|
||||
const { loading: isTasksLoading, records: tasks } = useFindManyRecords<Task>({
|
||||
skip: !isCommandMenuOpened,
|
||||
objectNameSingular: CoreObjectNameSingular.Task,
|
||||
filter: deferredCommandMenuSearch
|
||||
? makeOrFilterVariables([
|
||||
{ title: { ilike: `%${deferredCommandMenuSearch}%` } },
|
||||
{ body: { ilike: `%${deferredCommandMenuSearch}%` } },
|
||||
])
|
||||
: undefined,
|
||||
limit: 3,
|
||||
});
|
||||
|
||||
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}`,
|
||||
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 ?? '',
|
||||
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 ?? '',
|
||||
to: `object/opportunity/${id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
type="rounded"
|
||||
avatarUrl={null}
|
||||
placeholderColorSeed={id}
|
||||
placeholder={name ?? ''}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
[opportunities],
|
||||
);
|
||||
|
||||
const noteCommands = useMemo(
|
||||
() =>
|
||||
notes?.map((note) => ({
|
||||
id: note.id,
|
||||
label: note.title ?? '',
|
||||
to: '',
|
||||
onCommandClick: () => openActivityRightDrawer(note.id),
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: IconNotes,
|
||||
})),
|
||||
[notes, openActivityRightDrawer],
|
||||
);
|
||||
|
||||
const tasksCommands = useMemo(
|
||||
() =>
|
||||
tasks?.map((task) => ({
|
||||
id: task.id,
|
||||
label: task.title ?? '',
|
||||
to: '',
|
||||
onCommandClick: () => openActivityRightDrawer(task.id),
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: IconCheckbox,
|
||||
})),
|
||||
[tasks, openActivityRightDrawer],
|
||||
);
|
||||
|
||||
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(() => {
|
||||
const customObjectCommandsArray: Command[] = [];
|
||||
Object.values(customObjectRecordsMap).forEach((objectRecords) => {
|
||||
customObjectCommandsArray.push(
|
||||
...objectRecords.map((objectRecord) => ({
|
||||
id: objectRecord.record.id,
|
||||
label: objectRecord.recordIdentifier.name,
|
||||
to: `object/${objectRecord.objectMetadataItem.nameSingular}/${objectRecord.record.id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
type="rounded"
|
||||
avatarUrl={objectRecord.record.avatarUrl}
|
||||
placeholderColorSeed={objectRecord.record.id}
|
||||
placeholder={objectRecord.recordIdentifier.name ?? ''}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
return customObjectCommandsArray;
|
||||
}, [customObjectRecordsMap]);
|
||||
|
||||
const isLoading = loading || isNotesLoading || isTasksLoading;
|
||||
|
||||
return {
|
||||
copilotCommands,
|
||||
navigateCommands,
|
||||
actionRecordSelectionCommands,
|
||||
actionGlobalCommands,
|
||||
actionObjectCommands,
|
||||
workflowRunRecordSelectionCommands,
|
||||
workflowRunGlobalCommands,
|
||||
peopleCommands,
|
||||
companyCommands,
|
||||
opportunityCommands,
|
||||
noteCommands,
|
||||
tasksCommands,
|
||||
customObjectCommands,
|
||||
isLoading,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
@ -12,8 +12,12 @@ import { useRecoilValue } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
export const useCommandMenuHotKeys = () => {
|
||||
const { closeCommandMenu, toggleCommandMenu, setGlobalCommandMenuContext } =
|
||||
useCommandMenu();
|
||||
const {
|
||||
closeCommandMenu,
|
||||
openRecordsSearchPage,
|
||||
toggleCommandMenu,
|
||||
setGlobalCommandMenuContext,
|
||||
} = useCommandMenu();
|
||||
|
||||
const commandMenuSearch = useRecoilValue(commandMenuSearchState);
|
||||
|
||||
@ -36,6 +40,18 @@ export const useCommandMenuHotKeys = () => {
|
||||
[toggleCommandMenu],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
['/'],
|
||||
() => {
|
||||
openRecordsSearchPage();
|
||||
},
|
||||
AppHotkeyScope.KeyboardShortcutMenu,
|
||||
[openRecordsSearchPage],
|
||||
{
|
||||
ignoreModifiers: true,
|
||||
},
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Escape],
|
||||
() => {
|
||||
|
||||
@ -10,9 +10,10 @@ export const useMatchCommands = ({
|
||||
const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300); // 200ms - 500ms
|
||||
|
||||
const checkInShortcuts = (cmd: Command, search: string) => {
|
||||
return (cmd.firstHotKey + (cmd.secondHotKey ?? ''))
|
||||
const concatenatedString = cmd.hotKeys?.join('') ?? '';
|
||||
return concatenatedString
|
||||
.toLowerCase()
|
||||
.includes(search.toLowerCase());
|
||||
.includes(search.toLowerCase().trim());
|
||||
};
|
||||
|
||||
const checkInLabels = (cmd: Command, search: string) => {
|
||||
|
||||
@ -12,24 +12,21 @@ export const useMatchingCommandMenuCommands = ({
|
||||
copilotCommands,
|
||||
navigateCommands,
|
||||
actionRecordSelectionCommands,
|
||||
actionObjectCommands,
|
||||
actionGlobalCommands,
|
||||
workflowRunRecordSelectionCommands,
|
||||
workflowRunGlobalCommands,
|
||||
peopleCommands,
|
||||
companyCommands,
|
||||
opportunityCommands,
|
||||
noteCommands,
|
||||
tasksCommands,
|
||||
customObjectCommands,
|
||||
isLoading,
|
||||
} = useCommandMenuCommands();
|
||||
|
||||
const matchingNavigateCommand = matchCommands(navigateCommands);
|
||||
const matchingNavigateCommands = matchCommands(navigateCommands);
|
||||
|
||||
const matchingStandardActionRecordSelectionCommands = matchCommands(
|
||||
actionRecordSelectionCommands,
|
||||
);
|
||||
|
||||
const matchingStandardActionObjectCommands =
|
||||
matchCommands(actionObjectCommands);
|
||||
|
||||
const matchingStandardActionGlobalCommands =
|
||||
matchCommands(actionGlobalCommands);
|
||||
|
||||
@ -41,33 +38,22 @@ export const useMatchingCommandMenuCommands = ({
|
||||
workflowRunGlobalCommands,
|
||||
);
|
||||
|
||||
const isNoResults =
|
||||
const noResults =
|
||||
!matchingStandardActionRecordSelectionCommands.length &&
|
||||
!matchingWorkflowRunRecordSelectionCommands.length &&
|
||||
!matchingStandardActionGlobalCommands.length &&
|
||||
!matchingWorkflowRunGlobalCommands.length &&
|
||||
!matchingNavigateCommand.length &&
|
||||
!peopleCommands?.length &&
|
||||
!companyCommands?.length &&
|
||||
!opportunityCommands?.length &&
|
||||
!noteCommands?.length &&
|
||||
!tasksCommands?.length &&
|
||||
!customObjectCommands?.length;
|
||||
!matchingStandardActionObjectCommands.length &&
|
||||
!matchingNavigateCommands.length;
|
||||
|
||||
return {
|
||||
isNoResults,
|
||||
isLoading,
|
||||
noResults,
|
||||
copilotCommands,
|
||||
matchingStandardActionRecordSelectionCommands,
|
||||
matchingStandardActionObjectCommands,
|
||||
matchingWorkflowRunRecordSelectionCommands,
|
||||
matchingStandardActionGlobalCommands,
|
||||
matchingWorkflowRunGlobalCommands,
|
||||
matchingNavigateCommand,
|
||||
peopleCommands,
|
||||
companyCommands,
|
||||
opportunityCommands,
|
||||
noteCommands,
|
||||
tasksCommands,
|
||||
customObjectCommands,
|
||||
matchingNavigateCommands,
|
||||
};
|
||||
};
|
||||
|
||||
@ -0,0 +1,229 @@
|
||||
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/relation-picker/hooks/useMultiObjectSearch';
|
||||
import { useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap } from '@/object-record/relation-picker/hooks/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap';
|
||||
import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables';
|
||||
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 { useDebounce } from 'use-debounce';
|
||||
import { getLogoUrlFromDomainName } from '~/utils';
|
||||
|
||||
const MAX_SEARCH_RESULTS_PER_OBJECT = 8;
|
||||
|
||||
export const useSearchRecords = () => {
|
||||
const commandMenuSearch = useRecoilValue(commandMenuSearchState);
|
||||
|
||||
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 { objectRecordsMap: matchesSearchFilterObjectRecords } =
|
||||
useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap({
|
||||
multiObjectRecordsQueryResult:
|
||||
matchesSearchFilterObjectRecordsQueryResult,
|
||||
});
|
||||
|
||||
const { loading: isNotesLoading, records: notes } = useFindManyRecords<Note>({
|
||||
objectNameSingular: CoreObjectNameSingular.Note,
|
||||
filter: deferredCommandMenuSearch
|
||||
? makeOrFilterVariables([
|
||||
{ title: { 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}%` } },
|
||||
{ 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}`,
|
||||
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 ?? '',
|
||||
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 ?? '',
|
||||
to: `object/opportunity/${id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
type="rounded"
|
||||
avatarUrl={null}
|
||||
placeholderColorSeed={id}
|
||||
placeholder={name ?? ''}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
[opportunities],
|
||||
);
|
||||
|
||||
const openNoteRightDrawer = useOpenActivityRightDrawer({
|
||||
objectNameSingular: CoreObjectNameSingular.Note,
|
||||
});
|
||||
|
||||
const openTaskRightDrawer = useOpenActivityRightDrawer({
|
||||
objectNameSingular: CoreObjectNameSingular.Task,
|
||||
});
|
||||
|
||||
const noteCommands = useMemo(
|
||||
() =>
|
||||
notes?.map((note) => ({
|
||||
id: note.id,
|
||||
label: note.title ?? '',
|
||||
to: '',
|
||||
onCommandClick: () => openNoteRightDrawer(note.id),
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: IconNotes,
|
||||
})),
|
||||
[notes, openNoteRightDrawer],
|
||||
);
|
||||
|
||||
const tasksCommands = useMemo(
|
||||
() =>
|
||||
tasks?.map((task) => ({
|
||||
id: task.id,
|
||||
label: task.title ?? '',
|
||||
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,
|
||||
to: `object/${objectRecord.objectMetadataItem.nameSingular}/${objectRecord.record.id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
type="rounded"
|
||||
avatarUrl={objectRecord.record.avatarUrl}
|
||||
placeholderColorSeed={objectRecord.record.id}
|
||||
placeholder={objectRecord.recordIdentifier.name ?? ''}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
);
|
||||
}, [customObjectRecordsMap]);
|
||||
|
||||
const commands = [
|
||||
...(peopleCommands ?? []),
|
||||
...(companyCommands ?? []),
|
||||
...(opportunityCommands ?? []),
|
||||
...(noteCommands ?? []),
|
||||
...(tasksCommands ?? []),
|
||||
...(customObjectCommands ?? []),
|
||||
];
|
||||
|
||||
const noResults =
|
||||
!peopleCommands?.length &&
|
||||
!companyCommands?.length &&
|
||||
!opportunityCommands?.length &&
|
||||
!noteCommands?.length &&
|
||||
!tasksCommands?.length &&
|
||||
!customObjectCommands?.length;
|
||||
|
||||
return {
|
||||
loading: loading || isNotesLoading || isTasksLoading,
|
||||
noResults,
|
||||
commandGroups: [
|
||||
{
|
||||
heading: t`Results`,
|
||||
items: commands,
|
||||
},
|
||||
],
|
||||
hasMore: false,
|
||||
pageSize: 0,
|
||||
onLoadMore: () => {},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user