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:
Raphaël Bosi
2025-01-29 18:23:40 +01:00
committed by GitHub
parent 03f3ccd060
commit ce296fae4f
52 changed files with 1539 additions and 1361 deletions

View File

@ -8,7 +8,7 @@ import { useContext, useEffect } from 'react';
type RegisterRecordActionEffectProps = {
action: ActionMenuEntry & {
actionHook: ActionHook;
useAction: ActionHook;
};
objectMetadataItem: ObjectMetadataItem;
};
@ -17,7 +17,7 @@ export const RegisterRecordActionEffect = ({
action,
objectMetadataItem,
}: RegisterRecordActionEffectProps) => {
const { shouldBeRegistered, onClick, ConfirmationModal } = action.actionHook({
const { shouldBeRegistered, onClick, ConfirmationModal } = action.useAction({
objectMetadataItem,
});

View File

@ -23,7 +23,7 @@ import {
export const DEFAULT_ACTIONS_CONFIG_V1: Record<
string,
ActionMenuEntry & {
actionHook: ActionHook;
useAction: ActionHook;
}
> = {
addToFavoritesSingleRecord: {
@ -37,7 +37,7 @@ export const DEFAULT_ACTIONS_CONFIG_V1: Record<
ActionViewType.SHOW_PAGE,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useAddToFavoritesSingleRecordAction,
useAction: useAddToFavoritesSingleRecordAction,
},
removeFromFavoritesSingleRecord: {
type: ActionMenuEntryType.Standard,
@ -50,7 +50,7 @@ export const DEFAULT_ACTIONS_CONFIG_V1: Record<
ActionViewType.SHOW_PAGE,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useRemoveFromFavoritesSingleRecordAction,
useAction: useRemoveFromFavoritesSingleRecordAction,
},
deleteSingleRecord: {
type: ActionMenuEntryType.Standard,
@ -65,7 +65,7 @@ export const DEFAULT_ACTIONS_CONFIG_V1: Record<
ActionViewType.SHOW_PAGE,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useDeleteSingleRecordAction,
useAction: useDeleteSingleRecordAction,
},
deleteMultipleRecords: {
type: ActionMenuEntryType.Standard,
@ -78,7 +78,7 @@ export const DEFAULT_ACTIONS_CONFIG_V1: Record<
accent: 'danger',
isPinned: true,
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
actionHook: useDeleteMultipleRecordsAction,
useAction: useDeleteMultipleRecordsAction,
},
exportMultipleRecords: {
type: ActionMenuEntryType.Standard,
@ -91,11 +91,11 @@ export const DEFAULT_ACTIONS_CONFIG_V1: Record<
accent: 'default',
isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
actionHook: useExportMultipleRecordsAction,
useAction: useExportMultipleRecordsAction,
},
exportView: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
scope: ActionMenuEntryScope.Object,
key: NoSelectionRecordActionKeys.EXPORT_VIEW,
label: 'Export view',
shortLabel: 'Export',
@ -104,6 +104,6 @@ export const DEFAULT_ACTIONS_CONFIG_V1: Record<
accent: 'default',
isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
actionHook: useExportMultipleRecordsAction,
useAction: useExportMultipleRecordsAction,
},
};

View File

@ -33,12 +33,12 @@ import {
export const DEFAULT_ACTIONS_CONFIG_V2: Record<
string,
ActionMenuEntry & {
actionHook: ActionHook;
useAction: ActionHook;
}
> = {
createNewRecord: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
scope: ActionMenuEntryScope.Object,
key: NoSelectionRecordActionKeys.CREATE_NEW_RECORD,
label: 'Create new record',
shortLabel: 'New record',
@ -46,7 +46,7 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
isPinned: true,
Icon: IconPlus,
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
actionHook: useCreateNewTableRecordNoSelectionRecordAction,
useAction: useCreateNewTableRecordNoSelectionRecordAction,
},
exportNoteToPdf: {
type: ActionMenuEntryType.Standard,
@ -58,7 +58,7 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
isPinned: false,
Icon: IconFileExport,
availableOn: [ActionViewType.SHOW_PAGE],
actionHook: useExportNoteAction,
useAction: useExportNoteAction,
},
addToFavoritesSingleRecord: {
type: ActionMenuEntryType.Standard,
@ -73,7 +73,7 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionViewType.SHOW_PAGE,
],
actionHook: useAddToFavoritesSingleRecordAction,
useAction: useAddToFavoritesSingleRecordAction,
},
removeFromFavoritesSingleRecord: {
type: ActionMenuEntryType.Standard,
@ -88,7 +88,7 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionViewType.SHOW_PAGE,
],
actionHook: useRemoveFromFavoritesSingleRecordAction,
useAction: useRemoveFromFavoritesSingleRecordAction,
},
deleteSingleRecord: {
type: ActionMenuEntryType.Standard,
@ -104,7 +104,7 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionViewType.SHOW_PAGE,
],
actionHook: useDeleteSingleRecordAction,
useAction: useDeleteSingleRecordAction,
},
deleteMultipleRecords: {
type: ActionMenuEntryType.Standard,
@ -117,7 +117,7 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
accent: 'danger',
isPinned: true,
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
actionHook: useDeleteMultipleRecordsAction,
useAction: useDeleteMultipleRecordsAction,
},
exportMultipleRecords: {
type: ActionMenuEntryType.Standard,
@ -130,11 +130,11 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
accent: 'default',
isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
actionHook: useExportMultipleRecordsAction,
useAction: useExportMultipleRecordsAction,
},
exportView: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.RecordSelection,
scope: ActionMenuEntryScope.Object,
key: NoSelectionRecordActionKeys.EXPORT_VIEW,
label: 'Export view',
shortLabel: 'Export',
@ -143,7 +143,7 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
accent: 'default',
isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
actionHook: useExportMultipleRecordsAction,
useAction: useExportMultipleRecordsAction,
},
destroySingleRecord: {
type: ActionMenuEntryType.Standard,
@ -159,7 +159,7 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionViewType.SHOW_PAGE,
],
actionHook: useDestroySingleRecordAction,
useAction: useDestroySingleRecordAction,
},
navigateToPreviousRecord: {
type: ActionMenuEntryType.Standard,
@ -171,7 +171,7 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
isPinned: true,
Icon: IconChevronUp,
availableOn: [ActionViewType.SHOW_PAGE],
actionHook: useNavigateToPreviousRecordSingleRecordAction,
useAction: useNavigateToPreviousRecordSingleRecordAction,
},
navigateToNextRecord: {
type: ActionMenuEntryType.Standard,
@ -183,6 +183,6 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
isPinned: true,
Icon: IconChevronDown,
availableOn: [ActionViewType.SHOW_PAGE],
actionHook: useNavigateToNextRecordSingleRecordAction,
useAction: useNavigateToNextRecordSingleRecordAction,
},
};

View File

