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 = { type RegisterRecordActionEffectProps = {
action: ActionMenuEntry & { action: ActionMenuEntry & {
actionHook: ActionHook; useAction: ActionHook;
}; };
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
}; };
@ -17,7 +17,7 @@ export const RegisterRecordActionEffect = ({
action, action,
objectMetadataItem, objectMetadataItem,
}: RegisterRecordActionEffectProps) => { }: RegisterRecordActionEffectProps) => {
const { shouldBeRegistered, onClick, ConfirmationModal } = action.actionHook({ const { shouldBeRegistered, onClick, ConfirmationModal } = action.useAction({
objectMetadataItem, objectMetadataItem,
}); });

View File

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

View File

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

View File

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

View File

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

View File

@ -31,7 +31,7 @@ import {
export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record< export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
string, string,
ActionMenuEntry & { ActionMenuEntry & {
actionHook: ActionHook; useAction: ActionHook;
} }
> = { > = {
useAsDraftWorkflowVersionSingleRecord: { useAsDraftWorkflowVersionSingleRecord: {
@ -47,7 +47,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
ActionViewType.SHOW_PAGE, ActionViewType.SHOW_PAGE,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
], ],
actionHook: useUseAsDraftWorkflowVersionSingleRecordAction, useAction: useUseAsDraftWorkflowVersionSingleRecordAction,
}, },
seeWorkflowRunsSingleRecord: { seeWorkflowRunsSingleRecord: {
key: WorkflowVersionSingleRecordActionKeys.SEE_RUNS, key: WorkflowVersionSingleRecordActionKeys.SEE_RUNS,
@ -61,7 +61,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
ActionViewType.SHOW_PAGE, ActionViewType.SHOW_PAGE,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
], ],
actionHook: useSeeRunsWorkflowVersionSingleRecordAction, useAction: useSeeRunsWorkflowVersionSingleRecordAction,
}, },
seeWorkflowVersionsHistorySingleRecord: { seeWorkflowVersionsHistorySingleRecord: {
key: WorkflowVersionSingleRecordActionKeys.SEE_VERSIONS, key: WorkflowVersionSingleRecordActionKeys.SEE_VERSIONS,
@ -75,7 +75,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
ActionViewType.SHOW_PAGE, ActionViewType.SHOW_PAGE,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
], ],
actionHook: useSeeVersionsWorkflowVersionSingleRecordAction, useAction: useSeeVersionsWorkflowVersionSingleRecordAction,
}, },
navigateToPreviousRecord: { navigateToPreviousRecord: {
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
@ -86,7 +86,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
position: 4, position: 4,
Icon: IconChevronUp, Icon: IconChevronUp,
availableOn: [ActionViewType.SHOW_PAGE], availableOn: [ActionViewType.SHOW_PAGE],
actionHook: useNavigateToPreviousRecordSingleRecordAction, useAction: useNavigateToPreviousRecordSingleRecordAction,
}, },
navigateToNextRecord: { navigateToNextRecord: {
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
@ -97,7 +97,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
position: 5, position: 5,
Icon: IconChevronDown, Icon: IconChevronDown,
availableOn: [ActionViewType.SHOW_PAGE], availableOn: [ActionViewType.SHOW_PAGE],
actionHook: useNavigateToNextRecordSingleRecordAction, useAction: useNavigateToNextRecordSingleRecordAction,
}, },
addToFavoritesSingleRecord: { addToFavoritesSingleRecord: {
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
@ -112,7 +112,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionViewType.SHOW_PAGE, ActionViewType.SHOW_PAGE,
], ],
actionHook: useAddToFavoritesSingleRecordAction, useAction: useAddToFavoritesSingleRecordAction,
}, },
removeFromFavoritesSingleRecord: { removeFromFavoritesSingleRecord: {
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
@ -127,7 +127,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionViewType.SHOW_PAGE, ActionViewType.SHOW_PAGE,
], ],
actionHook: useRemoveFromFavoritesSingleRecordAction, useAction: useRemoveFromFavoritesSingleRecordAction,
}, },
exportMultipleRecords: { exportMultipleRecords: {
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
@ -140,7 +140,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
accent: 'default', accent: 'default',
isPinned: false, isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION], availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
actionHook: useExportMultipleRecordsAction, useAction: useExportMultipleRecordsAction,
}, },
exportView: { exportView: {
type: ActionMenuEntryType.Standard, type: ActionMenuEntryType.Standard,
@ -153,6 +153,6 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
accent: 'default', accent: 'default',
isPinned: false, isPinned: false,
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION], 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 { import {
ActionMenuEntryScope, ActionMenuEntryScope,
ActionMenuEntryType, ActionMenuEntryType,
@ -11,55 +10,49 @@ import { capitalize } from 'twenty-shared';
import { IconSettingsAutomation, isDefined } from 'twenty-ui'; import { IconSettingsAutomation, isDefined } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql'; import { FeatureFlagKey } from '~/generated/graphql';
export const useWorkflowRunActions = () => { export const useRunWorkflowActions = () => {
const isWorkflowEnabled = useIsFeatureEnabled( const isWorkflowEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsWorkflowEnabled, FeatureFlagKey.IsWorkflowEnabled,
); );
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const { records: activeWorkflowVersions } = useAllActiveWorkflowVersions({ const { records: activeWorkflowVersions } = useAllActiveWorkflowVersions({
triggerType: 'MANUAL', triggerType: 'MANUAL',
}); });
const { runWorkflowVersion } = useRunWorkflowVersion(); const { runWorkflowVersion } = useRunWorkflowVersion();
const addWorkflowRunActions = () => { if (!isWorkflowEnabled) {
if (!isWorkflowEnabled) { return { runWorkflowActions: [] };
return; }
}
for (const [ const runWorkflowActions = activeWorkflowVersions
index, .map((activeWorkflowVersion, index) => {
activeWorkflowVersion,
] of activeWorkflowVersions.entries()) {
if (!isDefined(activeWorkflowVersion.workflow)) { if (!isDefined(activeWorkflowVersion.workflow)) {
continue; return undefined;
} }
const name = capitalize(activeWorkflowVersion.workflow.name); const name = capitalize(activeWorkflowVersion.workflow.name);
addActionMenuEntry({ return {
type: ActionMenuEntryType.WorkflowRun, type: ActionMenuEntryType.WorkflowRun,
key: `workflow-run-${activeWorkflowVersion.id}`, key: `workflow-run-${activeWorkflowVersion.id}`,
scope: ActionMenuEntryScope.Global, scope: ActionMenuEntryScope.Global,
label: name, label: name,
position: index, position: index,
Icon: IconSettingsAutomation, Icon: IconSettingsAutomation,
onClick: async () => { useAction: () => {
await runWorkflowVersion({ return {
workflowVersionId: activeWorkflowVersion.id, shouldBeRegistered: true,
}); onClick: async () => {
await runWorkflowVersion({
workflowVersionId: activeWorkflowVersion.id,
});
},
};
}, },
}); };
} })
}; .filter(isDefined);
const removeWorkflowRunActions = () => { return { runWorkflowActions };
for (const activeWorkflowVersion of activeWorkflowVersions) {
removeActionMenuEntry(`workflow-run-${activeWorkflowVersion.id}`);
}
};
return { addWorkflowRunActions, removeWorkflowRunActions };
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,92 +1,37 @@
import { CommandGroup } from '@/command-menu/components/CommandGroup'; import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuDefaultSelectionEffect } from '@/command-menu/components/CommandMenuDefaultSelectionEffect'; import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { ResetContextToSelectionCommandButton } from '@/command-menu/components/ResetContextToSelectionCommandButton'; import { ResetContextToSelectionCommandButton } from '@/command-menu/components/ResetContextToSelectionCommandButton';
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight'; import { RESET_CONTEXT_TO_SELECTION } from '@/command-menu/constants/ResetContextToSelection';
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
import { useCommandMenuOnItemClick } from '@/command-menu/hooks/useCommandMenuOnItemClick';
import { useMatchingCommandMenuCommands } from '@/command-menu/hooks/useMatchingCommandMenuCommands'; import { useMatchingCommandMenuCommands } from '@/command-menu/hooks/useMatchingCommandMenuCommands';
import { useResetPreviousCommandMenuContext } from '@/command-menu/hooks/useResetPreviousCommandMenuContext';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { Command } from '@/command-menu/types/Command'; import { Command } from '@/command-menu/types/Command';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; 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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
const MOBILE_NAVIGATION_BAR_HEIGHT = 64; export type CommandGroupConfig = {
type CommandGroupConfig = {
heading: string; heading: string;
items?: Command[]; 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 = () => { export const CommandMenu = () => {
const { t } = useLingui(); const { t } = useLingui();
const { onItemClick } = useCommandMenuOnItemClick();
const { resetPreviousCommandMenuContext } =
useResetPreviousCommandMenuContext();
const commandMenuSearch = useRecoilValue(commandMenuSearchState); const commandMenuSearch = useRecoilValue(commandMenuSearchState);
const isMobile = useIsMobile();
const { const {
isNoResults, noResults,
isLoading,
copilotCommands, copilotCommands,
matchingStandardActionRecordSelectionCommands, matchingStandardActionRecordSelectionCommands,
matchingStandardActionObjectCommands,
matchingWorkflowRunRecordSelectionCommands, matchingWorkflowRunRecordSelectionCommands,
matchingStandardActionGlobalCommands, matchingStandardActionGlobalCommands,
matchingWorkflowRunGlobalCommands, matchingWorkflowRunGlobalCommands,
matchingNavigateCommand, matchingNavigateCommands,
peopleCommands,
companyCommands,
opportunityCommands,
noteCommands,
tasksCommands,
customObjectCommands,
} = useMatchingCommandMenuCommands({ } = useMatchingCommandMenuCommands({
commandMenuSearch, commandMenuSearch,
}); });
@ -94,16 +39,11 @@ export const CommandMenu = () => {
const selectableItems: Command[] = copilotCommands const selectableItems: Command[] = copilotCommands
.concat( .concat(
matchingStandardActionRecordSelectionCommands, matchingStandardActionRecordSelectionCommands,
matchingStandardActionObjectCommands,
matchingWorkflowRunRecordSelectionCommands, matchingWorkflowRunRecordSelectionCommands,
matchingStandardActionGlobalCommands, matchingStandardActionGlobalCommands,
matchingWorkflowRunGlobalCommands, matchingWorkflowRunGlobalCommands,
matchingNavigateCommand, matchingNavigateCommands,
peopleCommands,
companyCommands,
opportunityCommands,
noteCommands,
tasksCommands,
customObjectCommands,
) )
.filter(isDefined); .filter(isDefined);
@ -115,7 +55,7 @@ export const CommandMenu = () => {
const selectableItemIds = selectableItems.map((item) => item.id); const selectableItemIds = selectableItems.map((item) => item.id);
if (isNonEmptyString(previousContextStoreCurrentObjectMetadataId)) { if (isNonEmptyString(previousContextStoreCurrentObjectMetadataId)) {
selectableItemIds.unshift('reset-context-to-selection'); selectableItemIds.unshift(RESET_CONTEXT_TO_SELECTION);
} }
const commandGroups: CommandGroupConfig[] = [ const commandGroups: CommandGroupConfig[] = [
@ -125,130 +65,35 @@ export const CommandMenu = () => {
}, },
{ {
heading: t`Record Selection`, heading: t`Record Selection`,
items: matchingStandardActionRecordSelectionCommands, items: matchingStandardActionRecordSelectionCommands.concat(
matchingWorkflowRunRecordSelectionCommands,
),
}, },
{ {
heading: t`Workflow Record Selection`, heading: t`Object`,
items: matchingWorkflowRunRecordSelectionCommands, items: matchingStandardActionObjectCommands,
}, },
{ {
heading: t`View`, heading: t`Global`,
items: matchingStandardActionGlobalCommands, items: matchingStandardActionGlobalCommands
}, .concat(matchingNavigateCommands)
{ .concat(matchingWorkflowRunGlobalCommands),
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,
}, },
]; ];
return ( return (
<> <CommandMenuList
<CommandMenuDefaultSelectionEffect commandGroups={commandGroups}
selectableItemIds={selectableItemIds} selectableItemIds={selectableItemIds}
/> noResults={noResults}
>
<StyledList> {isNonEmptyString(previousContextStoreCurrentObjectMetadataId) && (
<ScrollWrapper <CommandGroup heading={t`Context`}>
contextProviderName="commandMenu" <SelectableItem itemId={RESET_CONTEXT_TO_SELECTION}>
componentInstanceId={`scroll-wrapper-command-menu`} <ResetContextToSelectionCommandButton />
> </SelectableItem>
<StyledInnerList isMobile={isMobile}> </CommandGroup>
<SelectableList )}
selectableListId="command-menu-list" </CommandMenuList>
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>
</>
); );
}; };

View File

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

View File

@ -12,8 +12,7 @@ export type CommandMenuItemProps = {
id: string; id: string;
onClick?: () => void; onClick?: () => void;
Icon?: IconComponent; Icon?: IconComponent;
firstHotKey?: string; hotKeys?: string[];
secondHotKey?: string;
shouldCloseCommandMenuOnClick?: boolean; shouldCloseCommandMenuOnClick?: boolean;
RightComponent?: ReactNode; RightComponent?: ReactNode;
}; };
@ -24,8 +23,7 @@ export const CommandMenuItem = ({
id, id,
onClick, onClick,
Icon, Icon,
firstHotKey, hotKeys,
secondHotKey,
shouldCloseCommandMenuOnClick, shouldCloseCommandMenuOnClick,
RightComponent, RightComponent,
}: CommandMenuItemProps) => { }: CommandMenuItemProps) => {
@ -42,8 +40,7 @@ export const CommandMenuItem = ({
<MenuItemCommand <MenuItemCommand
LeftIcon={Icon} LeftIcon={Icon}
text={label} text={label}
firstHotKey={firstHotKey} hotKeys={hotKeys}
secondHotKey={secondHotKey}
onClick={() => onClick={() =>
onItemClick({ onItemClick({
shouldCloseCommandMenuOnClick, 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 { CommandMenuContextChip } from '@/command-menu/components/CommandMenuContextChip';
import { CommandMenuContextRecordChip } from '@/command-menu/components/CommandMenuContextRecordChip'; 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_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding'; import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle'; import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
@ -95,11 +95,12 @@ export const CommandMenuTopBar = () => {
return ( return (
<StyledInputContainer> <StyledInputContainer>
<StyledContentContainer> <StyledContentContainer>
{isDefined(contextStoreCurrentObjectMetadataId) && ( {commandMenuPage !== CommandMenuPages.SearchRecords &&
<CommandMenuContextRecordChip isDefined(contextStoreCurrentObjectMetadataId) && (
objectMetadataItemId={contextStoreCurrentObjectMetadataId} <CommandMenuContextRecordChip
/> objectMetadataItemId={contextStoreCurrentObjectMetadataId}
)} />
)}
{isDefined(Icon) && ( {isDefined(Icon) && (
<CommandMenuContextChip <CommandMenuContextChip
Icons={[<Icon size={theme.icon.size.sm} />]} Icons={[<Icon size={theme.icon.size.sm} />]}
@ -107,7 +108,8 @@ export const CommandMenuTopBar = () => {
/> />
)} )}
{commandMenuPage === CommandMenuPages.Root && ( {(commandMenuPage === CommandMenuPages.Root ||
commandMenuPage === CommandMenuPages.SearchRecords) && (
<StyledInput <StyledInput
autoFocus autoFocus
value={commandMenuSearch} value={commandMenuSearch}

View File

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

View File

@ -85,42 +85,43 @@ export const DefaultWithoutSearch: Story = {
play: async () => { play: async () => {
const canvas = within(document.body); const canvas = within(document.body);
expect(await canvas.findByText('Go to People')).toBeInTheDocument(); expect(await canvas.findByText('Go to People')).toBeVisible();
expect(await canvas.findByText('Go to Companies')).toBeInTheDocument(); expect(await canvas.findByText('Go to Companies')).toBeVisible();
expect(await canvas.findByText('Go to Opportunities')).toBeInTheDocument(); expect(await canvas.findByText('Go to Opportunities')).toBeVisible();
expect(await canvas.findByText('Go to Settings')).toBeInTheDocument(); expect(await canvas.findByText('Go to Settings')).toBeVisible();
expect(await canvas.findByText('Go to Tasks')).toBeInTheDocument(); expect(await canvas.findByText('Go to Tasks')).toBeVisible();
}, },
}; };
export const MatchingPersonCompanyActivityCreateNavigate: 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, '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 = {
play: async () => { play: async () => {
const canvas = within(document.body); const canvas = within(document.body);
const searchInput = await canvas.findByPlaceholderText('Type anything'); const searchInput = await canvas.findByPlaceholderText('Type anything');
await sleep(openTimeout); await sleep(openTimeout);
await userEvent.type(searchInput, 'ta'); 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 () => { play: async () => {
const canvas = within(document.body); const canvas = within(document.body);
const searchInput = await canvas.findByPlaceholderText('Type anything'); const searchInput = await canvas.findByPlaceholderText('Type anything');
await sleep(openTimeout); await sleep(openTimeout);
await userEvent.type(searchInput, 'alex'); await userEvent.type(searchInput, 'gp');
expect(await canvas.findByText('Sylvie Palmer')).toBeInTheDocument(); 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', label: 'Go to People',
type: CommandType.Navigate, type: CommandType.Navigate,
firstHotKey: 'G', hotKeys: ['G', 'P'],
secondHotKey: 'P',
Icon: IconUser, Icon: IconUser,
shouldCloseCommandMenuOnClick: true, shouldCloseCommandMenuOnClick: true,
}, },
@ -33,8 +32,7 @@ export const COMMAND_MENU_NAVIGATE_COMMANDS: { [key: string]: Command } = {
}), }),
label: 'Go to Companies', label: 'Go to Companies',
type: CommandType.Navigate, type: CommandType.Navigate,
firstHotKey: 'G', hotKeys: ['G', 'C'],
secondHotKey: 'C',
Icon: IconBuildingSkyscraper, Icon: IconBuildingSkyscraper,
shouldCloseCommandMenuOnClick: true, shouldCloseCommandMenuOnClick: true,
}, },
@ -45,8 +43,7 @@ export const COMMAND_MENU_NAVIGATE_COMMANDS: { [key: string]: Command } = {
}), }),
label: 'Go to Opportunities', label: 'Go to Opportunities',
type: CommandType.Navigate, type: CommandType.Navigate,
firstHotKey: 'G', hotKeys: ['G', 'O'],
secondHotKey: 'O',
Icon: IconTargetArrow, Icon: IconTargetArrow,
shouldCloseCommandMenuOnClick: true, shouldCloseCommandMenuOnClick: true,
}, },
@ -55,8 +52,7 @@ export const COMMAND_MENU_NAVIGATE_COMMANDS: { [key: string]: Command } = {
to: getSettingsPath(SettingsPath.ProfilePage), to: getSettingsPath(SettingsPath.ProfilePage),
label: 'Go to Settings', label: 'Go to Settings',
type: CommandType.Navigate, type: CommandType.Navigate,
firstHotKey: 'G', hotKeys: ['G', 'S'],
secondHotKey: 'S',
Icon: IconSettings, Icon: IconSettings,
shouldCloseCommandMenuOnClick: true, shouldCloseCommandMenuOnClick: true,
}, },
@ -67,8 +63,7 @@ export const COMMAND_MENU_NAVIGATE_COMMANDS: { [key: string]: Command } = {
}), }),
label: 'Go to Tasks', label: 'Go to Tasks',
type: CommandType.Navigate, type: CommandType.Navigate,
firstHotKey: 'G', hotKeys: ['G', 'T'],
secondHotKey: 'T',
Icon: IconCheckbox, Icon: IconCheckbox,
shouldCloseCommandMenuOnClick: true, 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 { RightDrawerAIChat } from '@/activities/copilot/right-drawer/components/RightDrawerAIChat';
import { RightDrawerEmailThread } from '@/activities/emails/right-drawer/components/RightDrawerEmailThread'; import { RightDrawerEmailThread } from '@/activities/emails/right-drawer/components/RightDrawerEmailThread';
import { CommandMenu } from '@/command-menu/components/CommandMenu'; 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 { RightDrawerRecord } from '@/object-record/record-right-drawer/components/RightDrawerRecord';
import { RightDrawerWorkflowEditStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowEditStep'; import { RightDrawerWorkflowEditStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowEditStep';
import { RightDrawerWorkflowViewStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowViewStep'; import { RightDrawerWorkflowViewStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowViewStep';
@ -28,4 +29,5 @@ export const COMMAND_MENU_PAGES_CONFIG = new Map<
], ],
[CommandMenuPages.WorkflowStepEdit, <RightDrawerWorkflowEditStep />], [CommandMenuPages.WorkflowStepEdit, <RightDrawerWorkflowEditStep />],
[CommandMenuPages.WorkflowStepView, <RightDrawerWorkflowViewStep />], [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 { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
import { useCopyContextStoreStates } from '@/command-menu/hooks/useCopyContextStoreAndActionMenuStates'; import { useCopyContextStoreStates } from '@/command-menu/hooks/useCopyContextStoreAndActionMenuStates';
import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates'; import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle'; import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; 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 { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState'; import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent'; import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
import { IconSearch } from 'twenty-ui';
import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState'; import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState';
export const useCommandMenu = () => { export const useCommandMenu = () => {
@ -71,6 +72,7 @@ export const useCommandMenu = () => {
Icon: undefined, Icon: undefined,
}); });
set(isCommandMenuOpenedState, false); set(isCommandMenuOpenedState, false);
set(commandMenuSearchState, '');
resetSelectedItem(); resetSelectedItem();
goBackToPreviousHotkeyScope(); goBackToPreviousHotkeyScope();
@ -110,6 +112,20 @@ export const useCommandMenu = () => {
[openCommandMenu], [openCommandMenu],
); );
const openRecordsSearchPage = useRecoilCallback(
({ set }) => {
return () => {
set(commandMenuPageState, CommandMenuPages.SearchRecords);
set(commandMenuPageInfoState, {
title: 'Search',
Icon: IconSearch,
});
openCommandMenu();
};
},
[openCommandMenu],
);
const setGlobalCommandMenuContext = useRecoilCallback( const setGlobalCommandMenuContext = useRecoilCallback(
({ set }) => { ({ set }) => {
return () => { return () => {
@ -161,6 +177,7 @@ export const useCommandMenu = () => {
return { return {
openCommandMenu, openCommandMenu,
closeCommandMenu, closeCommandMenu,
openRecordsSearchPage,
openRecordInCommandMenu, openRecordInCommandMenu,
toggleCommandMenu, toggleCommandMenu,
setGlobalCommandMenuContext, setGlobalCommandMenuContext,

View File

@ -5,43 +5,25 @@ import {
} from '@/action-menu/types/ActionMenuEntry'; } from '@/action-menu/types/ActionMenuEntry';
import { useOpenCopilotRightDrawer } from '@/activities/copilot/right-drawer/hooks/useOpenCopilotRightDrawer'; import { useOpenCopilotRightDrawer } from '@/activities/copilot/right-drawer/hooks/useOpenCopilotRightDrawer';
import { copilotQueryState } from '@/activities/copilot/right-drawer/states/copilotQueryState'; 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 { COMMAND_MENU_NAVIGATE_COMMANDS } from '@/command-menu/constants/CommandMenuNavigateCommands';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { import {
Command, Command,
CommandScope, CommandScope,
CommandType, CommandType,
} from '@/command-menu/types/Command'; } 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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import isEmpty from 'lodash.isempty';
import { useMemo } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
import { Avatar, IconCheckbox, IconNotes, IconSparkles } from 'twenty-ui'; import { IconSparkles } from 'twenty-ui';
import { useDebounce } from 'use-debounce'; import { useDebounce } from 'use-debounce';
import { FeatureFlagKey } from '~/generated/graphql'; import { FeatureFlagKey } from '~/generated/graphql';
import { getLogoUrlFromDomainName } from '~/utils';
export const useCommandMenuCommands = () => { export const useCommandMenuCommands = () => {
const actionMenuEntries = useRecoilComponentValueV2( const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentSelector, actionMenuEntriesComponentSelector,
); );
const openActivityRightDrawer = useOpenActivityRightDrawer({
objectNameSingular: CoreObjectNameSingular.Note,
});
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
const commandMenuSearch = useRecoilValue(commandMenuSearchState); const commandMenuSearch = useRecoilValue(commandMenuSearchState);
const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300); // 200ms - 500ms const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300); // 200ms - 500ms
@ -78,6 +60,23 @@ export const useCommandMenuCommands = () => {
onCommandClick: actionMenuEntry.onClick, onCommandClick: actionMenuEntry.onClick,
type: CommandType.StandardAction, type: CommandType.StandardAction,
scope: CommandScope.RecordSelection, 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 const actionGlobalCommands: Command[] = actionMenuEntries
@ -93,6 +92,7 @@ export const useCommandMenuCommands = () => {
onCommandClick: actionMenuEntry.onClick, onCommandClick: actionMenuEntry.onClick,
type: CommandType.StandardAction, type: CommandType.StandardAction,
scope: CommandScope.Global, scope: CommandScope.Global,
hotKeys: actionMenuEntry.hotKeys,
})); }));
const workflowRunRecordSelectionCommands: Command[] = actionMenuEntries const workflowRunRecordSelectionCommands: Command[] = actionMenuEntries
@ -108,6 +108,7 @@ export const useCommandMenuCommands = () => {
onCommandClick: actionMenuEntry.onClick, onCommandClick: actionMenuEntry.onClick,
type: CommandType.WorkflowRun, type: CommandType.WorkflowRun,
scope: CommandScope.RecordSelection, scope: CommandScope.RecordSelection,
hotKeys: actionMenuEntry.hotKeys,
})); }));
const workflowRunGlobalCommands: Command[] = actionMenuEntries const workflowRunGlobalCommands: Command[] = actionMenuEntries
@ -123,193 +124,16 @@ export const useCommandMenuCommands = () => {
onCommandClick: actionMenuEntry.onClick, onCommandClick: actionMenuEntry.onClick,
type: CommandType.WorkflowRun, type: CommandType.WorkflowRun,
scope: CommandScope.Global, 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 { return {
copilotCommands, copilotCommands,
navigateCommands, navigateCommands,
actionRecordSelectionCommands, actionRecordSelectionCommands,
actionGlobalCommands, actionGlobalCommands,
actionObjectCommands,
workflowRunRecordSelectionCommands, workflowRunRecordSelectionCommands,
workflowRunGlobalCommands, 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 { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu'; import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
@ -12,8 +12,12 @@ import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
export const useCommandMenuHotKeys = () => { export const useCommandMenuHotKeys = () => {
const { closeCommandMenu, toggleCommandMenu, setGlobalCommandMenuContext } = const {
useCommandMenu(); closeCommandMenu,
openRecordsSearchPage,
toggleCommandMenu,
setGlobalCommandMenuContext,
} = useCommandMenu();
const commandMenuSearch = useRecoilValue(commandMenuSearchState); const commandMenuSearch = useRecoilValue(commandMenuSearchState);
@ -36,6 +40,18 @@ export const useCommandMenuHotKeys = () => {
[toggleCommandMenu], [toggleCommandMenu],
); );
useScopedHotkeys(
['/'],
() => {
openRecordsSearchPage();
},
AppHotkeyScope.KeyboardShortcutMenu,
[openRecordsSearchPage],
{
ignoreModifiers: true,
},
);
useScopedHotkeys( useScopedHotkeys(
[Key.Escape], [Key.Escape],
() => { () => {

View File

@ -10,9 +10,10 @@ export const useMatchCommands = ({
const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300); // 200ms - 500ms const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300); // 200ms - 500ms
const checkInShortcuts = (cmd: Command, search: string) => { const checkInShortcuts = (cmd: Command, search: string) => {
return (cmd.firstHotKey + (cmd.secondHotKey ?? '')) const concatenatedString = cmd.hotKeys?.join('') ?? '';
return concatenatedString
.toLowerCase() .toLowerCase()
.includes(search.toLowerCase()); .includes(search.toLowerCase().trim());
}; };
const checkInLabels = (cmd: Command, search: string) => { const checkInLabels = (cmd: Command, search: string) => {

View File

@ -12,24 +12,21 @@ export const useMatchingCommandMenuCommands = ({
copilotCommands, copilotCommands,
navigateCommands, navigateCommands,
actionRecordSelectionCommands, actionRecordSelectionCommands,
actionObjectCommands,
actionGlobalCommands, actionGlobalCommands,
workflowRunRecordSelectionCommands, workflowRunRecordSelectionCommands,
workflowRunGlobalCommands, workflowRunGlobalCommands,
peopleCommands,
companyCommands,
opportunityCommands,
noteCommands,
tasksCommands,
customObjectCommands,
isLoading,
} = useCommandMenuCommands(); } = useCommandMenuCommands();
const matchingNavigateCommand = matchCommands(navigateCommands); const matchingNavigateCommands = matchCommands(navigateCommands);
const matchingStandardActionRecordSelectionCommands = matchCommands( const matchingStandardActionRecordSelectionCommands = matchCommands(
actionRecordSelectionCommands, actionRecordSelectionCommands,
); );
const matchingStandardActionObjectCommands =
matchCommands(actionObjectCommands);
const matchingStandardActionGlobalCommands = const matchingStandardActionGlobalCommands =
matchCommands(actionGlobalCommands); matchCommands(actionGlobalCommands);
@ -41,33 +38,22 @@ export const useMatchingCommandMenuCommands = ({
workflowRunGlobalCommands, workflowRunGlobalCommands,
); );
const isNoResults = const noResults =
!matchingStandardActionRecordSelectionCommands.length && !matchingStandardActionRecordSelectionCommands.length &&
!matchingWorkflowRunRecordSelectionCommands.length && !matchingWorkflowRunRecordSelectionCommands.length &&
!matchingStandardActionGlobalCommands.length && !matchingStandardActionGlobalCommands.length &&
!matchingWorkflowRunGlobalCommands.length && !matchingWorkflowRunGlobalCommands.length &&
!matchingNavigateCommand.length && !matchingStandardActionObjectCommands.length &&
!peopleCommands?.length && !matchingNavigateCommands.length;
!companyCommands?.length &&
!opportunityCommands?.length &&
!noteCommands?.length &&
!tasksCommands?.length &&
!customObjectCommands?.length;
return { return {
isNoResults, noResults,
isLoading,
copilotCommands, copilotCommands,
matchingStandardActionRecordSelectionCommands, matchingStandardActionRecordSelectionCommands,
matchingStandardActionObjectCommands,
matchingWorkflowRunRecordSelectionCommands, matchingWorkflowRunRecordSelectionCommands,
matchingStandardActionGlobalCommands, matchingStandardActionGlobalCommands,
matchingWorkflowRunGlobalCommands, matchingWorkflowRunGlobalCommands,
matchingNavigateCommand, matchingNavigateCommands,
peopleCommands,
companyCommands,
opportunityCommands,
noteCommands,
tasksCommands,
customObjectCommands,
}; };
}; };

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'; import { createState } from '@ui/utilities/state/utils/createState';
export const commandMenuPageState = createState<CommandMenuPages>({ export const commandMenuPageState = createState<CommandMenuPages>({

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@ type NavigationBarItemName = 'main' | 'search' | 'tasks' | 'settings';
export const MobileNavigationBar = () => { export const MobileNavigationBar = () => {
const [isCommandMenuOpened] = useRecoilState(isCommandMenuOpenedState); const [isCommandMenuOpened] = useRecoilState(isCommandMenuOpenedState);
const { closeCommandMenu, openCommandMenu } = useCommandMenu(); const { closeCommandMenu, openRecordsSearchPage } = useCommandMenu();
const isSettingsPage = useIsSettingsPage(); const isSettingsPage = useIsSettingsPage();
const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] = const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] =
useRecoilState(isNavigationDrawerExpandedState); useRecoilState(isNavigationDrawerExpandedState);
@ -53,12 +53,7 @@ export const MobileNavigationBar = () => {
{ {
name: 'search', name: 'search',
Icon: IconSearch, Icon: IconSearch,
onClick: () => { onClick: openRecordsSearchPage,
if (!isCommandMenuOpened) {
openCommandMenu();
}
setIsNavigationDrawerExpanded(false);
},
}, },
{ {
name: 'settings', 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'; import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
export const mapRightDrawerPageToCommandMenuPage = ( export const mapRightDrawerPageToCommandMenuPage = (

View File

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

View File

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

View File

@ -1,17 +1,11 @@
msgid "" msgid ""
msgstr "" 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" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: @lingui/cli\n" "X-Generator: @lingui/cli\n"
"Language: en\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 #: src/modules/view/standard-objects/view-field.workspace-entity.ts:32
msgid "(System) View Fields" 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" msgid "If the event is related to a particular object"
msgstr "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:48
#: src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts:49 #: src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts:49
msgid "Is canceled" 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 = { export type MenuItemCommandProps = {
LeftIcon?: IconComponent; LeftIcon?: IconComponent;
text: string; text: string;
firstHotKey?: string; hotKeys?: string[];
secondHotKey?: string;
className?: string; className?: string;
isSelected?: boolean; isSelected?: boolean;
onClick?: () => void; onClick?: () => void;
@ -77,8 +76,7 @@ export type MenuItemCommandProps = {
export const MenuItemCommand = ({ export const MenuItemCommand = ({
LeftIcon, LeftIcon,
text, text,
firstHotKey, hotKeys,
secondHotKey,
className, className,
isSelected, isSelected,
onClick, onClick,
@ -102,12 +100,7 @@ export const MenuItemCommand = ({
<StyledMenuItemLabelText>{text}</StyledMenuItemLabelText> <StyledMenuItemLabelText>{text}</StyledMenuItemLabelText>
{RightComponent} {RightComponent}
</StyledMenuItemLeftContent> </StyledMenuItemLeftContent>
{!isMobile && ( {!isMobile && <MenuItemCommandHotKeys hotKeys={hotKeys} />}
<MenuItemCommandHotKeys
firstHotKey={firstHotKey}
secondHotKey={secondHotKey}
/>
)}
</StyledMenuItemCommandContainer> </StyledMenuItemCommandContainer>
); );
}; };

View File

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

View File

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