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
@@ -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,
});
@@ -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,
},
};
@@ -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,
},
};
@@ -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,
},
};
@@ -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,
},
};
@@ -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,
},
};
@@ -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} />
))}
</>
);
};
@@ -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;
};
@@ -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;
};
@@ -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} />
))}
</>
);
};
@@ -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: ['/'],
},
};
@@ -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 };
};
@@ -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,
};
};
@@ -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 };
};
@@ -0,0 +1,3 @@
export enum RecordAgnosticActionsKey {
SEARCH_RECORDS = 'search-records',
}
@@ -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',
@@ -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>
)}
</>
@@ -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>
)}
</>
@@ -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>
)}
</>
@@ -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[];
};
@@ -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>
);
};
@@ -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
@@ -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,
@@ -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>
</>
);
};
@@ -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}
@@ -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={
@@ -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();
},
};
@@ -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,
},
@@ -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 />],
]);
@@ -0,0 +1 @@
export const RESET_CONTEXT_TO_SELECTION = 'reset-context-to-selection';
@@ -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: () => {},
};
};
@@ -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}
/>
);
};
@@ -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>({
@@ -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;
};
@@ -8,4 +8,5 @@ export enum CommandMenuPages {
WorkflowStepSelectAction = 'workflow-step-select-action',
WorkflowStepView = 'workflow-step-view',
WorkflowStepEdit = 'workflow-step-edit',
SearchRecords = 'search-records',
}
@@ -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`}
@@ -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',
@@ -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 = (
@@ -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>
)}
@@ -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,
);
@@ -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 it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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>
);
};
@@ -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>
@@ -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}