@ -44,7 +44,7 @@ import {
export const WORKFLOW_ACTIONS_CONFIG: Record<
string,
ActionMenuEntry & {
actionHook: ActionHook;
useAction: ActionHook;
}
> = {
createNewRecord: {
@ -57,7 +57,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
isPinned: true,
Icon: IconPlus,
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
actionHook: useCreateNewTableRecordNoSelectionRecordAction,
useAction: useCreateNewTableRecordNoSelectionRecordAction,
},
activateWorkflowSingleRecord: {
key: WorkflowSingleRecordActionKeys.ACTIVATE,
@ -72,7 +72,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
ActionViewType.SHOW_PAGE,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useActivateWorkflowSingleRecordAction,
useAction: useActivateWorkflowSingleRecordAction,
},
deactivateWorkflowSingleRecord: {
key: WorkflowSingleRecordActionKeys.DEACTIVATE,
@ -87,7 +87,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
ActionViewType.SHOW_PAGE,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useDeactivateWorkflowSingleRecordAction,
useAction: useDeactivateWorkflowSingleRecordAction,
},
discardWorkflowDraftSingleRecord: {
key: WorkflowSingleRecordActionKeys.DISCARD_DRAFT,
@ -102,7 +102,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
ActionViewType.SHOW_PAGE,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useDiscardDraftWorkflowSingleRecordAction,
useAction: useDiscardDraftWorkflowSingleRecordAction,
},
seeWorkflowActiveVersionSingleRecord: {
key: WorkflowSingleRecordActionKeys.SEE_ACTIVE_VERSION,
@ -117,7 +117,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
ActionViewType.SHOW_PAGE,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useSeeActiveVersionWorkflowSingleRecordAction,
useAction: useSeeActiveVersionWorkflowSingleRecordAction,
},
seeWorkflowRunsSingleRecord: {
key: WorkflowSingleRecordActionKeys.SEE_RUNS,
@ -132,7 +132,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
ActionViewType.SHOW_PAGE,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useSeeRunsWorkflowSingleRecordAction,
useAction: useSeeRunsWorkflowSingleRecordAction,
},
seeWorkflowVersionsHistorySingleRecord: {
key: WorkflowSingleRecordActionKeys.SEE_VERSIONS,
@ -147,7 +147,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
ActionViewType.SHOW_PAGE,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useSeeVersionsWorkflowSingleRecordAction,
useAction: useSeeVersionsWorkflowSingleRecordAction,
},
testWorkflowSingleRecord: {
key: WorkflowSingleRecordActionKeys.TEST,
@ -162,7 +162,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
ActionViewType.SHOW_PAGE,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useTestWorkflowSingleRecordAction,
useAction: useTestWorkflowSingleRecordAction,
},
navigateToPreviousRecord: {
type: ActionMenuEntryType.Standard,
@ -173,7 +173,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
position: 8,
Icon: IconChevronUp,
availableOn: [ActionViewType.SHOW_PAGE],
actionHook: useNavigateToPreviousRecordSingleRecordAction,
useAction: useNavigateToPreviousRecordSingleRecordAction,
},
navigateToNextRecord: {
type: ActionMenuEntryType.Standard,
@ -184,7 +184,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
position: 9,
Icon: IconChevronDown,
availableOn: [ActionViewType.SHOW_PAGE],
actionHook: useNavigateToNextRecordSingleRecordAction,
useAction: useNavigateToNextRecordSingleRecordAction,
},
addToFavoritesSingleRecord: {
type: ActionMenuEntryType.Standard,
@ -199,7 +199,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionViewType.SHOW_PAGE,
],
actionHook: useAddToFavoritesSingleRecordAction,
useAction: useAddToFavoritesSingleRecordAction,
},
removeFromFavoritesSingleRecord: {
type: ActionMenuEntryType.Standard,
@ -214,7 +214,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionViewType.SHOW_PAGE,
],
actionHook: useRemoveFromFavoritesSingleRecordAction,
useAction: useRemoveFromFavoritesSingleRecordAction,
},
deleteSingleRecord: {
type: ActionMenuEntryType.Standard,
@ -230,7 +230,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionViewType.SHOW_PAGE,
],
actionHook: useDeleteSingleRecordAction,
useAction: useDeleteSingleRecordAction,
},
deleteMultipleRecords: {
type: ActionMenuEntryType.Standard,
@ -243,7 +243,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
accent: 'danger',
isPinned: true,
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
actionHook: useDeleteMultipleRecordsAction,
useAction: useDeleteMultipleRecordsAction,
},
destroySingleRecord: {
type: ActionMenuEntryType.Standard,
@ -259,7 +259,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionViewType.SHOW_PAGE,
],
actionHook: useDestroySingleRecordAction,
useAction: useDestroySingleRecordAction,
},
exportMultipleRecords: {
type: ActionMenuEntryType.Standard,
@ -272,7 +272,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
accent: 'default',
isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
actionHook: useExportMultipleRecordsAction,
useAction: useExportMultipleRecordsAction,
},
exportView: {
type: ActionMenuEntryType.Standard,
@ -285,6 +285,6 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
accent: 'default',
isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
actionHook: useExportMultipleRecordsAction,
useAction: useExportMultipleRecordsAction,
},
};

View File

@ -24,7 +24,7 @@ import {
export const WORKFLOW_RUNS_ACTIONS_CONFIG: Record<
string,
ActionMenuEntry & {
actionHook: ActionHook;
useAction: ActionHook;
}
> = {
addToFavoritesSingleRecord: {
@ -40,7 +40,7 @@ export const WORKFLOW_RUNS_ACTIONS_CONFIG: Record<
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionViewType.SHOW_PAGE,
],
actionHook: useAddToFavoritesSingleRecordAction,
useAction: useAddToFavoritesSingleRecordAction,
},
removeFromFavoritesSingleRecord: {
type: ActionMenuEntryType.Standard,
@ -55,7 +55,7 @@ export const WORKFLOW_RUNS_ACTIONS_CONFIG: Record<
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionViewType.SHOW_PAGE,
],
actionHook: useRemoveFromFavoritesSingleRecordAction,
useAction: useRemoveFromFavoritesSingleRecordAction,
},
navigateToPreviousRecord: {
type: ActionMenuEntryType.Standard,
@ -67,7 +67,7 @@ export const WORKFLOW_RUNS_ACTIONS_CONFIG: Record<
isPinned: true,
Icon: IconChevronUp,
availableOn: [ActionViewType.SHOW_PAGE],
actionHook: useNavigateToPreviousRecordSingleRecordAction,
useAction: useNavigateToPreviousRecordSingleRecordAction,
},
navigateToNextRecord: {
type: ActionMenuEntryType.Standard,
@ -79,7 +79,7 @@ export const WORKFLOW_RUNS_ACTIONS_CONFIG: Record<
isPinned: true,
Icon: IconChevronDown,
availableOn: [ActionViewType.SHOW_PAGE],
actionHook: useNavigateToNextRecordSingleRecordAction,
useAction: useNavigateToNextRecordSingleRecordAction,
},
exportMultipleRecords: {
type: ActionMenuEntryType.Standard,
@ -92,7 +92,7 @@ export const WORKFLOW_RUNS_ACTIONS_CONFIG: Record<
accent: 'default',
isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
actionHook: useExportMultipleRecordsAction,
useAction: useExportMultipleRecordsAction,
},
exportView: {
type: ActionMenuEntryType.Standard,
@ -105,6 +105,6 @@ export const WORKFLOW_RUNS_ACTIONS_CONFIG: Record<
accent: 'default',
isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
actionHook: useExportMultipleRecordsAction,
useAction: useExportMultipleRecordsAction,
},
};

View File

@ -31,7 +31,7 @@ import {
export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
string,
ActionMenuEntry & {
actionHook: ActionHook;
useAction: ActionHook;
}
> = {
useAsDraftWorkflowVersionSingleRecord: {
@ -47,7 +47,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
ActionViewType.SHOW_PAGE,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useUseAsDraftWorkflowVersionSingleRecordAction,
useAction: useUseAsDraftWorkflowVersionSingleRecordAction,
},
seeWorkflowRunsSingleRecord: {
key: WorkflowVersionSingleRecordActionKeys.SEE_RUNS,
@ -61,7 +61,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
ActionViewType.SHOW_PAGE,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useSeeRunsWorkflowVersionSingleRecordAction,
useAction: useSeeRunsWorkflowVersionSingleRecordAction,
},
seeWorkflowVersionsHistorySingleRecord: {
key: WorkflowVersionSingleRecordActionKeys.SEE_VERSIONS,
@ -75,7 +75,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
ActionViewType.SHOW_PAGE,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
],
actionHook: useSeeVersionsWorkflowVersionSingleRecordAction,
useAction: useSeeVersionsWorkflowVersionSingleRecordAction,
},
navigateToPreviousRecord: {
type: ActionMenuEntryType.Standard,
@ -86,7 +86,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
position: 4,
Icon: IconChevronUp,
availableOn: [ActionViewType.SHOW_PAGE],
actionHook: useNavigateToPreviousRecordSingleRecordAction,
useAction: useNavigateToPreviousRecordSingleRecordAction,
},
navigateToNextRecord: {
type: ActionMenuEntryType.Standard,
@ -97,7 +97,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
position: 5,
Icon: IconChevronDown,
availableOn: [ActionViewType.SHOW_PAGE],
actionHook: useNavigateToNextRecordSingleRecordAction,
useAction: useNavigateToNextRecordSingleRecordAction,
},
addToFavoritesSingleRecord: {
type: ActionMenuEntryType.Standard,
@ -112,7 +112,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionViewType.SHOW_PAGE,
],
actionHook: useAddToFavoritesSingleRecordAction,
useAction: useAddToFavoritesSingleRecordAction,
},
removeFromFavoritesSingleRecord: {
type: ActionMenuEntryType.Standard,
@ -127,7 +127,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionViewType.SHOW_PAGE,
],
actionHook: useRemoveFromFavoritesSingleRecordAction,
useAction: useRemoveFromFavoritesSingleRecordAction,
},
exportMultipleRecords: {
type: ActionMenuEntryType.Standard,
@ -140,7 +140,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
accent: 'default',
isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
actionHook: useExportMultipleRecordsAction,
useAction: useExportMultipleRecordsAction,
},
exportView: {
type: ActionMenuEntryType.Standard,
@ -153,6 +153,6 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
accent: 'default',
isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
actionHook: useExportMultipleRecordsAction,
useAction: useExportMultipleRecordsAction,
},
};

View File

@ -0,0 +1,12 @@
import { RegisterAgnosticRecordActionEffect } from '@/action-menu/actions/record-agnostic-actions/components/RegisterAgnosticRecordActionEffect';
import { RECORD_AGNOSTIC_ACTIONS_CONFIG } from '@/action-menu/actions/record-agnostic-actions/constants/RecordAgnosticActionsConfig';
export const RecordAgnosticActionMenuEntriesSetter = () => {
return (
<>
{Object.values(RECORD_AGNOSTIC_ACTIONS_CONFIG).map((action) => (
<RegisterAgnosticRecordActionEffect key={action.key} action={action} />
))}
</>
);
};

View File

@ -1,17 +0,0 @@
import { useRecordAgnosticActions } from '@/action-menu/actions/record-agnostic-actions/hooks/useRecordAgnosticActions';
import { useEffect } from 'react';
export const RecordAgnosticActionsSetterEffect = () => {
const { registerRecordAgnosticActions, unregisterRecordAgnosticActions } =
useRecordAgnosticActions();
useEffect(() => {
registerRecordAgnosticActions();
return () => {
unregisterRecordAgnosticActions();
};
}, [registerRecordAgnosticActions, unregisterRecordAgnosticActions]);
return null;
};

View File

@ -0,0 +1,50 @@
import { ActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { wrapActionInCallbacks } from '@/action-menu/actions/utils/wrapActionInCallbacks';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
import { useContext, useEffect } from 'react';
type RegisterAgnosticRecordActionEffectProps = {
action: ActionMenuEntry & {
useAction: ActionHookWithoutObjectMetadataItem;
};
};
export const RegisterAgnosticRecordActionEffect = ({
action,
}: RegisterAgnosticRecordActionEffectProps) => {
const { shouldBeRegistered, onClick, ConfirmationModal } = action.useAction();
const { onActionStartedCallback, onActionExecutedCallback } =
useContext(ActionMenuContext);
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const wrappedAction = wrapActionInCallbacks({
action: {
...action,
onClick,
ConfirmationModal,
},
onActionStartedCallback,
onActionExecutedCallback,
});
useEffect(() => {
if (shouldBeRegistered) {
addActionMenuEntry(wrappedAction);
}
return () => {
removeActionMenuEntry(wrappedAction.key);
};
}, [
addActionMenuEntry,
removeActionMenuEntry,
shouldBeRegistered,
wrappedAction,
]);
return null;
};

View File

@ -0,0 +1,14 @@
import { RegisterAgnosticRecordActionEffect } from '@/action-menu/actions/record-agnostic-actions/components/RegisterAgnosticRecordActionEffect';
import { useRunWorkflowActions } from '@/action-menu/actions/record-agnostic-actions/run-workflow-actions/hooks/useRunWorkflowActions';
export const RunWorkflowRecordAgnosticActionMenuEntriesSetter = () => {
const { runWorkflowActions } = useRunWorkflowActions();
return (
<>
{runWorkflowActions.map((action) => (
<RegisterAgnosticRecordActionEffect key={action.key} action={action} />
))}
</>
);
};

View File

@ -0,0 +1,31 @@
import { useSearchRecordsRecordAgnosticAction } from '@/action-menu/actions/record-agnostic-actions/hooks/useSearchRecordsRecordAgnosticAction';
import { RecordAgnosticActionsKey } from '@/action-menu/actions/record-agnostic-actions/types/RecordAgnosticActionsKey';
import { ActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import {
ActionMenuEntry,
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { IconSearch } from 'twenty-ui';
export const RECORD_AGNOSTIC_ACTIONS_CONFIG: Record<
string,
ActionMenuEntry & {
useAction: ActionHookWithoutObjectMetadataItem;
}
> = {
searchRecords: {
type: ActionMenuEntryType.Standard,
scope: ActionMenuEntryScope.Global,
key: RecordAgnosticActionsKey.SEARCH_RECORDS,
label: 'Search records',
shortLabel: 'Search',
position: 0,
isPinned: false,
Icon: IconSearch,
availableOn: [ActionViewType.GLOBAL],
useAction: useSearchRecordsRecordAgnosticAction,
hotKeys: ['/'],
},
};

View File

@ -1,26 +0,0 @@
import { useWorkflowRunActions } from '@/action-menu/actions/record-agnostic-actions/workflow-run-actions/hooks/useWorkflowRunActions';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
export const useRecordAgnosticActions = () => {
const isWorkflowEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsWorkflowEnabled,
);
const { addWorkflowRunActions, removeWorkflowRunActions } =
useWorkflowRunActions();
const registerRecordAgnosticActions = () => {
if (isWorkflowEnabled) {
addWorkflowRunActions();
}
};
const unregisterRecordAgnosticActions = () => {
if (isWorkflowEnabled) {
removeWorkflowRunActions();
}
};
return { registerRecordAgnosticActions, unregisterRecordAgnosticActions };
};

View File

@ -0,0 +1,28 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { useRecoilCallback } from 'recoil';
import { IconSearch } from 'twenty-ui';
export const useSearchRecordsRecordAgnosticAction = () => {
const { openCommandMenu } = useCommandMenu();
const onClick = useRecoilCallback(
({ set }) =>
() => {
set(commandMenuPageState, CommandMenuPages.SearchRecords);
set(commandMenuPageInfoState, {
title: 'Search',
Icon: IconSearch,
});
openCommandMenu();
},
[openCommandMenu],
);
return {
onClick,
shouldBeRegistered: true,
};
};

View File

@ -1,4 +1,3 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import {
ActionMenuEntryScope,
ActionMenuEntryType,
@ -11,55 +10,49 @@ import { capitalize } from 'twenty-shared';
import { IconSettingsAutomation, isDefined } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql';
export const useWorkflowRunActions = () => {
export const useRunWorkflowActions = () => {
const isWorkflowEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsWorkflowEnabled,
);
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const { records: activeWorkflowVersions } = useAllActiveWorkflowVersions({
triggerType: 'MANUAL',
});
const { runWorkflowVersion } = useRunWorkflowVersion();
const addWorkflowRunActions = () => {
if (!isWorkflowEnabled) {
return;
}
if (!isWorkflowEnabled) {
return { runWorkflowActions: [] };
}
for (const [
index,
activeWorkflowVersion,
] of activeWorkflowVersions.entries()) {
const runWorkflowActions = activeWorkflowVersions
.map((activeWorkflowVersion, index) => {
if (!isDefined(activeWorkflowVersion.workflow)) {
continue;
return undefined;
}
const name = capitalize(activeWorkflowVersion.workflow.name);
addActionMenuEntry({
return {
type: ActionMenuEntryType.WorkflowRun,
key: `workflow-run-${activeWorkflowVersion.id}`,
scope: ActionMenuEntryScope.Global,
label: name,
position: index,
Icon: IconSettingsAutomation,
onClick: async () => {
await runWorkflowVersion({
workflowVersionId: activeWorkflowVersion.id,
});
useAction: () => {
return {
shouldBeRegistered: true,
onClick: async () => {
await runWorkflowVersion({
workflowVersionId: activeWorkflowVersion.id,
});
},
};
},
});
}
};
};
})
.filter(isDefined);
const removeWorkflowRunActions = () => {
for (const activeWorkflowVersion of activeWorkflowVersions) {
removeActionMenuEntry(`workflow-run-${activeWorkflowVersion.id}`);
}
};
return { addWorkflowRunActions, removeWorkflowRunActions };
return { runWorkflowActions };
};

View File

@ -0,0 +1,3 @@
export enum RecordAgnosticActionsKey {
SEARCH_RECORDS = 'search-records',
}

View File

@ -1,4 +1,5 @@
export enum ActionViewType {
GLOBAL = 'GLOBAL',
INDEX_PAGE_BULK_SELECTION = 'INDEX_PAGE_BULK_SELECTION',
INDEX_PAGE_SINGLE_RECORD_SELECTION = 'INDEX_PAGE_SINGLE_RECORD_SELECTION',
INDEX_PAGE_NO_SELECTION = 'INDEX_PAGE_NO_SELECTION',

View File

@ -1,6 +1,7 @@
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
import { MultipleRecordsActionKeys } from '@/action-menu/actions/record-actions/multiple-records/types/MultipleRecordsActionKeys';
import { RecordAgnosticActionsSetterEffect } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionsSetterEffect';
import { RecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionMenuEntriesSetter';
import { RunWorkflowRecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RunWorkflowRecordAgnosticActionMenuEntriesSetter';
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
import { RecordIndexActionMenuBar } from '@/action-menu/components/RecordIndexActionMenuBar';
import { RecordIndexActionMenuButtons } from '@/action-menu/components/RecordIndexActionMenuButtons';
@ -21,14 +22,14 @@ export const RecordIndexActionMenu = ({ indexId }: { indexId: string }) => {
contextStoreCurrentObjectMetadataIdComponentState,
);
const isWorkflowEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsWorkflowEnabled,
);
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const isWorkflowEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsWorkflowEnabled,
);
const isMobile = useIsMobile();
const setIsLoadMoreLocked = useSetRecoilComponentStateV2(
@ -63,7 +64,10 @@ export const RecordIndexActionMenu = ({ indexId }: { indexId: string }) => {
<ActionMenuConfirmationModals />
<RecordIndexActionMenuEffect />
<RecordActionMenuEntriesSetter />
{isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />}
<RecordAgnosticActionMenuEntriesSetter />
{isWorkflowEnabled && (
<RunWorkflowRecordAgnosticActionMenuEntriesSetter />
)}
</ActionMenuContext.Provider>
)}
</>

View File

@ -1,5 +1,6 @@
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
import { RecordAgnosticActionsSetterEffect } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionsSetterEffect';
import { RecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionMenuEntriesSetter';
import { RunWorkflowRecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RunWorkflowRecordAgnosticActionMenuEntriesSetter';
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
import { RecordShowActionMenuButtons } from '@/action-menu/components/RecordShowActionMenuButtons';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
@ -29,14 +30,14 @@ export const RecordShowActionMenu = ({
contextStoreCurrentObjectMetadataIdComponentState,
);
const isWorkflowEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsWorkflowEnabled,
);
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const isWorkflowEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsWorkflowEnabled,
);
// TODO: refactor RecordShowPageBaseHeader to use the context store
return (
@ -63,7 +64,10 @@ export const RecordShowActionMenu = ({
)}
<ActionMenuConfirmationModals />
<RecordActionMenuEntriesSetter />
{isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />}
<RecordAgnosticActionMenuEntriesSetter />
{isWorkflowEnabled && (
<RunWorkflowRecordAgnosticActionMenuEntriesSetter />
)}
</ActionMenuContext.Provider>
)}
</>

View File

@ -1,5 +1,6 @@
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
import { RecordAgnosticActionsSetterEffect } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionsSetterEffect';
import { RecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionMenuEntriesSetter';
import { RunWorkflowRecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RunWorkflowRecordAgnosticActionMenuEntriesSetter';
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
import { RightDrawerActionMenuDropdown } from '@/action-menu/components/RightDrawerActionMenuDropdown';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
@ -7,7 +8,7 @@ import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const RecordShowRightDrawerActionMenu = () => {
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
@ -25,7 +26,10 @@ export const RecordShowRightDrawerActionMenu = () => {
<RightDrawerActionMenuDropdown />
<ActionMenuConfirmationModals />
<RecordActionMenuEntriesSetter />
{isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />}
<RecordAgnosticActionMenuEntriesSetter />
{isWorkflowEnabled && (
<RunWorkflowRecordAgnosticActionMenuEntriesSetter />
)}
</ActionMenuContext.Provider>
)}
</>

View File

@ -11,6 +11,7 @@ export enum ActionMenuEntryType {
export enum ActionMenuEntryScope {
Global = 'Global',
RecordSelection = 'RecordSelection',
Object = 'Object',
}
export type ActionMenuEntry = {
@ -26,4 +27,5 @@ export type ActionMenuEntry = {
availableOn?: ActionViewType[];
onClick?: (event?: MouseEvent<HTMLElement>) => void;
ConfirmationModal?: ReactElement<ConfirmationModalProps>;
hotKeys?: string[];
};

View File

@ -1,92 +1,37 @@
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuDefaultSelectionEffect } from '@/command-menu/components/CommandMenuDefaultSelectionEffect';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { ResetContextToSelectionCommandButton } from '@/command-menu/components/ResetContextToSelectionCommandButton';
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
import { useCommandMenuOnItemClick } from '@/command-menu/hooks/useCommandMenuOnItemClick';
import { RESET_CONTEXT_TO_SELECTION } from '@/command-menu/constants/ResetContextToSelection';
import { useMatchingCommandMenuCommands } from '@/command-menu/hooks/useMatchingCommandMenuCommands';
import { useResetPreviousCommandMenuContext } from '@/command-menu/hooks/useResetPreviousCommandMenuContext';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { Command } from '@/command-menu/types/Command';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';
const MOBILE_NAVIGATION_BAR_HEIGHT = 64;
type CommandGroupConfig = {
export type CommandGroupConfig = {
heading: string;
items?: Command[];
};
const StyledList = styled.div`
background: ${({ theme }) => theme.background.secondary};
overscroll-behavior: contain;
transition: 100ms ease;
transition-property: height;
`;
const StyledInnerList = styled.div<{ isMobile: boolean }>`
max-height: ${({ isMobile }) =>
isMobile
? `calc(100dvh - ${COMMAND_MENU_SEARCH_BAR_HEIGHT}px - ${
COMMAND_MENU_SEARCH_BAR_PADDING * 2
}px - ${MOBILE_NAVIGATION_BAR_HEIGHT}px)`
: `calc(100dvh - ${COMMAND_MENU_SEARCH_BAR_HEIGHT}px - ${
COMMAND_MENU_SEARCH_BAR_PADDING * 2
}px)`};
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(1)};
width: calc(100% - ${({ theme }) => theme.spacing(4)});
`;
const StyledEmpty = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.light};
display: flex;
font-size: ${({ theme }) => theme.font.size.md};
height: 64px;
justify-content: center;
white-space: pre-wrap;
`;
export const CommandMenu = () => {
const { t } = useLingui();
const { onItemClick } = useCommandMenuOnItemClick();
const { resetPreviousCommandMenuContext } =
useResetPreviousCommandMenuContext();
const commandMenuSearch = useRecoilValue(commandMenuSearchState);
const isMobile = useIsMobile();
const {
isNoResults,
isLoading,
noResults,
copilotCommands,
matchingStandardActionRecordSelectionCommands,
matchingStandardActionObjectCommands,
matchingWorkflowRunRecordSelectionCommands,
matchingStandardActionGlobalCommands,
matchingWorkflowRunGlobalCommands,
matchingNavigateCommand,
peopleCommands,
companyCommands,
opportunityCommands,
noteCommands,
tasksCommands,
customObjectCommands,
matchingNavigateCommands,
} = useMatchingCommandMenuCommands({
commandMenuSearch,
});
@ -94,16 +39,11 @@ export const CommandMenu = () => {
const selectableItems: Command[] = copilotCommands
.concat(
matchingStandardActionRecordSelectionCommands,
matchingStandardActionObjectCommands,
matchingWorkflowRunRecordSelectionCommands,
matchingStandardActionGlobalCommands,
matchingWorkflowRunGlobalCommands,
matchingNavigateCommand,
peopleCommands,
companyCommands,
opportunityCommands,
noteCommands,
tasksCommands,
customObjectCommands,
matchingNavigateCommands,
)
.filter(isDefined);
@ -115,7 +55,7 @@ export const CommandMenu = () => {
const selectableItemIds = selectableItems.map((item) => item.id);
if (isNonEmptyString(previousContextStoreCurrentObjectMetadataId)) {
selectableItemIds.unshift('reset-context-to-selection');
selectableItemIds.unshift(RESET_CONTEXT_TO_SELECTION);
}
const commandGroups: CommandGroupConfig[] = [
@ -125,130 +65,35 @@ export const CommandMenu = () => {
},
{
heading: t`Record Selection`,
items: matchingStandardActionRecordSelectionCommands,
items: matchingStandardActionRecordSelectionCommands.concat(
matchingWorkflowRunRecordSelectionCommands,
),
},
{
heading: t`Workflow Record Selection`,
items: matchingWorkflowRunRecordSelectionCommands,
heading: t`Object`,
items: matchingStandardActionObjectCommands,
},
{
heading: t`View`,
items: matchingStandardActionGlobalCommands,
},
{
heading: t`Workflows`,
items: matchingWorkflowRunGlobalCommands,
},
{
heading: t`Navigate`,
items: matchingNavigateCommand,
},
{
heading: t`People`,
items: peopleCommands,
},
{
heading: t`Companies`,
items: companyCommands,
},
{
heading: t`Opportunities`,
items: opportunityCommands,
},
{
heading: t`Notes`,
items: noteCommands,
},
{
heading: t`Tasks`,
items: tasksCommands,
},
{
heading: t`Custom Objects`,
items: customObjectCommands,
heading: t`Global`,
items: matchingStandardActionGlobalCommands
.concat(matchingNavigateCommands)
.concat(matchingWorkflowRunGlobalCommands),
},
];
return (
<>
<CommandMenuDefaultSelectionEffect
selectableItemIds={selectableItemIds}
/>
<StyledList>
<ScrollWrapper
contextProviderName="commandMenu"
componentInstanceId={`scroll-wrapper-command-menu`}
>
<StyledInnerList isMobile={isMobile}>
<SelectableList
selectableListId="command-menu-list"
selectableItemIdArray={selectableItemIds}
hotkeyScope={AppHotkeyScope.CommandMenu}
onEnter={(itemId) => {
if (itemId === 'reset-context-to-selection') {
resetPreviousCommandMenuContext();
return;
}
const command = selectableItems.find(
(item) => item.id === itemId,
);
if (isDefined(command)) {
const { to, onCommandClick, shouldCloseCommandMenuOnClick } =
command;
onItemClick({
shouldCloseCommandMenuOnClick,
onClick: onCommandClick,
to,
});
}
}}
>
{isNonEmptyString(
previousContextStoreCurrentObjectMetadataId,
) && (
<CommandGroup heading={t`Context`} key={t`Context`}>
<SelectableItem itemId="reset-context-to-selection">
<ResetContextToSelectionCommandButton />
</SelectableItem>
</CommandGroup>
)}
{isNoResults && !isLoading && (
<StyledEmpty>No results found</StyledEmpty>
)}
{commandGroups.map(({ heading, items }) =>
items?.length ? (
<CommandGroup heading={heading} key={heading}>
{items.map((item) => {
return (
<SelectableItem itemId={item.id} key={item.id}>
<CommandMenuItem
key={item.id}
id={item.id}
Icon={item.Icon}
label={item.label}
to={item.to}
onClick={item.onCommandClick}
firstHotKey={item.firstHotKey}
secondHotKey={item.secondHotKey}
shouldCloseCommandMenuOnClick={
item.shouldCloseCommandMenuOnClick
}
/>
</SelectableItem>
);
})}
</CommandGroup>
) : null,
)}
</SelectableList>
</StyledInnerList>
</ScrollWrapper>
</StyledList>
</>
<CommandMenuList
commandGroups={commandGroups}
selectableItemIds={selectableItemIds}
noResults={noResults}
>
{isNonEmptyString(previousContextStoreCurrentObjectMetadataId) && (
<CommandGroup heading={t`Context`}>
<SelectableItem itemId={RESET_CONTEXT_TO_SELECTION}>
<ResetContextToSelectionCommandButton />
</SelectableItem>
</CommandGroup>
)}
</CommandMenuList>
);
};

View File

@ -1,5 +1,7 @@
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
import { RecordAgnosticActionsSetterEffect } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionsSetterEffect';
import { RecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionMenuEntriesSetter';
import { RunWorkflowRecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RunWorkflowRecordAgnosticActionMenuEntriesSetter';
import { RecordAgnosticActionsKey } from '@/action-menu/actions/record-agnostic-actions/types/RecordAgnosticActionsKey';
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
@ -20,7 +22,7 @@ import { motion } from 'framer-motion';
import { useRef } from 'react';
import { useRecoilValue } from 'recoil';
import { useIsMobile } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
const StyledCommandMenu = styled(motion.div)`
background: ${({ theme }) => theme.background.secondary};
@ -45,9 +47,6 @@ export const CommandMenuContainer = ({
}) => {
const { toggleCommandMenu, closeCommandMenu } = useCommandMenu();
const isWorkflowEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsWorkflowEnabled,
);
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
const commandMenuRef = useRef<HTMLDivElement>(null);
@ -74,6 +73,10 @@ export const CommandMenuContainer = ({
const theme = useTheme();
const isWorkflowEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsWorkflowEnabled,
);
return (
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: 'command-menu' }}
@ -87,11 +90,18 @@ export const CommandMenuContainer = ({
<ActionMenuContext.Provider
value={{
isInRightDrawer: false,
onActionExecutedCallback: toggleCommandMenu,
onActionExecutedCallback: ({ key }) => {
if (key !== RecordAgnosticActionsKey.SEARCH_RECORDS) {
toggleCommandMenu();
}
},
}}
>
<RecordActionMenuEntriesSetter />
{isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />}
<RecordAgnosticActionMenuEntriesSetter />
{isWorkflowEnabled && (
<RunWorkflowRecordAgnosticActionMenuEntriesSetter />
)}
<ActionMenuConfirmationModals />
{isCommandMenuOpened && (
<StyledCommandMenu

View File

@ -12,8 +12,7 @@ export type CommandMenuItemProps = {
id: string;
onClick?: () => void;
Icon?: IconComponent;
firstHotKey?: string;
secondHotKey?: string;
hotKeys?: string[];
shouldCloseCommandMenuOnClick?: boolean;
RightComponent?: ReactNode;
};
@ -24,8 +23,7 @@ export const CommandMenuItem = ({
id,
onClick,
Icon,
firstHotKey,
secondHotKey,
hotKeys,
shouldCloseCommandMenuOnClick,
RightComponent,
}: CommandMenuItemProps) => {
@ -42,8 +40,7 @@ export const CommandMenuItem = ({
<MenuItemCommand
LeftIcon={Icon}
text={label}
firstHotKey={firstHotKey}
secondHotKey={secondHotKey}
hotKeys={hotKeys}
onClick={() =>
onItemClick({
shouldCloseCommandMenuOnClick,

View File

@ -0,0 +1,146 @@
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandGroupConfig } from '@/command-menu/components/CommandMenu';
import { CommandMenuDefaultSelectionEffect } from '@/command-menu/components/CommandMenuDefaultSelectionEffect';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
import { RESET_CONTEXT_TO_SELECTION } from '@/command-menu/constants/ResetContextToSelection';
import { useCommandMenuOnItemClick } from '@/command-menu/hooks/useCommandMenuOnItemClick';
import { useResetPreviousCommandMenuContext } from '@/command-menu/hooks/useResetPreviousCommandMenuContext';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import styled from '@emotion/styled';
import { MOBILE_VIEWPORT, isDefined } from 'twenty-ui';
const MOBILE_NAVIGATION_BAR_HEIGHT = 64;
export type CommandMenuListProps = {
commandGroups: CommandGroupConfig[];
selectableItemIds: string[];
children?: React.ReactNode;
loading?: boolean;
noResults?: boolean;
};
const StyledList = styled.div`
background: ${({ theme }) => theme.background.secondary};
overscroll-behavior: contain;
transition: 100ms ease;
transition-property: height;
`;
const StyledInnerList = styled.div`
max-height: calc(
100dvh - ${COMMAND_MENU_SEARCH_BAR_HEIGHT}px -
${COMMAND_MENU_SEARCH_BAR_PADDING * 2}px -
${MOBILE_NAVIGATION_BAR_HEIGHT}px
);
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(1)};
width: calc(100% - ${({ theme }) => theme.spacing(4)});
@media (min-width: ${MOBILE_VIEWPORT}px) {
max-height: calc(
100dvh - ${COMMAND_MENU_SEARCH_BAR_HEIGHT}px -
${COMMAND_MENU_SEARCH_BAR_PADDING * 2}px
);
}
`;
const StyledEmpty = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.light};
display: flex;
font-size: ${({ theme }) => theme.font.size.md};
height: 64px;
justify-content: center;
white-space: pre-wrap;
`;
export const CommandMenuList = ({
commandGroups,
selectableItemIds,
children,
loading = false,
noResults = false,
}: CommandMenuListProps) => {
const { onItemClick } = useCommandMenuOnItemClick();
const commands = commandGroups.flatMap((group) => group.items ?? []);
const { resetPreviousCommandMenuContext } =
useResetPreviousCommandMenuContext();
return (
<>
<CommandMenuDefaultSelectionEffect
selectableItemIds={selectableItemIds}
/>
<StyledList>
<ScrollWrapper
contextProviderName="commandMenu"
componentInstanceId={`scroll-wrapper-command-menu`}
>
<StyledInnerList>
<SelectableList
selectableListId="command-menu-list"
hotkeyScope={AppHotkeyScope.CommandMenuOpen}
selectableItemIdArray={selectableItemIds}
onEnter={(itemId) => {
if (itemId === RESET_CONTEXT_TO_SELECTION) {
resetPreviousCommandMenuContext();
return;
}
const command = commands.find((item) => item.id === itemId);
if (isDefined(command)) {
const { to, onCommandClick, shouldCloseCommandMenuOnClick } =
command;
onItemClick({
shouldCloseCommandMenuOnClick,
onClick: onCommandClick,
to,
});
}
}}
>
{children}
{noResults && !loading && (
<StyledEmpty>No result found</StyledEmpty>
)}
{commandGroups.map(({ heading, items }) =>
items?.length ? (
<CommandGroup heading={heading} key={heading}>
{items.map((item) => {
return (
<SelectableItem itemId={item.id} key={item.id}>
<CommandMenuItem
key={item.id}
id={item.id}
Icon={item.Icon}
label={item.label}
to={item.to}
onClick={item.onCommandClick}
hotKeys={item.hotKeys}
shouldCloseCommandMenuOnClick={
item.shouldCloseCommandMenuOnClick
}
/>
</SelectableItem>
);
})}
</CommandGroup>
) : null,
)}
</SelectableList>
</StyledInnerList>
</ScrollWrapper>
</StyledList>
</>
);
};

View File

@ -1,12 +1,12 @@
import { CommandMenuContextChip } from '@/command-menu/components/CommandMenuContextChip';
import { CommandMenuContextRecordChip } from '@/command-menu/components/CommandMenuContextRecordChip';
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useTheme } from '@emotion/react';
@ -95,11 +95,12 @@ export const CommandMenuTopBar = () => {
return (
<StyledInputContainer>
<StyledContentContainer>
{isDefined(contextStoreCurrentObjectMetadataId) && (
<CommandMenuContextRecordChip
objectMetadataItemId={contextStoreCurrentObjectMetadataId}
/>
)}
{commandMenuPage !== CommandMenuPages.SearchRecords &&
isDefined(contextStoreCurrentObjectMetadataId) && (
<CommandMenuContextRecordChip
objectMetadataItemId={contextStoreCurrentObjectMetadataId}
/>
)}
{isDefined(Icon) && (
<CommandMenuContextChip
Icons={[<Icon size={theme.icon.size.sm} />]}
@ -107,7 +108,8 @@ export const CommandMenuTopBar = () => {
/>
)}
{commandMenuPage === CommandMenuPages.Root && (
{(commandMenuPage === CommandMenuPages.Root ||
commandMenuPage === CommandMenuPages.SearchRecords) && (
<StyledInput
autoFocus
value={commandMenuSearch}

View File

@ -1,5 +1,6 @@
import { CommandMenuContextRecordChip } from '@/command-menu/components/CommandMenuContextRecordChip';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { RESET_CONTEXT_TO_SELECTION } from '@/command-menu/constants/ResetContextToSelection';
import { useResetPreviousCommandMenuContext } from '@/command-menu/hooks/useResetPreviousCommandMenuContext';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
@ -40,7 +41,7 @@ export const ResetContextToSelectionCommandButton = () => {
return (
<CommandMenuItem
id="reset-context-to-selection"
id={RESET_CONTEXT_TO_SELECTION}
Icon={IconArrowBackUp}
label={t`Reset to`}
RightComponent={

View File

@ -85,42 +85,43 @@ export const DefaultWithoutSearch: Story = {
play: async () => {
const canvas = within(document.body);
expect(await canvas.findByText('Go to People')).toBeInTheDocument();
expect(await canvas.findByText('Go to Companies')).toBeInTheDocument();
expect(await canvas.findByText('Go to Opportunities')).toBeInTheDocument();
expect(await canvas.findByText('Go to Settings')).toBeInTheDocument();
expect(await canvas.findByText('Go to Tasks')).toBeInTheDocument();
expect(await canvas.findByText('Go to People')).toBeVisible();
expect(await canvas.findByText('Go to Companies')).toBeVisible();
expect(await canvas.findByText('Go to Opportunities')).toBeVisible();
expect(await canvas.findByText('Go to Settings')).toBeVisible();
expect(await canvas.findByText('Go to Tasks')).toBeVisible();
},
};
export const MatchingPersonCompanyActivityCreateNavigate: Story = {
play: async () => {
const canvas = within(document.body);
const searchInput = await canvas.findByPlaceholderText('Type anything');
await sleep(openTimeout);
await userEvent.type(searchInput, 'n');
expect(await canvas.findByText('Linkedin')).toBeInTheDocument();
expect(await canvas.findByText(companiesMock[0].name)).toBeInTheDocument();
expect(await canvas.findByText('Go to Companies')).toBeInTheDocument();
},
};
export const OnlyMatchingCreateAndNavigate: Story = {
export const MatchingNavigate: Story = {
play: async () => {
const canvas = within(document.body);
const searchInput = await canvas.findByPlaceholderText('Type anything');
await sleep(openTimeout);
await userEvent.type(searchInput, 'ta');
expect(await canvas.findByText('Go to Tasks')).toBeInTheDocument();
expect(await canvas.findByText('Go to Tasks')).toBeVisible();
},
};
export const AtleastMatchingOnePerson: Story = {
export const MatchingNavigateShortcuts: Story = {
play: async () => {
const canvas = within(document.body);
const searchInput = await canvas.findByPlaceholderText('Type anything');
await sleep(openTimeout);
await userEvent.type(searchInput, 'alex');
expect(await canvas.findByText('Sylvie Palmer')).toBeInTheDocument();
await userEvent.type(searchInput, 'gp');
expect(await canvas.findByText('Go to People')).toBeVisible();
},
};
export const SearchRecordsAction: Story = {
play: async () => {
const canvas = within(document.body);
const searchRecordsButton = await canvas.findByText('Search records');
await userEvent.click(searchRecordsButton);
const searchInput = await canvas.findByPlaceholderText('Type anything');
await sleep(openTimeout);
await userEvent.type(searchInput, 'n');
expect(await canvas.findByText('Linkedin')).toBeVisible();
expect(await canvas.findByText(companiesMock[0].name)).toBeVisible();
},
};

View File

@ -21,8 +21,7 @@ export const COMMAND_MENU_NAVIGATE_COMMANDS: { [key: string]: Command } = {
}),
label: 'Go to People',
type: CommandType.Navigate,
firstHotKey: 'G',
secondHotKey: 'P',
hotKeys: ['G', 'P'],
Icon: IconUser,
shouldCloseCommandMenuOnClick: true,
},
@ -33,8 +32,7 @@ export const COMMAND_MENU_NAVIGATE_COMMANDS: { [key: string]: Command } = {
}),
label: 'Go to Companies',
type: CommandType.Navigate,
firstHotKey: 'G',
secondHotKey: 'C',
hotKeys: ['G', 'C'],
Icon: IconBuildingSkyscraper,
shouldCloseCommandMenuOnClick: true,
},
@ -45,8 +43,7 @@ export const COMMAND_MENU_NAVIGATE_COMMANDS: { [key: string]: Command } = {
}),
label: 'Go to Opportunities',
type: CommandType.Navigate,
firstHotKey: 'G',
secondHotKey: 'O',
hotKeys: ['G', 'O'],
Icon: IconTargetArrow,
shouldCloseCommandMenuOnClick: true,
},
@ -55,8 +52,7 @@ export const COMMAND_MENU_NAVIGATE_COMMANDS: { [key: string]: Command } = {
to: getSettingsPath(SettingsPath.ProfilePage),
label: 'Go to Settings',
type: CommandType.Navigate,
firstHotKey: 'G',
secondHotKey: 'S',
hotKeys: ['G', 'S'],
Icon: IconSettings,
shouldCloseCommandMenuOnClick: true,
},
@ -67,8 +63,7 @@ export const COMMAND_MENU_NAVIGATE_COMMANDS: { [key: string]: Command } = {
}),
label: 'Go to Tasks',
type: CommandType.Navigate,
firstHotKey: 'G',
secondHotKey: 'T',
hotKeys: ['G', 'T'],
Icon: IconCheckbox,
shouldCloseCommandMenuOnClick: true,
},

View File

@ -2,7 +2,8 @@ import { RightDrawerCalendarEvent } from '@/activities/calendar/right-drawer/com
import { RightDrawerAIChat } from '@/activities/copilot/right-drawer/components/RightDrawerAIChat';
import { RightDrawerEmailThread } from '@/activities/emails/right-drawer/components/RightDrawerEmailThread';
import { CommandMenu } from '@/command-menu/components/CommandMenu';
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
import { CommandMenuSearchRecordsPage } from '@/command-menu/pages/components/CommandMenuSearchRecordsPage';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { RightDrawerRecord } from '@/object-record/record-right-drawer/components/RightDrawerRecord';
import { RightDrawerWorkflowEditStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowEditStep';
import { RightDrawerWorkflowViewStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowViewStep';
@ -28,4 +29,5 @@ export const COMMAND_MENU_PAGES_CONFIG = new Map<
],
[CommandMenuPages.WorkflowStepEdit, <RightDrawerWorkflowEditStep />],
[CommandMenuPages.WorkflowStepView, <RightDrawerWorkflowViewStep />],
[CommandMenuPages.SearchRecords, <CommandMenuSearchRecordsPage />],
]);

View File

@ -0,0 +1 @@
export const RESET_CONTEXT_TO_SELECTION = 'reset-context-to-selection';

View File

@ -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,

View File

@ -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,
};
};

View File

@ -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],
() => {

View File

@ -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) => {

View File

@ -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,
};
};

View File

@ -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: () => {},
};
};

View File

@ -0,0 +1,20 @@
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { useSearchRecords } from '@/command-menu/hooks/useSearchRecords';
import { useMemo } from 'react';
export const CommandMenuSearchRecordsPage = () => {
const { commandGroups, loading, noResults } = useSearchRecords();
const selectableItemIds = useMemo(() => {
return commandGroups.flatMap((group) => group.items).map((item) => item.id);
}, [commandGroups]);
return (
<CommandMenuList
commandGroups={commandGroups}
selectableItemIds={selectableItemIds}
loading={loading}
noResults={noResults}
/>
);
};

View File

@ -1,4 +1,4 @@
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { createState } from '@ui/utilities/state/utils/createState';
export const commandMenuPageState = createState<CommandMenuPages>({

View File

@ -9,6 +9,7 @@ export enum CommandType {
export enum CommandScope {
Global = 'Global',
RecordSelection = 'RecordSelection',
Object = 'Object',
}
export type Command = {
@ -18,8 +19,7 @@ export type Command = {
type?: CommandType;
scope?: CommandScope;
Icon?: IconComponent;
firstHotKey?: string;
secondHotKey?: string;
hotKeys?: string[];
onCommandClick?: () => void;
shouldCloseCommandMenuOnClick?: boolean;
};

View File

@ -8,4 +8,5 @@ export enum CommandMenuPages {
WorkflowStepSelectAction = 'workflow-step-select-action',
WorkflowStepView = 'workflow-step-view',
WorkflowStepEdit = 'workflow-step-edit',
SearchRecords = 'search-records',
}

View File

@ -1,6 +1,6 @@
import { useLocation } from 'react-router-dom';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { IconSearch, IconSettings, getOsControlSymbol } from 'twenty-ui';
import { IconSearch, IconSettings } from 'twenty-ui';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CurrentWorkspaceMemberFavoritesFolders } from '@/favorites/components/CurrentWorkspaceMemberFavoritesFolders';
@ -28,7 +28,6 @@ const StyledInnerContainer = styled.div`
export const MainNavigationDrawerItems = () => {
const isMobile = useIsMobile();
const { toggleCommandMenu } = useCommandMenu();
const location = useLocation();
const setNavigationMemorizedUrl = useSetRecoilState(
navigationMemorizedUrlState,
@ -42,6 +41,8 @@ export const MainNavigationDrawerItems = () => {
const { t } = useLingui();
const { openRecordsSearchPage } = useCommandMenu();
return (
<>
{!isMobile && (
@ -49,8 +50,8 @@ export const MainNavigationDrawerItems = () => {
<NavigationDrawerItem
label={t`Search`}
Icon={IconSearch}
onClick={toggleCommandMenu}
keyboard={[getOsControlSymbol(), 'K']}
onClick={openRecordsSearchPage}
keyboard={['/']}
/>
<NavigationDrawerItem
label={t`Settings`}

View File

@ -17,7 +17,7 @@ type NavigationBarItemName = 'main' | 'search' | 'tasks' | 'settings';
export const MobileNavigationBar = () => {
const [isCommandMenuOpened] = useRecoilState(isCommandMenuOpenedState);
const { closeCommandMenu, openCommandMenu } = useCommandMenu();
const { closeCommandMenu, openRecordsSearchPage } = useCommandMenu();
const isSettingsPage = useIsSettingsPage();
const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] =
useRecoilState(isNavigationDrawerExpandedState);
@ -53,12 +53,7 @@ export const MobileNavigationBar = () => {
{
name: 'search',
Icon: IconSearch,
onClick: () => {
if (!isCommandMenuOpened) {
openCommandMenu();
}
setIsNavigationDrawerExpanded(false);
},
onClick: openRecordsSearchPage,
},
{
name: 'settings',

View File

@ -1,4 +1,4 @@
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
export const mapRightDrawerPageToCommandMenuPage = (

View File

@ -14,6 +14,7 @@ import { useRecoilState } from 'recoil';
import { capitalize } from 'twenty-shared';
import {
IconComponent,
Label,
MOBILE_VIEWPORT,
Pill,
TablerIconsProps,
@ -163,13 +164,16 @@ const StyledItemCount = styled.span`
const StyledKeyBoardShortcut = styled.span`
align-items: center;
border-radius: 4px;
color: ${({ theme }) => theme.font.color.light};
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
height: ${({ theme }) => theme.spacing(4)};
justify-content: center;
letter-spacing: 1px;
margin-left: auto;
visibility: hidden;
width: ${({ theme }) => theme.spacing(4)};
border-radius: ${({ theme }) => theme.border.radius.sm};
border: 1px solid ${({ theme }) => theme.border.color.strong};
background: ${({ theme }) => theme.background.transparent.lighter};
`;
const StyledNavigationDrawerItemContainer = styled.div`
@ -339,7 +343,7 @@ export const NavigationDrawerItem = ({
{keyboard && (
<NavigationDrawerAnimatedCollapseWrapper>
<StyledKeyBoardShortcut className="keyboard-shortcuts">
{keyboard}
<Label>{keyboard}</Label>
</StyledKeyBoardShortcut>
</NavigationDrawerAnimatedCollapseWrapper>
)}

View File

@ -32,6 +32,10 @@ export const useScopedHotkeys = (
? options.preventDefault === true
: true;
const ignoreModifiers = isDefined(options?.ignoreModifiers)
? options.ignoreModifiers === true
: false;
return useHotkeys(
keys,
(keyboardEvent, hotkeysEvent) => {
@ -52,6 +56,7 @@ export const useScopedHotkeys = (
{
enableOnContentEditable,
enableOnFormTags,
ignoreModifiers,
},
dependencies,
);

View File

@ -1,17 +1,11 @@
msgid ""
msgstr ""
"POT-Creation-Date: 2025-01-28 21:09+0100\n"
"POT-Creation-Date: 2025-01-29 18:14+0100\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n"
"Language: en\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: \n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: \n"
#: src/modules/view/standard-objects/view-field.workspace-entity.ts:32
msgid "(System) View Fields"
@ -916,10 +910,6 @@ msgstr "Ideal Customer Profile: Indicates whether the company is the most suita
msgid "If the event is related to a particular object"
msgstr "If the event is related to a particular object"
#: src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts:94
#~ msgid "inked Object Metadata Id"
#~ msgstr "inked Object Metadata Id"
#: src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts:48
#: src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts:49
msgid "Is canceled"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -66,8 +66,7 @@ const StyledMenuItemCommandContainer = styled.div<{ isSelected?: boolean }>`
export type MenuItemCommandProps = {
LeftIcon?: IconComponent;
text: string;
firstHotKey?: string;
secondHotKey?: string;
hotKeys?: string[];
className?: string;
isSelected?: boolean;
onClick?: () => void;
@ -77,8 +76,7 @@ export type MenuItemCommandProps = {
export const MenuItemCommand = ({
LeftIcon,
text,
firstHotKey,
secondHotKey,
hotKeys,
className,
isSelected,
onClick,
@ -102,12 +100,7 @@ export const MenuItemCommand = ({
<StyledMenuItemLabelText>{text}</StyledMenuItemLabelText>
{RightComponent}
</StyledMenuItemLeftContent>
{!isMobile && (
<MenuItemCommandHotKeys
firstHotKey={firstHotKey}
secondHotKey={secondHotKey}
/>
)}
{!isMobile && <MenuItemCommandHotKeys hotKeys={hotKeys} />}
</StyledMenuItemCommandContainer>
);
};

View File

@ -34,27 +34,24 @@ const StyledCommandKey = styled.div`
`;
export type MenuItemCommandHotKeysProps = {
firstHotKey?: string;
hotKeys?: string[];
joinLabel?: string;
secondHotKey?: string;
};
export const MenuItemCommandHotKeys = ({
firstHotKey,
secondHotKey,
hotKeys,
joinLabel = 'then',
}: MenuItemCommandHotKeysProps) => {
return (
<StyledCommandText>
{firstHotKey && (
{hotKeys && (
<StyledCommandTextContainer>
<StyledCommandKey>{firstHotKey}</StyledCommandKey>
{secondHotKey && (
{hotKeys.map((hotKey, index) => (
<>
{joinLabel}
<StyledCommandKey>{secondHotKey}</StyledCommandKey>
<StyledCommandKey key={index}>{hotKey}</StyledCommandKey>
{index < hotKeys.length - 1 && joinLabel}
</>
)}
))}
</StyledCommandTextContainer>
)}
</StyledCommandText>

View File

@ -20,15 +20,13 @@ type Story = StoryObj<typeof MenuItemCommand>;
export const Default: Story = {
args: {
text: 'First option',
firstHotKey: '⌘',
secondHotKey: '1',
hotKeys: ['⌘', '1'],
},
render: (props) => (
<MenuItemCommand
LeftIcon={props.LeftIcon}
text={props.text}
firstHotKey={props.firstHotKey}
secondHotKey={props.secondHotKey}
hotKeys={props.hotKeys}
className={props.className}
onClick={props.onClick}
isSelected={false}
@ -40,8 +38,7 @@ export const Default: Story = {
export const Catalog: CatalogStory<Story, typeof MenuItemCommand> = {
args: {
text: 'Menu item',
firstHotKey: '⌘',
secondHotKey: '1',
hotKeys: ['⌘', '1'],
},
argTypes: {
className: { control: false },
@ -85,8 +82,7 @@ export const Catalog: CatalogStory<Story, typeof MenuItemCommand> = {
<MenuItemCommand
LeftIcon={props.LeftIcon}
text={props.text}
firstHotKey={props.firstHotKey}
secondHotKey={props.secondHotKey}
hotKeys={props.hotKeys}
className={props.className}
onClick={props.onClick}
isSelected={false}