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:
@ -8,7 +8,7 @@ import { useContext, useEffect } from 'react';
|
||||
|
||||
type RegisterRecordActionEffectProps = {
|
||||
action: ActionMenuEntry & {
|
||||
actionHook: ActionHook;
|
||||
useAction: ActionHook;
|
||||
};
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
};
|
||||
@ -17,7 +17,7 @@ export const RegisterRecordActionEffect = ({
|
||||
action,
|
||||
objectMetadataItem,
|
||||
}: RegisterRecordActionEffectProps) => {
|
||||
const { shouldBeRegistered, onClick, ConfirmationModal } = action.actionHook({
|
||||
const { shouldBeRegistered, onClick, ConfirmationModal } = action.useAction({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ import {
|
||||
export const DEFAULT_ACTIONS_CONFIG_V1: Record<
|
||||
string,
|
||||
ActionMenuEntry & {
|
||||
actionHook: ActionHook;
|
||||
useAction: ActionHook;
|
||||
}
|
||||
> = {
|
||||
addToFavoritesSingleRecord: {
|
||||
@ -37,7 +37,7 @@ export const DEFAULT_ACTIONS_CONFIG_V1: Record<
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useAddToFavoritesSingleRecordAction,
|
||||
useAction: useAddToFavoritesSingleRecordAction,
|
||||
},
|
||||
removeFromFavoritesSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -50,7 +50,7 @@ export const DEFAULT_ACTIONS_CONFIG_V1: Record<
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
||||
useAction: useRemoveFromFavoritesSingleRecordAction,
|
||||
},
|
||||
deleteSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -65,7 +65,7 @@ export const DEFAULT_ACTIONS_CONFIG_V1: Record<
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useDeleteSingleRecordAction,
|
||||
useAction: useDeleteSingleRecordAction,
|
||||
},
|
||||
deleteMultipleRecords: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -78,7 +78,7 @@ export const DEFAULT_ACTIONS_CONFIG_V1: Record<
|
||||
accent: 'danger',
|
||||
isPinned: true,
|
||||
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
|
||||
actionHook: useDeleteMultipleRecordsAction,
|
||||
useAction: useDeleteMultipleRecordsAction,
|
||||
},
|
||||
exportMultipleRecords: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -91,11 +91,11 @@ export const DEFAULT_ACTIONS_CONFIG_V1: Record<
|
||||
accent: 'default',
|
||||
isPinned: false,
|
||||
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
|
||||
actionHook: useExportMultipleRecordsAction,
|
||||
useAction: useExportMultipleRecordsAction,
|
||||
},
|
||||
exportView: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
scope: ActionMenuEntryScope.Object,
|
||||
key: NoSelectionRecordActionKeys.EXPORT_VIEW,
|
||||
label: 'Export view',
|
||||
shortLabel: 'Export',
|
||||
@ -104,6 +104,6 @@ export const DEFAULT_ACTIONS_CONFIG_V1: Record<
|
||||
accent: 'default',
|
||||
isPinned: false,
|
||||
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
|
||||
actionHook: useExportMultipleRecordsAction,
|
||||
useAction: useExportMultipleRecordsAction,
|
||||
},
|
||||
};
|
||||
|
||||
@ -33,12 +33,12 @@ import {
|
||||
export const DEFAULT_ACTIONS_CONFIG_V2: Record<
|
||||
string,
|
||||
ActionMenuEntry & {
|
||||
actionHook: ActionHook;
|
||||
useAction: ActionHook;
|
||||
}
|
||||
> = {
|
||||
createNewRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
scope: ActionMenuEntryScope.Object,
|
||||
key: NoSelectionRecordActionKeys.CREATE_NEW_RECORD,
|
||||
label: 'Create new record',
|
||||
shortLabel: 'New record',
|
||||
@ -46,7 +46,7 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
|
||||
isPinned: true,
|
||||
Icon: IconPlus,
|
||||
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
|
||||
actionHook: useCreateNewTableRecordNoSelectionRecordAction,
|
||||
useAction: useCreateNewTableRecordNoSelectionRecordAction,
|
||||
},
|
||||
exportNoteToPdf: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -58,7 +58,7 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
|
||||
isPinned: false,
|
||||
Icon: IconFileExport,
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
actionHook: useExportNoteAction,
|
||||
useAction: useExportNoteAction,
|
||||
},
|
||||
addToFavoritesSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -73,7 +73,7 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useAddToFavoritesSingleRecordAction,
|
||||
useAction: useAddToFavoritesSingleRecordAction,
|
||||
},
|
||||
removeFromFavoritesSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -88,7 +88,7 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
||||
useAction: useRemoveFromFavoritesSingleRecordAction,
|
||||
},
|
||||
deleteSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -104,7 +104,7 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useDeleteSingleRecordAction,
|
||||
useAction: useDeleteSingleRecordAction,
|
||||
},
|
||||
deleteMultipleRecords: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -117,7 +117,7 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
|
||||
accent: 'danger',
|
||||
isPinned: true,
|
||||
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
|
||||
actionHook: useDeleteMultipleRecordsAction,
|
||||
useAction: useDeleteMultipleRecordsAction,
|
||||
},
|
||||
exportMultipleRecords: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -130,11 +130,11 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
|
||||
accent: 'default',
|
||||
isPinned: false,
|
||||
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
|
||||
actionHook: useExportMultipleRecordsAction,
|
||||
useAction: useExportMultipleRecordsAction,
|
||||
},
|
||||
exportView: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
scope: ActionMenuEntryScope.Object,
|
||||
key: NoSelectionRecordActionKeys.EXPORT_VIEW,
|
||||
label: 'Export view',
|
||||
shortLabel: 'Export',
|
||||
@ -143,7 +143,7 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
|
||||
accent: 'default',
|
||||
isPinned: false,
|
||||
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
|
||||
actionHook: useExportMultipleRecordsAction,
|
||||
useAction: useExportMultipleRecordsAction,
|
||||
},
|
||||
destroySingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -159,7 +159,7 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useDestroySingleRecordAction,
|
||||
useAction: useDestroySingleRecordAction,
|
||||
},
|
||||
navigateToPreviousRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -171,7 +171,7 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
|
||||
isPinned: true,
|
||||
Icon: IconChevronUp,
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
actionHook: useNavigateToPreviousRecordSingleRecordAction,
|
||||
useAction: useNavigateToPreviousRecordSingleRecordAction,
|
||||
},
|
||||
navigateToNextRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -183,6 +183,6 @@ export const DEFAULT_ACTIONS_CONFIG_V2: Record<
|
||||
isPinned: true,
|
||||
Icon: IconChevronDown,
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
actionHook: useNavigateToNextRecordSingleRecordAction,
|
||||
useAction: useNavigateToNextRecordSingleRecordAction,
|
||||
},
|
||||
};
|
||||
|
||||
@ -44,7 +44,7 @@ import {
|
||||
export const WORKFLOW_ACTIONS_CONFIG: Record<
|
||||
string,
|
||||
ActionMenuEntry & {
|
||||
actionHook: ActionHook;
|
||||
useAction: ActionHook;
|
||||
}
|
||||
> = {
|
||||
createNewRecord: {
|
||||
@ -57,7 +57,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
|
||||
isPinned: true,
|
||||
Icon: IconPlus,
|
||||
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
|
||||
actionHook: useCreateNewTableRecordNoSelectionRecordAction,
|
||||
useAction: useCreateNewTableRecordNoSelectionRecordAction,
|
||||
},
|
||||
activateWorkflowSingleRecord: {
|
||||
key: WorkflowSingleRecordActionKeys.ACTIVATE,
|
||||
@ -72,7 +72,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useActivateWorkflowSingleRecordAction,
|
||||
useAction: useActivateWorkflowSingleRecordAction,
|
||||
},
|
||||
deactivateWorkflowSingleRecord: {
|
||||
key: WorkflowSingleRecordActionKeys.DEACTIVATE,
|
||||
@ -87,7 +87,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useDeactivateWorkflowSingleRecordAction,
|
||||
useAction: useDeactivateWorkflowSingleRecordAction,
|
||||
},
|
||||
discardWorkflowDraftSingleRecord: {
|
||||
key: WorkflowSingleRecordActionKeys.DISCARD_DRAFT,
|
||||
@ -102,7 +102,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useDiscardDraftWorkflowSingleRecordAction,
|
||||
useAction: useDiscardDraftWorkflowSingleRecordAction,
|
||||
},
|
||||
seeWorkflowActiveVersionSingleRecord: {
|
||||
key: WorkflowSingleRecordActionKeys.SEE_ACTIVE_VERSION,
|
||||
@ -117,7 +117,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useSeeActiveVersionWorkflowSingleRecordAction,
|
||||
useAction: useSeeActiveVersionWorkflowSingleRecordAction,
|
||||
},
|
||||
seeWorkflowRunsSingleRecord: {
|
||||
key: WorkflowSingleRecordActionKeys.SEE_RUNS,
|
||||
@ -132,7 +132,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useSeeRunsWorkflowSingleRecordAction,
|
||||
useAction: useSeeRunsWorkflowSingleRecordAction,
|
||||
},
|
||||
seeWorkflowVersionsHistorySingleRecord: {
|
||||
key: WorkflowSingleRecordActionKeys.SEE_VERSIONS,
|
||||
@ -147,7 +147,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useSeeVersionsWorkflowSingleRecordAction,
|
||||
useAction: useSeeVersionsWorkflowSingleRecordAction,
|
||||
},
|
||||
testWorkflowSingleRecord: {
|
||||
key: WorkflowSingleRecordActionKeys.TEST,
|
||||
@ -162,7 +162,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useTestWorkflowSingleRecordAction,
|
||||
useAction: useTestWorkflowSingleRecordAction,
|
||||
},
|
||||
navigateToPreviousRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -173,7 +173,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
|
||||
position: 8,
|
||||
Icon: IconChevronUp,
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
actionHook: useNavigateToPreviousRecordSingleRecordAction,
|
||||
useAction: useNavigateToPreviousRecordSingleRecordAction,
|
||||
},
|
||||
navigateToNextRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -184,7 +184,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
|
||||
position: 9,
|
||||
Icon: IconChevronDown,
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
actionHook: useNavigateToNextRecordSingleRecordAction,
|
||||
useAction: useNavigateToNextRecordSingleRecordAction,
|
||||
},
|
||||
addToFavoritesSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -199,7 +199,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useAddToFavoritesSingleRecordAction,
|
||||
useAction: useAddToFavoritesSingleRecordAction,
|
||||
},
|
||||
removeFromFavoritesSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -214,7 +214,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
||||
useAction: useRemoveFromFavoritesSingleRecordAction,
|
||||
},
|
||||
deleteSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -230,7 +230,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useDeleteSingleRecordAction,
|
||||
useAction: useDeleteSingleRecordAction,
|
||||
},
|
||||
deleteMultipleRecords: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -243,7 +243,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
|
||||
accent: 'danger',
|
||||
isPinned: true,
|
||||
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
|
||||
actionHook: useDeleteMultipleRecordsAction,
|
||||
useAction: useDeleteMultipleRecordsAction,
|
||||
},
|
||||
destroySingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -259,7 +259,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useDestroySingleRecordAction,
|
||||
useAction: useDestroySingleRecordAction,
|
||||
},
|
||||
exportMultipleRecords: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -272,7 +272,7 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
|
||||
accent: 'default',
|
||||
isPinned: false,
|
||||
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
|
||||
actionHook: useExportMultipleRecordsAction,
|
||||
useAction: useExportMultipleRecordsAction,
|
||||
},
|
||||
exportView: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -285,6 +285,6 @@ export const WORKFLOW_ACTIONS_CONFIG: Record<
|
||||
accent: 'default',
|
||||
isPinned: false,
|
||||
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
|
||||
actionHook: useExportMultipleRecordsAction,
|
||||
useAction: useExportMultipleRecordsAction,
|
||||
},
|
||||
};
|
||||
|
||||
@ -24,7 +24,7 @@ import {
|
||||
export const WORKFLOW_RUNS_ACTIONS_CONFIG: Record<
|
||||
string,
|
||||
ActionMenuEntry & {
|
||||
actionHook: ActionHook;
|
||||
useAction: ActionHook;
|
||||
}
|
||||
> = {
|
||||
addToFavoritesSingleRecord: {
|
||||
@ -40,7 +40,7 @@ export const WORKFLOW_RUNS_ACTIONS_CONFIG: Record<
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useAddToFavoritesSingleRecordAction,
|
||||
useAction: useAddToFavoritesSingleRecordAction,
|
||||
},
|
||||
removeFromFavoritesSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -55,7 +55,7 @@ export const WORKFLOW_RUNS_ACTIONS_CONFIG: Record<
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
||||
useAction: useRemoveFromFavoritesSingleRecordAction,
|
||||
},
|
||||
navigateToPreviousRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -67,7 +67,7 @@ export const WORKFLOW_RUNS_ACTIONS_CONFIG: Record<
|
||||
isPinned: true,
|
||||
Icon: IconChevronUp,
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
actionHook: useNavigateToPreviousRecordSingleRecordAction,
|
||||
useAction: useNavigateToPreviousRecordSingleRecordAction,
|
||||
},
|
||||
navigateToNextRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -79,7 +79,7 @@ export const WORKFLOW_RUNS_ACTIONS_CONFIG: Record<
|
||||
isPinned: true,
|
||||
Icon: IconChevronDown,
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
actionHook: useNavigateToNextRecordSingleRecordAction,
|
||||
useAction: useNavigateToNextRecordSingleRecordAction,
|
||||
},
|
||||
exportMultipleRecords: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -92,7 +92,7 @@ export const WORKFLOW_RUNS_ACTIONS_CONFIG: Record<
|
||||
accent: 'default',
|
||||
isPinned: false,
|
||||
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
|
||||
actionHook: useExportMultipleRecordsAction,
|
||||
useAction: useExportMultipleRecordsAction,
|
||||
},
|
||||
exportView: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -105,6 +105,6 @@ export const WORKFLOW_RUNS_ACTIONS_CONFIG: Record<
|
||||
accent: 'default',
|
||||
isPinned: false,
|
||||
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
|
||||
actionHook: useExportMultipleRecordsAction,
|
||||
useAction: useExportMultipleRecordsAction,
|
||||
},
|
||||
};
|
||||
|
||||
@ -31,7 +31,7 @@ import {
|
||||
export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
|
||||
string,
|
||||
ActionMenuEntry & {
|
||||
actionHook: ActionHook;
|
||||
useAction: ActionHook;
|
||||
}
|
||||
> = {
|
||||
useAsDraftWorkflowVersionSingleRecord: {
|
||||
@ -47,7 +47,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useUseAsDraftWorkflowVersionSingleRecordAction,
|
||||
useAction: useUseAsDraftWorkflowVersionSingleRecordAction,
|
||||
},
|
||||
seeWorkflowRunsSingleRecord: {
|
||||
key: WorkflowVersionSingleRecordActionKeys.SEE_RUNS,
|
||||
@ -61,7 +61,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useSeeRunsWorkflowVersionSingleRecordAction,
|
||||
useAction: useSeeRunsWorkflowVersionSingleRecordAction,
|
||||
},
|
||||
seeWorkflowVersionsHistorySingleRecord: {
|
||||
key: WorkflowVersionSingleRecordActionKeys.SEE_VERSIONS,
|
||||
@ -75,7 +75,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useSeeVersionsWorkflowVersionSingleRecordAction,
|
||||
useAction: useSeeVersionsWorkflowVersionSingleRecordAction,
|
||||
},
|
||||
navigateToPreviousRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -86,7 +86,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
|
||||
position: 4,
|
||||
Icon: IconChevronUp,
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
actionHook: useNavigateToPreviousRecordSingleRecordAction,
|
||||
useAction: useNavigateToPreviousRecordSingleRecordAction,
|
||||
},
|
||||
navigateToNextRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -97,7 +97,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
|
||||
position: 5,
|
||||
Icon: IconChevronDown,
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
actionHook: useNavigateToNextRecordSingleRecordAction,
|
||||
useAction: useNavigateToNextRecordSingleRecordAction,
|
||||
},
|
||||
addToFavoritesSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -112,7 +112,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useAddToFavoritesSingleRecordAction,
|
||||
useAction: useAddToFavoritesSingleRecordAction,
|
||||
},
|
||||
removeFromFavoritesSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -127,7 +127,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
||||
useAction: useRemoveFromFavoritesSingleRecordAction,
|
||||
},
|
||||
exportMultipleRecords: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -140,7 +140,7 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
|
||||
accent: 'default',
|
||||
isPinned: false,
|
||||
availableOn: [ActionViewType.INDEX_PAGE_BULK_SELECTION],
|
||||
actionHook: useExportMultipleRecordsAction,
|
||||
useAction: useExportMultipleRecordsAction,
|
||||
},
|
||||
exportView: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -153,6 +153,6 @@ export const WORKFLOW_VERSIONS_ACTIONS_CONFIG: Record<
|
||||
accent: 'default',
|
||||
isPinned: false,
|
||||
availableOn: [ActionViewType.INDEX_PAGE_NO_SELECTION],
|
||||
actionHook: useExportMultipleRecordsAction,
|
||||
useAction: useExportMultipleRecordsAction,
|
||||
},
|
||||
};
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import { RegisterAgnosticRecordActionEffect } from '@/action-menu/actions/record-agnostic-actions/components/RegisterAgnosticRecordActionEffect';
|
||||
import { RECORD_AGNOSTIC_ACTIONS_CONFIG } from '@/action-menu/actions/record-agnostic-actions/constants/RecordAgnosticActionsConfig';
|
||||
|
||||
export const RecordAgnosticActionMenuEntriesSetter = () => {
|
||||
return (
|
||||
<>
|
||||
{Object.values(RECORD_AGNOSTIC_ACTIONS_CONFIG).map((action) => (
|
||||
<RegisterAgnosticRecordActionEffect key={action.key} action={action} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,17 +0,0 @@
|
||||
import { useRecordAgnosticActions } from '@/action-menu/actions/record-agnostic-actions/hooks/useRecordAgnosticActions';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const RecordAgnosticActionsSetterEffect = () => {
|
||||
const { registerRecordAgnosticActions, unregisterRecordAgnosticActions } =
|
||||
useRecordAgnosticActions();
|
||||
|
||||
useEffect(() => {
|
||||
registerRecordAgnosticActions();
|
||||
|
||||
return () => {
|
||||
unregisterRecordAgnosticActions();
|
||||
};
|
||||
}, [registerRecordAgnosticActions, unregisterRecordAgnosticActions]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -0,0 +1,50 @@
|
||||
import { ActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
|
||||
import { wrapActionInCallbacks } from '@/action-menu/actions/utils/wrapActionInCallbacks';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
|
||||
import { useContext, useEffect } from 'react';
|
||||
|
||||
type RegisterAgnosticRecordActionEffectProps = {
|
||||
action: ActionMenuEntry & {
|
||||
useAction: ActionHookWithoutObjectMetadataItem;
|
||||
};
|
||||
};
|
||||
|
||||
export const RegisterAgnosticRecordActionEffect = ({
|
||||
action,
|
||||
}: RegisterAgnosticRecordActionEffectProps) => {
|
||||
const { shouldBeRegistered, onClick, ConfirmationModal } = action.useAction();
|
||||
|
||||
const { onActionStartedCallback, onActionExecutedCallback } =
|
||||
useContext(ActionMenuContext);
|
||||
|
||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
||||
|
||||
const wrappedAction = wrapActionInCallbacks({
|
||||
action: {
|
||||
...action,
|
||||
onClick,
|
||||
ConfirmationModal,
|
||||
},
|
||||
onActionStartedCallback,
|
||||
onActionExecutedCallback,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldBeRegistered) {
|
||||
addActionMenuEntry(wrappedAction);
|
||||
}
|
||||
|
||||
return () => {
|
||||
removeActionMenuEntry(wrappedAction.key);
|
||||
};
|
||||
}, [
|
||||
addActionMenuEntry,
|
||||
removeActionMenuEntry,
|
||||
shouldBeRegistered,
|
||||
wrappedAction,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -0,0 +1,14 @@
|
||||
import { RegisterAgnosticRecordActionEffect } from '@/action-menu/actions/record-agnostic-actions/components/RegisterAgnosticRecordActionEffect';
|
||||
import { useRunWorkflowActions } from '@/action-menu/actions/record-agnostic-actions/run-workflow-actions/hooks/useRunWorkflowActions';
|
||||
|
||||
export const RunWorkflowRecordAgnosticActionMenuEntriesSetter = () => {
|
||||
const { runWorkflowActions } = useRunWorkflowActions();
|
||||
|
||||
return (
|
||||
<>
|
||||
{runWorkflowActions.map((action) => (
|
||||
<RegisterAgnosticRecordActionEffect key={action.key} action={action} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
import { useSearchRecordsRecordAgnosticAction } from '@/action-menu/actions/record-agnostic-actions/hooks/useSearchRecordsRecordAgnosticAction';
|
||||
import { RecordAgnosticActionsKey } from '@/action-menu/actions/record-agnostic-actions/types/RecordAgnosticActionsKey';
|
||||
import { ActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
|
||||
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
|
||||
import {
|
||||
ActionMenuEntry,
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { IconSearch } from 'twenty-ui';
|
||||
|
||||
export const RECORD_AGNOSTIC_ACTIONS_CONFIG: Record<
|
||||
string,
|
||||
ActionMenuEntry & {
|
||||
useAction: ActionHookWithoutObjectMetadataItem;
|
||||
}
|
||||
> = {
|
||||
searchRecords: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.Global,
|
||||
key: RecordAgnosticActionsKey.SEARCH_RECORDS,
|
||||
label: 'Search records',
|
||||
shortLabel: 'Search',
|
||||
position: 0,
|
||||
isPinned: false,
|
||||
Icon: IconSearch,
|
||||
availableOn: [ActionViewType.GLOBAL],
|
||||
useAction: useSearchRecordsRecordAgnosticAction,
|
||||
hotKeys: ['/'],
|
||||
},
|
||||
};
|
||||
@ -1,26 +0,0 @@
|
||||
import { useWorkflowRunActions } from '@/action-menu/actions/record-agnostic-actions/workflow-run-actions/hooks/useWorkflowRunActions';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
export const useRecordAgnosticActions = () => {
|
||||
const isWorkflowEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsWorkflowEnabled,
|
||||
);
|
||||
|
||||
const { addWorkflowRunActions, removeWorkflowRunActions } =
|
||||
useWorkflowRunActions();
|
||||
|
||||
const registerRecordAgnosticActions = () => {
|
||||
if (isWorkflowEnabled) {
|
||||
addWorkflowRunActions();
|
||||
}
|
||||
};
|
||||
|
||||
const unregisterRecordAgnosticActions = () => {
|
||||
if (isWorkflowEnabled) {
|
||||
removeWorkflowRunActions();
|
||||
}
|
||||
};
|
||||
|
||||
return { registerRecordAgnosticActions, unregisterRecordAgnosticActions };
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { IconSearch } from 'twenty-ui';
|
||||
|
||||
export const useSearchRecordsRecordAgnosticAction = () => {
|
||||
const { openCommandMenu } = useCommandMenu();
|
||||
|
||||
const onClick = useRecoilCallback(
|
||||
({ set }) =>
|
||||
() => {
|
||||
set(commandMenuPageState, CommandMenuPages.SearchRecords);
|
||||
set(commandMenuPageInfoState, {
|
||||
title: 'Search',
|
||||
Icon: IconSearch,
|
||||
});
|
||||
openCommandMenu();
|
||||
},
|
||||
[openCommandMenu],
|
||||
);
|
||||
|
||||
return {
|
||||
onClick,
|
||||
shouldBeRegistered: true,
|
||||
};
|
||||
};
|
||||
@ -1,4 +1,3 @@
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
@ -11,55 +10,49 @@ import { capitalize } from 'twenty-shared';
|
||||
import { IconSettingsAutomation, isDefined } from 'twenty-ui';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
export const useWorkflowRunActions = () => {
|
||||
export const useRunWorkflowActions = () => {
|
||||
const isWorkflowEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsWorkflowEnabled,
|
||||
);
|
||||
|
||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
||||
|
||||
const { records: activeWorkflowVersions } = useAllActiveWorkflowVersions({
|
||||
triggerType: 'MANUAL',
|
||||
});
|
||||
|
||||
const { runWorkflowVersion } = useRunWorkflowVersion();
|
||||
|
||||
const addWorkflowRunActions = () => {
|
||||
if (!isWorkflowEnabled) {
|
||||
return;
|
||||
}
|
||||
if (!isWorkflowEnabled) {
|
||||
return { runWorkflowActions: [] };
|
||||
}
|
||||
|
||||
for (const [
|
||||
index,
|
||||
activeWorkflowVersion,
|
||||
] of activeWorkflowVersions.entries()) {
|
||||
const runWorkflowActions = activeWorkflowVersions
|
||||
.map((activeWorkflowVersion, index) => {
|
||||
if (!isDefined(activeWorkflowVersion.workflow)) {
|
||||
continue;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const name = capitalize(activeWorkflowVersion.workflow.name);
|
||||
|
||||
addActionMenuEntry({
|
||||
return {
|
||||
type: ActionMenuEntryType.WorkflowRun,
|
||||
key: `workflow-run-${activeWorkflowVersion.id}`,
|
||||
scope: ActionMenuEntryScope.Global,
|
||||
label: name,
|
||||
position: index,
|
||||
Icon: IconSettingsAutomation,
|
||||
onClick: async () => {
|
||||
await runWorkflowVersion({
|
||||
workflowVersionId: activeWorkflowVersion.id,
|
||||
});
|
||||
useAction: () => {
|
||||
return {
|
||||
shouldBeRegistered: true,
|
||||
onClick: async () => {
|
||||
await runWorkflowVersion({
|
||||
workflowVersionId: activeWorkflowVersion.id,
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
const removeWorkflowRunActions = () => {
|
||||
for (const activeWorkflowVersion of activeWorkflowVersions) {
|
||||
removeActionMenuEntry(`workflow-run-${activeWorkflowVersion.id}`);
|
||||
}
|
||||
};
|
||||
|
||||
return { addWorkflowRunActions, removeWorkflowRunActions };
|
||||
return { runWorkflowActions };
|
||||
};
|
||||
@ -0,0 +1,3 @@
|
||||
export enum RecordAgnosticActionsKey {
|
||||
SEARCH_RECORDS = 'search-records',
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
export enum ActionViewType {
|
||||
GLOBAL = 'GLOBAL',
|
||||
INDEX_PAGE_BULK_SELECTION = 'INDEX_PAGE_BULK_SELECTION',
|
||||
INDEX_PAGE_SINGLE_RECORD_SELECTION = 'INDEX_PAGE_SINGLE_RECORD_SELECTION',
|
||||
INDEX_PAGE_NO_SELECTION = 'INDEX_PAGE_NO_SELECTION',
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
|
||||
import { MultipleRecordsActionKeys } from '@/action-menu/actions/record-actions/multiple-records/types/MultipleRecordsActionKeys';
|
||||
import { RecordAgnosticActionsSetterEffect } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionsSetterEffect';
|
||||
import { RecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionMenuEntriesSetter';
|
||||
import { RunWorkflowRecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RunWorkflowRecordAgnosticActionMenuEntriesSetter';
|
||||
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
|
||||
import { RecordIndexActionMenuBar } from '@/action-menu/components/RecordIndexActionMenuBar';
|
||||
import { RecordIndexActionMenuButtons } from '@/action-menu/components/RecordIndexActionMenuButtons';
|
||||
@ -21,14 +22,14 @@ export const RecordIndexActionMenu = ({ indexId }: { indexId: string }) => {
|
||||
contextStoreCurrentObjectMetadataIdComponentState,
|
||||
);
|
||||
|
||||
const isWorkflowEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsWorkflowEnabled,
|
||||
);
|
||||
|
||||
const isCommandMenuV2Enabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsCommandMenuV2Enabled,
|
||||
);
|
||||
|
||||
const isWorkflowEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsWorkflowEnabled,
|
||||
);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const setIsLoadMoreLocked = useSetRecoilComponentStateV2(
|
||||
@ -63,7 +64,10 @@ export const RecordIndexActionMenu = ({ indexId }: { indexId: string }) => {
|
||||
<ActionMenuConfirmationModals />
|
||||
<RecordIndexActionMenuEffect />
|
||||
<RecordActionMenuEntriesSetter />
|
||||
{isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />}
|
||||
<RecordAgnosticActionMenuEntriesSetter />
|
||||
{isWorkflowEnabled && (
|
||||
<RunWorkflowRecordAgnosticActionMenuEntriesSetter />
|
||||
)}
|
||||
</ActionMenuContext.Provider>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
|
||||
import { RecordAgnosticActionsSetterEffect } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionsSetterEffect';
|
||||
import { RecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionMenuEntriesSetter';
|
||||
import { RunWorkflowRecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RunWorkflowRecordAgnosticActionMenuEntriesSetter';
|
||||
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
|
||||
import { RecordShowActionMenuButtons } from '@/action-menu/components/RecordShowActionMenuButtons';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
@ -29,14 +30,14 @@ export const RecordShowActionMenu = ({
|
||||
contextStoreCurrentObjectMetadataIdComponentState,
|
||||
);
|
||||
|
||||
const isWorkflowEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsWorkflowEnabled,
|
||||
);
|
||||
|
||||
const isCommandMenuV2Enabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsCommandMenuV2Enabled,
|
||||
);
|
||||
|
||||
const isWorkflowEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsWorkflowEnabled,
|
||||
);
|
||||
|
||||
// TODO: refactor RecordShowPageBaseHeader to use the context store
|
||||
|
||||
return (
|
||||
@ -63,7 +64,10 @@ export const RecordShowActionMenu = ({
|
||||
)}
|
||||
<ActionMenuConfirmationModals />
|
||||
<RecordActionMenuEntriesSetter />
|
||||
{isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />}
|
||||
<RecordAgnosticActionMenuEntriesSetter />
|
||||
{isWorkflowEnabled && (
|
||||
<RunWorkflowRecordAgnosticActionMenuEntriesSetter />
|
||||
)}
|
||||
</ActionMenuContext.Provider>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
|
||||
import { RecordAgnosticActionsSetterEffect } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionsSetterEffect';
|
||||
import { RecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionMenuEntriesSetter';
|
||||
import { RunWorkflowRecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RunWorkflowRecordAgnosticActionMenuEntriesSetter';
|
||||
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
|
||||
import { RightDrawerActionMenuDropdown } from '@/action-menu/components/RightDrawerActionMenuDropdown';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
@ -7,7 +8,7 @@ import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
export const RecordShowRightDrawerActionMenu = () => {
|
||||
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
|
||||
@ -25,7 +26,10 @@ export const RecordShowRightDrawerActionMenu = () => {
|
||||
<RightDrawerActionMenuDropdown />
|
||||
<ActionMenuConfirmationModals />
|
||||
<RecordActionMenuEntriesSetter />
|
||||
{isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />}
|
||||
<RecordAgnosticActionMenuEntriesSetter />
|
||||
{isWorkflowEnabled && (
|
||||
<RunWorkflowRecordAgnosticActionMenuEntriesSetter />
|
||||
)}
|
||||
</ActionMenuContext.Provider>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -11,6 +11,7 @@ export enum ActionMenuEntryType {
|
||||
export enum ActionMenuEntryScope {
|
||||
Global = 'Global',
|
||||
RecordSelection = 'RecordSelection',
|
||||
Object = 'Object',
|
||||
}
|
||||
|
||||
export type ActionMenuEntry = {
|
||||
@ -26,4 +27,5 @@ export type ActionMenuEntry = {
|
||||
availableOn?: ActionViewType[];
|
||||
onClick?: (event?: MouseEvent<HTMLElement>) => void;
|
||||
ConfirmationModal?: ReactElement<ConfirmationModalProps>;
|
||||
hotKeys?: string[];
|
||||
};
|
||||
|
||||
@ -1,92 +1,37 @@
|
||||
import { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||
import { CommandMenuDefaultSelectionEffect } from '@/command-menu/components/CommandMenuDefaultSelectionEffect';
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
|
||||
import { ResetContextToSelectionCommandButton } from '@/command-menu/components/ResetContextToSelectionCommandButton';
|
||||
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
|
||||
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
|
||||
import { useCommandMenuOnItemClick } from '@/command-menu/hooks/useCommandMenuOnItemClick';
|
||||
import { RESET_CONTEXT_TO_SELECTION } from '@/command-menu/constants/ResetContextToSelection';
|
||||
import { useMatchingCommandMenuCommands } from '@/command-menu/hooks/useMatchingCommandMenuCommands';
|
||||
import { useResetPreviousCommandMenuContext } from '@/command-menu/hooks/useResetPreviousCommandMenuContext';
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { Command } from '@/command-menu/types/Command';
|
||||
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
const MOBILE_NAVIGATION_BAR_HEIGHT = 64;
|
||||
|
||||
type CommandGroupConfig = {
|
||||
export type CommandGroupConfig = {
|
||||
heading: string;
|
||||
items?: Command[];
|
||||
};
|
||||
|
||||
const StyledList = styled.div`
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
overscroll-behavior: contain;
|
||||
transition: 100ms ease;
|
||||
transition-property: height;
|
||||
`;
|
||||
|
||||
const StyledInnerList = styled.div<{ isMobile: boolean }>`
|
||||
max-height: ${({ isMobile }) =>
|
||||
isMobile
|
||||
? `calc(100dvh - ${COMMAND_MENU_SEARCH_BAR_HEIGHT}px - ${
|
||||
COMMAND_MENU_SEARCH_BAR_PADDING * 2
|
||||
}px - ${MOBILE_NAVIGATION_BAR_HEIGHT}px)`
|
||||
: `calc(100dvh - ${COMMAND_MENU_SEARCH_BAR_HEIGHT}px - ${
|
||||
COMMAND_MENU_SEARCH_BAR_PADDING * 2
|
||||
}px)`};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
padding-top: ${({ theme }) => theme.spacing(1)};
|
||||
width: calc(100% - ${({ theme }) => theme.spacing(4)});
|
||||
`;
|
||||
|
||||
const StyledEmpty = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
height: 64px;
|
||||
justify-content: center;
|
||||
white-space: pre-wrap;
|
||||
`;
|
||||
|
||||
export const CommandMenu = () => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const { onItemClick } = useCommandMenuOnItemClick();
|
||||
const { resetPreviousCommandMenuContext } =
|
||||
useResetPreviousCommandMenuContext();
|
||||
|
||||
const commandMenuSearch = useRecoilValue(commandMenuSearchState);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const {
|
||||
isNoResults,
|
||||
isLoading,
|
||||
noResults,
|
||||
copilotCommands,
|
||||
matchingStandardActionRecordSelectionCommands,
|
||||
matchingStandardActionObjectCommands,
|
||||
matchingWorkflowRunRecordSelectionCommands,
|
||||
matchingStandardActionGlobalCommands,
|
||||
matchingWorkflowRunGlobalCommands,
|
||||
matchingNavigateCommand,
|
||||
peopleCommands,
|
||||
companyCommands,
|
||||
opportunityCommands,
|
||||
noteCommands,
|
||||
tasksCommands,
|
||||
customObjectCommands,
|
||||
matchingNavigateCommands,
|
||||
} = useMatchingCommandMenuCommands({
|
||||
commandMenuSearch,
|
||||
});
|
||||
@ -94,16 +39,11 @@ export const CommandMenu = () => {
|
||||
const selectableItems: Command[] = copilotCommands
|
||||
.concat(
|
||||
matchingStandardActionRecordSelectionCommands,
|
||||
matchingStandardActionObjectCommands,
|
||||
matchingWorkflowRunRecordSelectionCommands,
|
||||
matchingStandardActionGlobalCommands,
|
||||
matchingWorkflowRunGlobalCommands,
|
||||
matchingNavigateCommand,
|
||||
peopleCommands,
|
||||
companyCommands,
|
||||
opportunityCommands,
|
||||
noteCommands,
|
||||
tasksCommands,
|
||||
customObjectCommands,
|
||||
matchingNavigateCommands,
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
@ -115,7 +55,7 @@ export const CommandMenu = () => {
|
||||
const selectableItemIds = selectableItems.map((item) => item.id);
|
||||
|
||||
if (isNonEmptyString(previousContextStoreCurrentObjectMetadataId)) {
|
||||
selectableItemIds.unshift('reset-context-to-selection');
|
||||
selectableItemIds.unshift(RESET_CONTEXT_TO_SELECTION);
|
||||
}
|
||||
|
||||
const commandGroups: CommandGroupConfig[] = [
|
||||
@ -125,130 +65,35 @@ export const CommandMenu = () => {
|
||||
},
|
||||
{
|
||||
heading: t`Record Selection`,
|
||||
items: matchingStandardActionRecordSelectionCommands,
|
||||
items: matchingStandardActionRecordSelectionCommands.concat(
|
||||
matchingWorkflowRunRecordSelectionCommands,
|
||||
),
|
||||
},
|
||||
{
|
||||
heading: t`Workflow Record Selection`,
|
||||
items: matchingWorkflowRunRecordSelectionCommands,
|
||||
heading: t`Object`,
|
||||
items: matchingStandardActionObjectCommands,
|
||||
},
|
||||
{
|
||||
heading: t`View`,
|
||||
items: matchingStandardActionGlobalCommands,
|
||||
},
|
||||
{
|
||||
heading: t`Workflows`,
|
||||
items: matchingWorkflowRunGlobalCommands,
|
||||
},
|
||||
{
|
||||
heading: t`Navigate`,
|
||||
items: matchingNavigateCommand,
|
||||
},
|
||||
{
|
||||
heading: t`People`,
|
||||
items: peopleCommands,
|
||||
},
|
||||
{
|
||||
heading: t`Companies`,
|
||||
items: companyCommands,
|
||||
},
|
||||
{
|
||||
heading: t`Opportunities`,
|
||||
items: opportunityCommands,
|
||||
},
|
||||
{
|
||||
heading: t`Notes`,
|
||||
items: noteCommands,
|
||||
},
|
||||
{
|
||||
heading: t`Tasks`,
|
||||
items: tasksCommands,
|
||||
},
|
||||
{
|
||||
heading: t`Custom Objects`,
|
||||
items: customObjectCommands,
|
||||
heading: t`Global`,
|
||||
items: matchingStandardActionGlobalCommands
|
||||
.concat(matchingNavigateCommands)
|
||||
.concat(matchingWorkflowRunGlobalCommands),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommandMenuDefaultSelectionEffect
|
||||
selectableItemIds={selectableItemIds}
|
||||
/>
|
||||
|
||||
<StyledList>
|
||||
<ScrollWrapper
|
||||
contextProviderName="commandMenu"
|
||||
componentInstanceId={`scroll-wrapper-command-menu`}
|
||||
>
|
||||
<StyledInnerList isMobile={isMobile}>
|
||||
<SelectableList
|
||||
selectableListId="command-menu-list"
|
||||
selectableItemIdArray={selectableItemIds}
|
||||
hotkeyScope={AppHotkeyScope.CommandMenu}
|
||||
onEnter={(itemId) => {
|
||||
if (itemId === 'reset-context-to-selection') {
|
||||
resetPreviousCommandMenuContext();
|
||||
return;
|
||||
}
|
||||
|
||||
const command = selectableItems.find(
|
||||
(item) => item.id === itemId,
|
||||
);
|
||||
|
||||
if (isDefined(command)) {
|
||||
const { to, onCommandClick, shouldCloseCommandMenuOnClick } =
|
||||
command;
|
||||
|
||||
onItemClick({
|
||||
shouldCloseCommandMenuOnClick,
|
||||
onClick: onCommandClick,
|
||||
to,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isNonEmptyString(
|
||||
previousContextStoreCurrentObjectMetadataId,
|
||||
) && (
|
||||
<CommandGroup heading={t`Context`} key={t`Context`}>
|
||||
<SelectableItem itemId="reset-context-to-selection">
|
||||
<ResetContextToSelectionCommandButton />
|
||||
</SelectableItem>
|
||||
</CommandGroup>
|
||||
)}
|
||||
|
||||
{isNoResults && !isLoading && (
|
||||
<StyledEmpty>No results found</StyledEmpty>
|
||||
)}
|
||||
{commandGroups.map(({ heading, items }) =>
|
||||
items?.length ? (
|
||||
<CommandGroup heading={heading} key={heading}>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<SelectableItem itemId={item.id} key={item.id}>
|
||||
<CommandMenuItem
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
Icon={item.Icon}
|
||||
label={item.label}
|
||||
to={item.to}
|
||||
onClick={item.onCommandClick}
|
||||
firstHotKey={item.firstHotKey}
|
||||
secondHotKey={item.secondHotKey}
|
||||
shouldCloseCommandMenuOnClick={
|
||||
item.shouldCloseCommandMenuOnClick
|
||||
}
|
||||
/>
|
||||
</SelectableItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
) : null,
|
||||
)}
|
||||
</SelectableList>
|
||||
</StyledInnerList>
|
||||
</ScrollWrapper>
|
||||
</StyledList>
|
||||
</>
|
||||
<CommandMenuList
|
||||
commandGroups={commandGroups}
|
||||
selectableItemIds={selectableItemIds}
|
||||
noResults={noResults}
|
||||
>
|
||||
{isNonEmptyString(previousContextStoreCurrentObjectMetadataId) && (
|
||||
<CommandGroup heading={t`Context`}>
|
||||
<SelectableItem itemId={RESET_CONTEXT_TO_SELECTION}>
|
||||
<ResetContextToSelectionCommandButton />
|
||||
</SelectableItem>
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandMenuList>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
|
||||
import { RecordAgnosticActionsSetterEffect } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionsSetterEffect';
|
||||
import { RecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionMenuEntriesSetter';
|
||||
import { RunWorkflowRecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RunWorkflowRecordAgnosticActionMenuEntriesSetter';
|
||||
import { RecordAgnosticActionsKey } from '@/action-menu/actions/record-agnostic-actions/types/RecordAgnosticActionsKey';
|
||||
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
@ -20,7 +22,7 @@ import { motion } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useIsMobile } from 'twenty-ui';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledCommandMenu = styled(motion.div)`
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
@ -45,9 +47,6 @@ export const CommandMenuContainer = ({
|
||||
}) => {
|
||||
const { toggleCommandMenu, closeCommandMenu } = useCommandMenu();
|
||||
|
||||
const isWorkflowEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsWorkflowEnabled,
|
||||
);
|
||||
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
|
||||
|
||||
const commandMenuRef = useRef<HTMLDivElement>(null);
|
||||
@ -74,6 +73,10 @@ export const CommandMenuContainer = ({
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const isWorkflowEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IsWorkflowEnabled,
|
||||
);
|
||||
|
||||
return (
|
||||
<RecordFiltersComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'command-menu' }}
|
||||
@ -87,11 +90,18 @@ export const CommandMenuContainer = ({
|
||||
<ActionMenuContext.Provider
|
||||
value={{
|
||||
isInRightDrawer: false,
|
||||
onActionExecutedCallback: toggleCommandMenu,
|
||||
onActionExecutedCallback: ({ key }) => {
|
||||
if (key !== RecordAgnosticActionsKey.SEARCH_RECORDS) {
|
||||
toggleCommandMenu();
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
<RecordActionMenuEntriesSetter />
|
||||
{isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />}
|
||||
<RecordAgnosticActionMenuEntriesSetter />
|
||||
{isWorkflowEnabled && (
|
||||
<RunWorkflowRecordAgnosticActionMenuEntriesSetter />
|
||||
)}
|
||||
<ActionMenuConfirmationModals />
|
||||
{isCommandMenuOpened && (
|
||||
<StyledCommandMenu
|
||||
|
||||
@ -12,8 +12,7 @@ export type CommandMenuItemProps = {
|
||||
id: string;
|
||||
onClick?: () => void;
|
||||
Icon?: IconComponent;
|
||||
firstHotKey?: string;
|
||||
secondHotKey?: string;
|
||||
hotKeys?: string[];
|
||||
shouldCloseCommandMenuOnClick?: boolean;
|
||||
RightComponent?: ReactNode;
|
||||
};
|
||||
@ -24,8 +23,7 @@ export const CommandMenuItem = ({
|
||||
id,
|
||||
onClick,
|
||||
Icon,
|
||||
firstHotKey,
|
||||
secondHotKey,
|
||||
hotKeys,
|
||||
shouldCloseCommandMenuOnClick,
|
||||
RightComponent,
|
||||
}: CommandMenuItemProps) => {
|
||||
@ -42,8 +40,7 @@ export const CommandMenuItem = ({
|
||||
<MenuItemCommand
|
||||
LeftIcon={Icon}
|
||||
text={label}
|
||||
firstHotKey={firstHotKey}
|
||||
secondHotKey={secondHotKey}
|
||||
hotKeys={hotKeys}
|
||||
onClick={() =>
|
||||
onItemClick({
|
||||
shouldCloseCommandMenuOnClick,
|
||||
|
||||
@ -0,0 +1,146 @@
|
||||
import { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||
import { CommandGroupConfig } from '@/command-menu/components/CommandMenu';
|
||||
import { CommandMenuDefaultSelectionEffect } from '@/command-menu/components/CommandMenuDefaultSelectionEffect';
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
|
||||
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
|
||||
import { RESET_CONTEXT_TO_SELECTION } from '@/command-menu/constants/ResetContextToSelection';
|
||||
import { useCommandMenuOnItemClick } from '@/command-menu/hooks/useCommandMenuOnItemClick';
|
||||
import { useResetPreviousCommandMenuContext } from '@/command-menu/hooks/useResetPreviousCommandMenuContext';
|
||||
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import styled from '@emotion/styled';
|
||||
import { MOBILE_VIEWPORT, isDefined } from 'twenty-ui';
|
||||
|
||||
const MOBILE_NAVIGATION_BAR_HEIGHT = 64;
|
||||
|
||||
export type CommandMenuListProps = {
|
||||
commandGroups: CommandGroupConfig[];
|
||||
selectableItemIds: string[];
|
||||
children?: React.ReactNode;
|
||||
loading?: boolean;
|
||||
noResults?: boolean;
|
||||
};
|
||||
|
||||
const StyledList = styled.div`
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
overscroll-behavior: contain;
|
||||
transition: 100ms ease;
|
||||
transition-property: height;
|
||||
`;
|
||||
|
||||
const StyledInnerList = styled.div`
|
||||
max-height: calc(
|
||||
100dvh - ${COMMAND_MENU_SEARCH_BAR_HEIGHT}px -
|
||||
${COMMAND_MENU_SEARCH_BAR_PADDING * 2}px -
|
||||
${MOBILE_NAVIGATION_BAR_HEIGHT}px
|
||||
);
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
padding-top: ${({ theme }) => theme.spacing(1)};
|
||||
width: calc(100% - ${({ theme }) => theme.spacing(4)});
|
||||
|
||||
@media (min-width: ${MOBILE_VIEWPORT}px) {
|
||||
max-height: calc(
|
||||
100dvh - ${COMMAND_MENU_SEARCH_BAR_HEIGHT}px -
|
||||
${COMMAND_MENU_SEARCH_BAR_PADDING * 2}px
|
||||
);
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledEmpty = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
height: 64px;
|
||||
justify-content: center;
|
||||
white-space: pre-wrap;
|
||||
`;
|
||||
|
||||
export const CommandMenuList = ({
|
||||
commandGroups,
|
||||
selectableItemIds,
|
||||
children,
|
||||
loading = false,
|
||||
noResults = false,
|
||||
}: CommandMenuListProps) => {
|
||||
const { onItemClick } = useCommandMenuOnItemClick();
|
||||
|
||||
const commands = commandGroups.flatMap((group) => group.items ?? []);
|
||||
|
||||
const { resetPreviousCommandMenuContext } =
|
||||
useResetPreviousCommandMenuContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommandMenuDefaultSelectionEffect
|
||||
selectableItemIds={selectableItemIds}
|
||||
/>
|
||||
<StyledList>
|
||||
<ScrollWrapper
|
||||
contextProviderName="commandMenu"
|
||||
componentInstanceId={`scroll-wrapper-command-menu`}
|
||||
>
|
||||
<StyledInnerList>
|
||||
<SelectableList
|
||||
selectableListId="command-menu-list"
|
||||
hotkeyScope={AppHotkeyScope.CommandMenuOpen}
|
||||
selectableItemIdArray={selectableItemIds}
|
||||
onEnter={(itemId) => {
|
||||
if (itemId === RESET_CONTEXT_TO_SELECTION) {
|
||||
resetPreviousCommandMenuContext();
|
||||
return;
|
||||
}
|
||||
|
||||
const command = commands.find((item) => item.id === itemId);
|
||||
|
||||
if (isDefined(command)) {
|
||||
const { to, onCommandClick, shouldCloseCommandMenuOnClick } =
|
||||
command;
|
||||
|
||||
onItemClick({
|
||||
shouldCloseCommandMenuOnClick,
|
||||
onClick: onCommandClick,
|
||||
to,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
{noResults && !loading && (
|
||||
<StyledEmpty>No result found</StyledEmpty>
|
||||
)}
|
||||
{commandGroups.map(({ heading, items }) =>
|
||||
items?.length ? (
|
||||
<CommandGroup heading={heading} key={heading}>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<SelectableItem itemId={item.id} key={item.id}>
|
||||
<CommandMenuItem
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
Icon={item.Icon}
|
||||
label={item.label}
|
||||
to={item.to}
|
||||
onClick={item.onCommandClick}
|
||||
hotKeys={item.hotKeys}
|
||||
shouldCloseCommandMenuOnClick={
|
||||
item.shouldCloseCommandMenuOnClick
|
||||
}
|
||||
/>
|
||||
</SelectableItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
) : null,
|
||||
)}
|
||||
</SelectableList>
|
||||
</StyledInnerList>
|
||||
</ScrollWrapper>
|
||||
</StyledList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,12 +1,12 @@
|
||||
import { CommandMenuContextChip } from '@/command-menu/components/CommandMenuContextChip';
|
||||
import { CommandMenuContextRecordChip } from '@/command-menu/components/CommandMenuContextRecordChip';
|
||||
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
|
||||
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
|
||||
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useTheme } from '@emotion/react';
|
||||
@ -95,11 +95,12 @@ export const CommandMenuTopBar = () => {
|
||||
return (
|
||||
<StyledInputContainer>
|
||||
<StyledContentContainer>
|
||||
{isDefined(contextStoreCurrentObjectMetadataId) && (
|
||||
<CommandMenuContextRecordChip
|
||||
objectMetadataItemId={contextStoreCurrentObjectMetadataId}
|
||||
/>
|
||||
)}
|
||||
{commandMenuPage !== CommandMenuPages.SearchRecords &&
|
||||
isDefined(contextStoreCurrentObjectMetadataId) && (
|
||||
<CommandMenuContextRecordChip
|
||||
objectMetadataItemId={contextStoreCurrentObjectMetadataId}
|
||||
/>
|
||||
)}
|
||||
{isDefined(Icon) && (
|
||||
<CommandMenuContextChip
|
||||
Icons={[<Icon size={theme.icon.size.sm} />]}
|
||||
@ -107,7 +108,8 @@ export const CommandMenuTopBar = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{commandMenuPage === CommandMenuPages.Root && (
|
||||
{(commandMenuPage === CommandMenuPages.Root ||
|
||||
commandMenuPage === CommandMenuPages.SearchRecords) && (
|
||||
<StyledInput
|
||||
autoFocus
|
||||
value={commandMenuSearch}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { CommandMenuContextRecordChip } from '@/command-menu/components/CommandMenuContextRecordChip';
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { RESET_CONTEXT_TO_SELECTION } from '@/command-menu/constants/ResetContextToSelection';
|
||||
import { useResetPreviousCommandMenuContext } from '@/command-menu/hooks/useResetPreviousCommandMenuContext';
|
||||
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
@ -40,7 +41,7 @@ export const ResetContextToSelectionCommandButton = () => {
|
||||
|
||||
return (
|
||||
<CommandMenuItem
|
||||
id="reset-context-to-selection"
|
||||
id={RESET_CONTEXT_TO_SELECTION}
|
||||
Icon={IconArrowBackUp}
|
||||
label={t`Reset to`}
|
||||
RightComponent={
|
||||
|
||||
@ -85,42 +85,43 @@ export const DefaultWithoutSearch: Story = {
|
||||
play: async () => {
|
||||
const canvas = within(document.body);
|
||||
|
||||
expect(await canvas.findByText('Go to People')).toBeInTheDocument();
|
||||
expect(await canvas.findByText('Go to Companies')).toBeInTheDocument();
|
||||
expect(await canvas.findByText('Go to Opportunities')).toBeInTheDocument();
|
||||
expect(await canvas.findByText('Go to Settings')).toBeInTheDocument();
|
||||
expect(await canvas.findByText('Go to Tasks')).toBeInTheDocument();
|
||||
expect(await canvas.findByText('Go to People')).toBeVisible();
|
||||
expect(await canvas.findByText('Go to Companies')).toBeVisible();
|
||||
expect(await canvas.findByText('Go to Opportunities')).toBeVisible();
|
||||
expect(await canvas.findByText('Go to Settings')).toBeVisible();
|
||||
expect(await canvas.findByText('Go to Tasks')).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const MatchingPersonCompanyActivityCreateNavigate: Story = {
|
||||
play: async () => {
|
||||
const canvas = within(document.body);
|
||||
const searchInput = await canvas.findByPlaceholderText('Type anything');
|
||||
await sleep(openTimeout);
|
||||
await userEvent.type(searchInput, 'n');
|
||||
expect(await canvas.findByText('Linkedin')).toBeInTheDocument();
|
||||
expect(await canvas.findByText(companiesMock[0].name)).toBeInTheDocument();
|
||||
expect(await canvas.findByText('Go to Companies')).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const OnlyMatchingCreateAndNavigate: Story = {
|
||||
export const MatchingNavigate: Story = {
|
||||
play: async () => {
|
||||
const canvas = within(document.body);
|
||||
const searchInput = await canvas.findByPlaceholderText('Type anything');
|
||||
await sleep(openTimeout);
|
||||
await userEvent.type(searchInput, 'ta');
|
||||
expect(await canvas.findByText('Go to Tasks')).toBeInTheDocument();
|
||||
expect(await canvas.findByText('Go to Tasks')).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const AtleastMatchingOnePerson: Story = {
|
||||
export const MatchingNavigateShortcuts: Story = {
|
||||
play: async () => {
|
||||
const canvas = within(document.body);
|
||||
const searchInput = await canvas.findByPlaceholderText('Type anything');
|
||||
await sleep(openTimeout);
|
||||
await userEvent.type(searchInput, 'alex');
|
||||
expect(await canvas.findByText('Sylvie Palmer')).toBeInTheDocument();
|
||||
await userEvent.type(searchInput, 'gp');
|
||||
expect(await canvas.findByText('Go to People')).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const SearchRecordsAction: Story = {
|
||||
play: async () => {
|
||||
const canvas = within(document.body);
|
||||
const searchRecordsButton = await canvas.findByText('Search records');
|
||||
await userEvent.click(searchRecordsButton);
|
||||
const searchInput = await canvas.findByPlaceholderText('Type anything');
|
||||
await sleep(openTimeout);
|
||||
await userEvent.type(searchInput, 'n');
|
||||
expect(await canvas.findByText('Linkedin')).toBeVisible();
|
||||
expect(await canvas.findByText(companiesMock[0].name)).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
@ -21,8 +21,7 @@ export const COMMAND_MENU_NAVIGATE_COMMANDS: { [key: string]: Command } = {
|
||||
}),
|
||||
label: 'Go to People',
|
||||
type: CommandType.Navigate,
|
||||
firstHotKey: 'G',
|
||||
secondHotKey: 'P',
|
||||
hotKeys: ['G', 'P'],
|
||||
Icon: IconUser,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
},
|
||||
@ -33,8 +32,7 @@ export const COMMAND_MENU_NAVIGATE_COMMANDS: { [key: string]: Command } = {
|
||||
}),
|
||||
label: 'Go to Companies',
|
||||
type: CommandType.Navigate,
|
||||
firstHotKey: 'G',
|
||||
secondHotKey: 'C',
|
||||
hotKeys: ['G', 'C'],
|
||||
Icon: IconBuildingSkyscraper,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
},
|
||||
@ -45,8 +43,7 @@ export const COMMAND_MENU_NAVIGATE_COMMANDS: { [key: string]: Command } = {
|
||||
}),
|
||||
label: 'Go to Opportunities',
|
||||
type: CommandType.Navigate,
|
||||
firstHotKey: 'G',
|
||||
secondHotKey: 'O',
|
||||
hotKeys: ['G', 'O'],
|
||||
Icon: IconTargetArrow,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
},
|
||||
@ -55,8 +52,7 @@ export const COMMAND_MENU_NAVIGATE_COMMANDS: { [key: string]: Command } = {
|
||||
to: getSettingsPath(SettingsPath.ProfilePage),
|
||||
label: 'Go to Settings',
|
||||
type: CommandType.Navigate,
|
||||
firstHotKey: 'G',
|
||||
secondHotKey: 'S',
|
||||
hotKeys: ['G', 'S'],
|
||||
Icon: IconSettings,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
},
|
||||
@ -67,8 +63,7 @@ export const COMMAND_MENU_NAVIGATE_COMMANDS: { [key: string]: Command } = {
|
||||
}),
|
||||
label: 'Go to Tasks',
|
||||
type: CommandType.Navigate,
|
||||
firstHotKey: 'G',
|
||||
secondHotKey: 'T',
|
||||
hotKeys: ['G', 'T'],
|
||||
Icon: IconCheckbox,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
},
|
||||
|
||||
@ -2,7 +2,8 @@ import { RightDrawerCalendarEvent } from '@/activities/calendar/right-drawer/com
|
||||
import { RightDrawerAIChat } from '@/activities/copilot/right-drawer/components/RightDrawerAIChat';
|
||||
import { RightDrawerEmailThread } from '@/activities/emails/right-drawer/components/RightDrawerEmailThread';
|
||||
import { CommandMenu } from '@/command-menu/components/CommandMenu';
|
||||
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
|
||||
import { CommandMenuSearchRecordsPage } from '@/command-menu/pages/components/CommandMenuSearchRecordsPage';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { RightDrawerRecord } from '@/object-record/record-right-drawer/components/RightDrawerRecord';
|
||||
import { RightDrawerWorkflowEditStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowEditStep';
|
||||
import { RightDrawerWorkflowViewStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowViewStep';
|
||||
@ -28,4 +29,5 @@ export const COMMAND_MENU_PAGES_CONFIG = new Map<
|
||||
],
|
||||
[CommandMenuPages.WorkflowStepEdit, <RightDrawerWorkflowEditStep />],
|
||||
[CommandMenuPages.WorkflowStepView, <RightDrawerWorkflowViewStep />],
|
||||
[CommandMenuPages.SearchRecords, <CommandMenuSearchRecordsPage />],
|
||||
]);
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export const RESET_CONTEXT_TO_SELECTION = 'reset-context-to-selection';
|
||||
@ -5,11 +5,11 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
||||
|
||||
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
|
||||
import { useCopyContextStoreStates } from '@/command-menu/hooks/useCopyContextStoreAndActionMenuStates';
|
||||
import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
@ -19,6 +19,7 @@ import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
|
||||
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
|
||||
import { IconSearch } from 'twenty-ui';
|
||||
import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState';
|
||||
|
||||
export const useCommandMenu = () => {
|
||||
@ -71,6 +72,7 @@ export const useCommandMenu = () => {
|
||||
Icon: undefined,
|
||||
});
|
||||
set(isCommandMenuOpenedState, false);
|
||||
set(commandMenuSearchState, '');
|
||||
resetSelectedItem();
|
||||
goBackToPreviousHotkeyScope();
|
||||
|
||||
@ -110,6 +112,20 @@ export const useCommandMenu = () => {
|
||||
[openCommandMenu],
|
||||
);
|
||||
|
||||
const openRecordsSearchPage = useRecoilCallback(
|
||||
({ set }) => {
|
||||
return () => {
|
||||
set(commandMenuPageState, CommandMenuPages.SearchRecords);
|
||||
set(commandMenuPageInfoState, {
|
||||
title: 'Search',
|
||||
Icon: IconSearch,
|
||||
});
|
||||
openCommandMenu();
|
||||
};
|
||||
},
|
||||
[openCommandMenu],
|
||||
);
|
||||
|
||||
const setGlobalCommandMenuContext = useRecoilCallback(
|
||||
({ set }) => {
|
||||
return () => {
|
||||
@ -161,6 +177,7 @@ export const useCommandMenu = () => {
|
||||
return {
|
||||
openCommandMenu,
|
||||
closeCommandMenu,
|
||||
openRecordsSearchPage,
|
||||
openRecordInCommandMenu,
|
||||
toggleCommandMenu,
|
||||
setGlobalCommandMenuContext,
|
||||
|
||||
@ -5,43 +5,25 @@ import {
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { useOpenCopilotRightDrawer } from '@/activities/copilot/right-drawer/hooks/useOpenCopilotRightDrawer';
|
||||
import { copilotQueryState } from '@/activities/copilot/right-drawer/states/copilotQueryState';
|
||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { COMMAND_MENU_NAVIGATE_COMMANDS } from '@/command-menu/constants/CommandMenuNavigateCommands';
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import {
|
||||
Command,
|
||||
CommandScope,
|
||||
CommandType,
|
||||
} from '@/command-menu/types/Command';
|
||||
import { Company } from '@/companies/types/Company';
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainName';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { useMultiObjectSearch } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
||||
import { useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap } from '@/object-record/relation-picker/hooks/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap';
|
||||
import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { Avatar, IconCheckbox, IconNotes, IconSparkles } from 'twenty-ui';
|
||||
import { IconSparkles } from 'twenty-ui';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
import { getLogoUrlFromDomainName } from '~/utils';
|
||||
|
||||
export const useCommandMenuCommands = () => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentSelector,
|
||||
);
|
||||
const openActivityRightDrawer = useOpenActivityRightDrawer({
|
||||
objectNameSingular: CoreObjectNameSingular.Note,
|
||||
});
|
||||
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
|
||||
|
||||
const commandMenuSearch = useRecoilValue(commandMenuSearchState);
|
||||
const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300); // 200ms - 500ms
|
||||
|
||||
@ -78,6 +60,23 @@ export const useCommandMenuCommands = () => {
|
||||
onCommandClick: actionMenuEntry.onClick,
|
||||
type: CommandType.StandardAction,
|
||||
scope: CommandScope.RecordSelection,
|
||||
hotKeys: actionMenuEntry.hotKeys,
|
||||
}));
|
||||
|
||||
const actionObjectCommands: Command[] = actionMenuEntries
|
||||
?.filter(
|
||||
(actionMenuEntry) =>
|
||||
actionMenuEntry.type === ActionMenuEntryType.Standard &&
|
||||
actionMenuEntry.scope === ActionMenuEntryScope.Object,
|
||||
)
|
||||
?.map((actionMenuEntry) => ({
|
||||
id: actionMenuEntry.key,
|
||||
label: actionMenuEntry.label,
|
||||
Icon: actionMenuEntry.Icon,
|
||||
onCommandClick: actionMenuEntry.onClick,
|
||||
type: CommandType.StandardAction,
|
||||
scope: CommandScope.Object,
|
||||
hotKeys: actionMenuEntry.hotKeys,
|
||||
}));
|
||||
|
||||
const actionGlobalCommands: Command[] = actionMenuEntries
|
||||
@ -93,6 +92,7 @@ export const useCommandMenuCommands = () => {
|
||||
onCommandClick: actionMenuEntry.onClick,
|
||||
type: CommandType.StandardAction,
|
||||
scope: CommandScope.Global,
|
||||
hotKeys: actionMenuEntry.hotKeys,
|
||||
}));
|
||||
|
||||
const workflowRunRecordSelectionCommands: Command[] = actionMenuEntries
|
||||
@ -108,6 +108,7 @@ export const useCommandMenuCommands = () => {
|
||||
onCommandClick: actionMenuEntry.onClick,
|
||||
type: CommandType.WorkflowRun,
|
||||
scope: CommandScope.RecordSelection,
|
||||
hotKeys: actionMenuEntry.hotKeys,
|
||||
}));
|
||||
|
||||
const workflowRunGlobalCommands: Command[] = actionMenuEntries
|
||||
@ -123,193 +124,16 @@ export const useCommandMenuCommands = () => {
|
||||
onCommandClick: actionMenuEntry.onClick,
|
||||
type: CommandType.WorkflowRun,
|
||||
scope: CommandScope.Global,
|
||||
hotKeys: actionMenuEntry.hotKeys,
|
||||
}));
|
||||
|
||||
const {
|
||||
matchesSearchFilterObjectRecordsQueryResult,
|
||||
matchesSearchFilterObjectRecordsLoading: loading,
|
||||
} = useMultiObjectSearch({
|
||||
excludedObjects: [CoreObjectNameSingular.Task, CoreObjectNameSingular.Note],
|
||||
searchFilterValue: deferredCommandMenuSearch ?? undefined,
|
||||
limit: 3,
|
||||
});
|
||||
|
||||
const { objectRecordsMap: matchesSearchFilterObjectRecords } =
|
||||
useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap({
|
||||
multiObjectRecordsQueryResult:
|
||||
matchesSearchFilterObjectRecordsQueryResult,
|
||||
});
|
||||
|
||||
const { loading: isNotesLoading, records: notes } = useFindManyRecords<Note>({
|
||||
skip: !isCommandMenuOpened,
|
||||
objectNameSingular: CoreObjectNameSingular.Note,
|
||||
filter: deferredCommandMenuSearch
|
||||
? makeOrFilterVariables([
|
||||
{ title: { ilike: `%${deferredCommandMenuSearch}%` } },
|
||||
{ body: { ilike: `%${deferredCommandMenuSearch}%` } },
|
||||
])
|
||||
: undefined,
|
||||
limit: 3,
|
||||
});
|
||||
|
||||
const { loading: isTasksLoading, records: tasks } = useFindManyRecords<Task>({
|
||||
skip: !isCommandMenuOpened,
|
||||
objectNameSingular: CoreObjectNameSingular.Task,
|
||||
filter: deferredCommandMenuSearch
|
||||
? makeOrFilterVariables([
|
||||
{ title: { ilike: `%${deferredCommandMenuSearch}%` } },
|
||||
{ body: { ilike: `%${deferredCommandMenuSearch}%` } },
|
||||
])
|
||||
: undefined,
|
||||
limit: 3,
|
||||
});
|
||||
|
||||
const people = matchesSearchFilterObjectRecords.people?.map(
|
||||
(people) => people.record,
|
||||
);
|
||||
const companies = matchesSearchFilterObjectRecords.companies?.map(
|
||||
(companies) => companies.record,
|
||||
);
|
||||
const opportunities = matchesSearchFilterObjectRecords.opportunities?.map(
|
||||
(opportunities) => opportunities.record,
|
||||
);
|
||||
|
||||
const peopleCommands = useMemo(
|
||||
() =>
|
||||
people?.map(({ id, name: { firstName, lastName }, avatarUrl }) => ({
|
||||
id,
|
||||
label: `${firstName} ${lastName}`,
|
||||
to: `object/person/${id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
type="rounded"
|
||||
avatarUrl={avatarUrl}
|
||||
placeholderColorSeed={id}
|
||||
placeholder={`${firstName} ${lastName}`}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
[people],
|
||||
);
|
||||
|
||||
const companyCommands = useMemo(
|
||||
() =>
|
||||
companies?.map((company) => ({
|
||||
id: company.id,
|
||||
label: company.name ?? '',
|
||||
to: `object/company/${company.id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
placeholderColorSeed={company.id}
|
||||
placeholder={company.name}
|
||||
avatarUrl={getLogoUrlFromDomainName(
|
||||
getCompanyDomainName(company as Company),
|
||||
)}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
[companies],
|
||||
);
|
||||
|
||||
const opportunityCommands = useMemo(
|
||||
() =>
|
||||
opportunities?.map(({ id, name }) => ({
|
||||
id,
|
||||
label: name ?? '',
|
||||
to: `object/opportunity/${id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
type="rounded"
|
||||
avatarUrl={null}
|
||||
placeholderColorSeed={id}
|
||||
placeholder={name ?? ''}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
[opportunities],
|
||||
);
|
||||
|
||||
const noteCommands = useMemo(
|
||||
() =>
|
||||
notes?.map((note) => ({
|
||||
id: note.id,
|
||||
label: note.title ?? '',
|
||||
to: '',
|
||||
onCommandClick: () => openActivityRightDrawer(note.id),
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: IconNotes,
|
||||
})),
|
||||
[notes, openActivityRightDrawer],
|
||||
);
|
||||
|
||||
const tasksCommands = useMemo(
|
||||
() =>
|
||||
tasks?.map((task) => ({
|
||||
id: task.id,
|
||||
label: task.title ?? '',
|
||||
to: '',
|
||||
onCommandClick: () => openActivityRightDrawer(task.id),
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: IconCheckbox,
|
||||
})),
|
||||
[tasks, openActivityRightDrawer],
|
||||
);
|
||||
|
||||
const customObjectRecordsMap = useMemo(() => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(matchesSearchFilterObjectRecords).filter(
|
||||
([namePlural, records]) =>
|
||||
![
|
||||
CoreObjectNamePlural.Person,
|
||||
CoreObjectNamePlural.Opportunity,
|
||||
CoreObjectNamePlural.Company,
|
||||
].includes(namePlural as CoreObjectNamePlural) && !isEmpty(records),
|
||||
),
|
||||
);
|
||||
}, [matchesSearchFilterObjectRecords]);
|
||||
|
||||
const customObjectCommands = useMemo(() => {
|
||||
const customObjectCommandsArray: Command[] = [];
|
||||
Object.values(customObjectRecordsMap).forEach((objectRecords) => {
|
||||
customObjectCommandsArray.push(
|
||||
...objectRecords.map((objectRecord) => ({
|
||||
id: objectRecord.record.id,
|
||||
label: objectRecord.recordIdentifier.name,
|
||||
to: `object/${objectRecord.objectMetadataItem.nameSingular}/${objectRecord.record.id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
type="rounded"
|
||||
avatarUrl={objectRecord.record.avatarUrl}
|
||||
placeholderColorSeed={objectRecord.record.id}
|
||||
placeholder={objectRecord.recordIdentifier.name ?? ''}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
return customObjectCommandsArray;
|
||||
}, [customObjectRecordsMap]);
|
||||
|
||||
const isLoading = loading || isNotesLoading || isTasksLoading;
|
||||
|
||||
return {
|
||||
copilotCommands,
|
||||
navigateCommands,
|
||||
actionRecordSelectionCommands,
|
||||
actionGlobalCommands,
|
||||
actionObjectCommands,
|
||||
workflowRunRecordSelectionCommands,
|
||||
workflowRunGlobalCommands,
|
||||
peopleCommands,
|
||||
companyCommands,
|
||||
opportunityCommands,
|
||||
noteCommands,
|
||||
tasksCommands,
|
||||
customObjectCommands,
|
||||
isLoading,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
@ -12,8 +12,12 @@ import { useRecoilValue } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
export const useCommandMenuHotKeys = () => {
|
||||
const { closeCommandMenu, toggleCommandMenu, setGlobalCommandMenuContext } =
|
||||
useCommandMenu();
|
||||
const {
|
||||
closeCommandMenu,
|
||||
openRecordsSearchPage,
|
||||
toggleCommandMenu,
|
||||
setGlobalCommandMenuContext,
|
||||
} = useCommandMenu();
|
||||
|
||||
const commandMenuSearch = useRecoilValue(commandMenuSearchState);
|
||||
|
||||
@ -36,6 +40,18 @@ export const useCommandMenuHotKeys = () => {
|
||||
[toggleCommandMenu],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
['/'],
|
||||
() => {
|
||||
openRecordsSearchPage();
|
||||
},
|
||||
AppHotkeyScope.KeyboardShortcutMenu,
|
||||
[openRecordsSearchPage],
|
||||
{
|
||||
ignoreModifiers: true,
|
||||
},
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Escape],
|
||||
() => {
|
||||
|
||||
@ -10,9 +10,10 @@ export const useMatchCommands = ({
|
||||
const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300); // 200ms - 500ms
|
||||
|
||||
const checkInShortcuts = (cmd: Command, search: string) => {
|
||||
return (cmd.firstHotKey + (cmd.secondHotKey ?? ''))
|
||||
const concatenatedString = cmd.hotKeys?.join('') ?? '';
|
||||
return concatenatedString
|
||||
.toLowerCase()
|
||||
.includes(search.toLowerCase());
|
||||
.includes(search.toLowerCase().trim());
|
||||
};
|
||||
|
||||
const checkInLabels = (cmd: Command, search: string) => {
|
||||
|
||||
@ -12,24 +12,21 @@ export const useMatchingCommandMenuCommands = ({
|
||||
copilotCommands,
|
||||
navigateCommands,
|
||||
actionRecordSelectionCommands,
|
||||
actionObjectCommands,
|
||||
actionGlobalCommands,
|
||||
workflowRunRecordSelectionCommands,
|
||||
workflowRunGlobalCommands,
|
||||
peopleCommands,
|
||||
companyCommands,
|
||||
opportunityCommands,
|
||||
noteCommands,
|
||||
tasksCommands,
|
||||
customObjectCommands,
|
||||
isLoading,
|
||||
} = useCommandMenuCommands();
|
||||
|
||||
const matchingNavigateCommand = matchCommands(navigateCommands);
|
||||
const matchingNavigateCommands = matchCommands(navigateCommands);
|
||||
|
||||
const matchingStandardActionRecordSelectionCommands = matchCommands(
|
||||
actionRecordSelectionCommands,
|
||||
);
|
||||
|
||||
const matchingStandardActionObjectCommands =
|
||||
matchCommands(actionObjectCommands);
|
||||
|
||||
const matchingStandardActionGlobalCommands =
|
||||
matchCommands(actionGlobalCommands);
|
||||
|
||||
@ -41,33 +38,22 @@ export const useMatchingCommandMenuCommands = ({
|
||||
workflowRunGlobalCommands,
|
||||
);
|
||||
|
||||
const isNoResults =
|
||||
const noResults =
|
||||
!matchingStandardActionRecordSelectionCommands.length &&
|
||||
!matchingWorkflowRunRecordSelectionCommands.length &&
|
||||
!matchingStandardActionGlobalCommands.length &&
|
||||
!matchingWorkflowRunGlobalCommands.length &&
|
||||
!matchingNavigateCommand.length &&
|
||||
!peopleCommands?.length &&
|
||||
!companyCommands?.length &&
|
||||
!opportunityCommands?.length &&
|
||||
!noteCommands?.length &&
|
||||
!tasksCommands?.length &&
|
||||
!customObjectCommands?.length;
|
||||
!matchingStandardActionObjectCommands.length &&
|
||||
!matchingNavigateCommands.length;
|
||||
|
||||
return {
|
||||
isNoResults,
|
||||
isLoading,
|
||||
noResults,
|
||||
copilotCommands,
|
||||
matchingStandardActionRecordSelectionCommands,
|
||||
matchingStandardActionObjectCommands,
|
||||
matchingWorkflowRunRecordSelectionCommands,
|
||||
matchingStandardActionGlobalCommands,
|
||||
matchingWorkflowRunGlobalCommands,
|
||||
matchingNavigateCommand,
|
||||
peopleCommands,
|
||||
companyCommands,
|
||||
opportunityCommands,
|
||||
noteCommands,
|
||||
tasksCommands,
|
||||
customObjectCommands,
|
||||
matchingNavigateCommands,
|
||||
};
|
||||
};
|
||||
|
||||
@ -0,0 +1,229 @@
|
||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { Company } from '@/companies/types/Company';
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainName';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { useMultiObjectSearch } from '@/object-record/relation-picker/hooks/useMultiObjectSearch';
|
||||
import { useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap } from '@/object-record/relation-picker/hooks/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap';
|
||||
import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Avatar, IconCheckbox, IconNotes } from 'twenty-ui';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { getLogoUrlFromDomainName } from '~/utils';
|
||||
|
||||
const MAX_SEARCH_RESULTS_PER_OBJECT = 8;
|
||||
|
||||
export const useSearchRecords = () => {
|
||||
const commandMenuSearch = useRecoilValue(commandMenuSearchState);
|
||||
|
||||
const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300);
|
||||
|
||||
const {
|
||||
matchesSearchFilterObjectRecordsQueryResult,
|
||||
matchesSearchFilterObjectRecordsLoading: loading,
|
||||
} = useMultiObjectSearch({
|
||||
excludedObjects: [CoreObjectNameSingular.Task, CoreObjectNameSingular.Note],
|
||||
searchFilterValue: deferredCommandMenuSearch ?? undefined,
|
||||
limit: MAX_SEARCH_RESULTS_PER_OBJECT,
|
||||
});
|
||||
|
||||
const { objectRecordsMap: matchesSearchFilterObjectRecords } =
|
||||
useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap({
|
||||
multiObjectRecordsQueryResult:
|
||||
matchesSearchFilterObjectRecordsQueryResult,
|
||||
});
|
||||
|
||||
const { loading: isNotesLoading, records: notes } = useFindManyRecords<Note>({
|
||||
objectNameSingular: CoreObjectNameSingular.Note,
|
||||
filter: deferredCommandMenuSearch
|
||||
? makeOrFilterVariables([
|
||||
{ title: { ilike: `%${deferredCommandMenuSearch}%` } },
|
||||
{ body: { ilike: `%${deferredCommandMenuSearch}%` } },
|
||||
])
|
||||
: undefined,
|
||||
limit: MAX_SEARCH_RESULTS_PER_OBJECT,
|
||||
});
|
||||
|
||||
const { loading: isTasksLoading, records: tasks } = useFindManyRecords<Task>({
|
||||
objectNameSingular: CoreObjectNameSingular.Task,
|
||||
filter: deferredCommandMenuSearch
|
||||
? makeOrFilterVariables([
|
||||
{ title: { ilike: `%${deferredCommandMenuSearch}%` } },
|
||||
{ body: { ilike: `%${deferredCommandMenuSearch}%` } },
|
||||
])
|
||||
: undefined,
|
||||
limit: MAX_SEARCH_RESULTS_PER_OBJECT,
|
||||
});
|
||||
|
||||
const people = matchesSearchFilterObjectRecords.people?.map(
|
||||
(people) => people.record,
|
||||
);
|
||||
const companies = matchesSearchFilterObjectRecords.companies?.map(
|
||||
(companies) => companies.record,
|
||||
);
|
||||
const opportunities = matchesSearchFilterObjectRecords.opportunities?.map(
|
||||
(opportunities) => opportunities.record,
|
||||
);
|
||||
|
||||
const peopleCommands = useMemo(
|
||||
() =>
|
||||
people?.map(({ id, name: { firstName, lastName }, avatarUrl }) => ({
|
||||
id,
|
||||
label: `${firstName} ${lastName}`,
|
||||
to: `object/person/${id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
type="rounded"
|
||||
avatarUrl={avatarUrl}
|
||||
placeholderColorSeed={id}
|
||||
placeholder={`${firstName} ${lastName}`}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
[people],
|
||||
);
|
||||
|
||||
const companyCommands = useMemo(
|
||||
() =>
|
||||
companies?.map((company) => ({
|
||||
id: company.id,
|
||||
label: company.name ?? '',
|
||||
to: `object/company/${company.id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
placeholderColorSeed={company.id}
|
||||
placeholder={company.name}
|
||||
avatarUrl={getLogoUrlFromDomainName(
|
||||
getCompanyDomainName(company as Company),
|
||||
)}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
[companies],
|
||||
);
|
||||
|
||||
const opportunityCommands = useMemo(
|
||||
() =>
|
||||
opportunities?.map(({ id, name }) => ({
|
||||
id,
|
||||
label: name ?? '',
|
||||
to: `object/opportunity/${id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
type="rounded"
|
||||
avatarUrl={null}
|
||||
placeholderColorSeed={id}
|
||||
placeholder={name ?? ''}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
[opportunities],
|
||||
);
|
||||
|
||||
const openNoteRightDrawer = useOpenActivityRightDrawer({
|
||||
objectNameSingular: CoreObjectNameSingular.Note,
|
||||
});
|
||||
|
||||
const openTaskRightDrawer = useOpenActivityRightDrawer({
|
||||
objectNameSingular: CoreObjectNameSingular.Task,
|
||||
});
|
||||
|
||||
const noteCommands = useMemo(
|
||||
() =>
|
||||
notes?.map((note) => ({
|
||||
id: note.id,
|
||||
label: note.title ?? '',
|
||||
to: '',
|
||||
onCommandClick: () => openNoteRightDrawer(note.id),
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: IconNotes,
|
||||
})),
|
||||
[notes, openNoteRightDrawer],
|
||||
);
|
||||
|
||||
const tasksCommands = useMemo(
|
||||
() =>
|
||||
tasks?.map((task) => ({
|
||||
id: task.id,
|
||||
label: task.title ?? '',
|
||||
to: '',
|
||||
onCommandClick: () => openTaskRightDrawer(task.id),
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: IconCheckbox,
|
||||
})),
|
||||
[tasks, openTaskRightDrawer],
|
||||
);
|
||||
|
||||
const customObjectRecordsMap = useMemo(() => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(matchesSearchFilterObjectRecords).filter(
|
||||
([namePlural, records]) =>
|
||||
![
|
||||
CoreObjectNamePlural.Person,
|
||||
CoreObjectNamePlural.Opportunity,
|
||||
CoreObjectNamePlural.Company,
|
||||
].includes(namePlural as CoreObjectNamePlural) && !isEmpty(records),
|
||||
),
|
||||
);
|
||||
}, [matchesSearchFilterObjectRecords]);
|
||||
|
||||
const customObjectCommands = useMemo(() => {
|
||||
return Object.values(customObjectRecordsMap).flatMap((objectRecords) =>
|
||||
objectRecords.map((objectRecord) => ({
|
||||
id: objectRecord.record.id,
|
||||
label: objectRecord.recordIdentifier.name,
|
||||
to: `object/${objectRecord.objectMetadataItem.nameSingular}/${objectRecord.record.id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
type="rounded"
|
||||
avatarUrl={objectRecord.record.avatarUrl}
|
||||
placeholderColorSeed={objectRecord.record.id}
|
||||
placeholder={objectRecord.recordIdentifier.name ?? ''}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
);
|
||||
}, [customObjectRecordsMap]);
|
||||
|
||||
const commands = [
|
||||
...(peopleCommands ?? []),
|
||||
...(companyCommands ?? []),
|
||||
...(opportunityCommands ?? []),
|
||||
...(noteCommands ?? []),
|
||||
...(tasksCommands ?? []),
|
||||
...(customObjectCommands ?? []),
|
||||
];
|
||||
|
||||
const noResults =
|
||||
!peopleCommands?.length &&
|
||||
!companyCommands?.length &&
|
||||
!opportunityCommands?.length &&
|
||||
!noteCommands?.length &&
|
||||
!tasksCommands?.length &&
|
||||
!customObjectCommands?.length;
|
||||
|
||||
return {
|
||||
loading: loading || isNotesLoading || isTasksLoading,
|
||||
noResults,
|
||||
commandGroups: [
|
||||
{
|
||||
heading: t`Results`,
|
||||
items: commands,
|
||||
},
|
||||
],
|
||||
hasMore: false,
|
||||
pageSize: 0,
|
||||
onLoadMore: () => {},
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,20 @@
|
||||
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
|
||||
import { useSearchRecords } from '@/command-menu/hooks/useSearchRecords';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const CommandMenuSearchRecordsPage = () => {
|
||||
const { commandGroups, loading, noResults } = useSearchRecords();
|
||||
|
||||
const selectableItemIds = useMemo(() => {
|
||||
return commandGroups.flatMap((group) => group.items).map((item) => item.id);
|
||||
}, [commandGroups]);
|
||||
|
||||
return (
|
||||
<CommandMenuList
|
||||
commandGroups={commandGroups}
|
||||
selectableItemIds={selectableItemIds}
|
||||
loading={loading}
|
||||
noResults={noResults}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { createState } from '@ui/utilities/state/utils/createState';
|
||||
|
||||
export const commandMenuPageState = createState<CommandMenuPages>({
|
||||
|
||||
@ -9,6 +9,7 @@ export enum CommandType {
|
||||
export enum CommandScope {
|
||||
Global = 'Global',
|
||||
RecordSelection = 'RecordSelection',
|
||||
Object = 'Object',
|
||||
}
|
||||
|
||||
export type Command = {
|
||||
@ -18,8 +19,7 @@ export type Command = {
|
||||
type?: CommandType;
|
||||
scope?: CommandScope;
|
||||
Icon?: IconComponent;
|
||||
firstHotKey?: string;
|
||||
secondHotKey?: string;
|
||||
hotKeys?: string[];
|
||||
onCommandClick?: () => void;
|
||||
shouldCloseCommandMenuOnClick?: boolean;
|
||||
};
|
||||
|
||||
@ -8,4 +8,5 @@ export enum CommandMenuPages {
|
||||
WorkflowStepSelectAction = 'workflow-step-select-action',
|
||||
WorkflowStepView = 'workflow-step-view',
|
||||
WorkflowStepEdit = 'workflow-step-edit',
|
||||
SearchRecords = 'search-records',
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { IconSearch, IconSettings, getOsControlSymbol } from 'twenty-ui';
|
||||
import { IconSearch, IconSettings } from 'twenty-ui';
|
||||
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { CurrentWorkspaceMemberFavoritesFolders } from '@/favorites/components/CurrentWorkspaceMemberFavoritesFolders';
|
||||
@ -28,7 +28,6 @@ const StyledInnerContainer = styled.div`
|
||||
|
||||
export const MainNavigationDrawerItems = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const { toggleCommandMenu } = useCommandMenu();
|
||||
const location = useLocation();
|
||||
const setNavigationMemorizedUrl = useSetRecoilState(
|
||||
navigationMemorizedUrlState,
|
||||
@ -42,6 +41,8 @@ export const MainNavigationDrawerItems = () => {
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
const { openRecordsSearchPage } = useCommandMenu();
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isMobile && (
|
||||
@ -49,8 +50,8 @@ export const MainNavigationDrawerItems = () => {
|
||||
<NavigationDrawerItem
|
||||
label={t`Search`}
|
||||
Icon={IconSearch}
|
||||
onClick={toggleCommandMenu}
|
||||
keyboard={[getOsControlSymbol(), 'K']}
|
||||
onClick={openRecordsSearchPage}
|
||||
keyboard={['/']}
|
||||
/>
|
||||
<NavigationDrawerItem
|
||||
label={t`Settings`}
|
||||
|
||||
@ -17,7 +17,7 @@ type NavigationBarItemName = 'main' | 'search' | 'tasks' | 'settings';
|
||||
|
||||
export const MobileNavigationBar = () => {
|
||||
const [isCommandMenuOpened] = useRecoilState(isCommandMenuOpenedState);
|
||||
const { closeCommandMenu, openCommandMenu } = useCommandMenu();
|
||||
const { closeCommandMenu, openRecordsSearchPage } = useCommandMenu();
|
||||
const isSettingsPage = useIsSettingsPage();
|
||||
const [isNavigationDrawerExpanded, setIsNavigationDrawerExpanded] =
|
||||
useRecoilState(isNavigationDrawerExpandedState);
|
||||
@ -53,12 +53,7 @@ export const MobileNavigationBar = () => {
|
||||
{
|
||||
name: 'search',
|
||||
Icon: IconSearch,
|
||||
onClick: () => {
|
||||
if (!isCommandMenuOpened) {
|
||||
openCommandMenu();
|
||||
}
|
||||
setIsNavigationDrawerExpanded(false);
|
||||
},
|
||||
onClick: openRecordsSearchPage,
|
||||
},
|
||||
{
|
||||
name: 'settings',
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||
|
||||
export const mapRightDrawerPageToCommandMenuPage = (
|
||||
|
||||
@ -14,6 +14,7 @@ import { useRecoilState } from 'recoil';
|
||||
import { capitalize } from 'twenty-shared';
|
||||
import {
|
||||
IconComponent,
|
||||
Label,
|
||||
MOBILE_VIEWPORT,
|
||||
Pill,
|
||||
TablerIconsProps,
|
||||
@ -163,13 +164,16 @@ const StyledItemCount = styled.span`
|
||||
|
||||
const StyledKeyBoardShortcut = styled.span`
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
height: ${({ theme }) => theme.spacing(4)};
|
||||
justify-content: center;
|
||||
letter-spacing: 1px;
|
||||
margin-left: auto;
|
||||
visibility: hidden;
|
||||
width: ${({ theme }) => theme.spacing(4)};
|
||||
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.strong};
|
||||
background: ${({ theme }) => theme.background.transparent.lighter};
|
||||
`;
|
||||
|
||||
const StyledNavigationDrawerItemContainer = styled.div`
|
||||
@ -339,7 +343,7 @@ export const NavigationDrawerItem = ({
|
||||
{keyboard && (
|
||||
<NavigationDrawerAnimatedCollapseWrapper>
|
||||
<StyledKeyBoardShortcut className="keyboard-shortcuts">
|
||||
{keyboard}
|
||||
<Label>{keyboard}</Label>
|
||||
</StyledKeyBoardShortcut>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
)}
|
||||
|
||||
@ -32,6 +32,10 @@ export const useScopedHotkeys = (
|
||||
? options.preventDefault === true
|
||||
: true;
|
||||
|
||||
const ignoreModifiers = isDefined(options?.ignoreModifiers)
|
||||
? options.ignoreModifiers === true
|
||||
: false;
|
||||
|
||||
return useHotkeys(
|
||||
keys,
|
||||
(keyboardEvent, hotkeysEvent) => {
|
||||
@ -52,6 +56,7 @@ export const useScopedHotkeys = (
|
||||
{
|
||||
enableOnContentEditable,
|
||||
enableOnFormTags,
|
||||
ignoreModifiers,
|
||||
},
|
||||
dependencies,
|
||||
);
|
||||
|
||||
@ -1,17 +1,11 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"POT-Creation-Date: 2025-01-28 21:09+0100\n"
|
||||
"POT-Creation-Date: 2025-01-29 18:14+0100\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: @lingui/cli\n"
|
||||
"Language: en\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#: src/modules/view/standard-objects/view-field.workspace-entity.ts:32
|
||||
msgid "(System) View Fields"
|
||||
@ -916,10 +910,6 @@ msgstr "Ideal Customer Profile: Indicates whether the company is the most suita
|
||||
msgid "If the event is related to a particular object"
|
||||
msgstr "If the event is related to a particular object"
|
||||
|
||||
#: src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts:94
|
||||
#~ msgid "inked Object Metadata Id"
|
||||
#~ msgstr "inked Object Metadata Id"
|
||||
|
||||
#: src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts:48
|
||||
#: src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts:49
|
||||
msgid "Is canceled"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -66,8 +66,7 @@ const StyledMenuItemCommandContainer = styled.div<{ isSelected?: boolean }>`
|
||||
export type MenuItemCommandProps = {
|
||||
LeftIcon?: IconComponent;
|
||||
text: string;
|
||||
firstHotKey?: string;
|
||||
secondHotKey?: string;
|
||||
hotKeys?: string[];
|
||||
className?: string;
|
||||
isSelected?: boolean;
|
||||
onClick?: () => void;
|
||||
@ -77,8 +76,7 @@ export type MenuItemCommandProps = {
|
||||
export const MenuItemCommand = ({
|
||||
LeftIcon,
|
||||
text,
|
||||
firstHotKey,
|
||||
secondHotKey,
|
||||
hotKeys,
|
||||
className,
|
||||
isSelected,
|
||||
onClick,
|
||||
@ -102,12 +100,7 @@ export const MenuItemCommand = ({
|
||||
<StyledMenuItemLabelText>{text}</StyledMenuItemLabelText>
|
||||
{RightComponent}
|
||||
</StyledMenuItemLeftContent>
|
||||
{!isMobile && (
|
||||
<MenuItemCommandHotKeys
|
||||
firstHotKey={firstHotKey}
|
||||
secondHotKey={secondHotKey}
|
||||
/>
|
||||
)}
|
||||
{!isMobile && <MenuItemCommandHotKeys hotKeys={hotKeys} />}
|
||||
</StyledMenuItemCommandContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -34,27 +34,24 @@ const StyledCommandKey = styled.div`
|
||||
`;
|
||||
|
||||
export type MenuItemCommandHotKeysProps = {
|
||||
firstHotKey?: string;
|
||||
hotKeys?: string[];
|
||||
joinLabel?: string;
|
||||
secondHotKey?: string;
|
||||
};
|
||||
|
||||
export const MenuItemCommandHotKeys = ({
|
||||
firstHotKey,
|
||||
secondHotKey,
|
||||
hotKeys,
|
||||
joinLabel = 'then',
|
||||
}: MenuItemCommandHotKeysProps) => {
|
||||
return (
|
||||
<StyledCommandText>
|
||||
{firstHotKey && (
|
||||
{hotKeys && (
|
||||
<StyledCommandTextContainer>
|
||||
<StyledCommandKey>{firstHotKey}</StyledCommandKey>
|
||||
{secondHotKey && (
|
||||
{hotKeys.map((hotKey, index) => (
|
||||
<>
|
||||
{joinLabel}
|
||||
<StyledCommandKey>{secondHotKey}</StyledCommandKey>
|
||||
<StyledCommandKey key={index}>{hotKey}</StyledCommandKey>
|
||||
{index < hotKeys.length - 1 && joinLabel}
|
||||
</>
|
||||
)}
|
||||
))}
|
||||
</StyledCommandTextContainer>
|
||||
)}
|
||||
</StyledCommandText>
|
||||
|
||||
@ -20,15 +20,13 @@ type Story = StoryObj<typeof MenuItemCommand>;
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
text: 'First option',
|
||||
firstHotKey: '⌘',
|
||||
secondHotKey: '1',
|
||||
hotKeys: ['⌘', '1'],
|
||||
},
|
||||
render: (props) => (
|
||||
<MenuItemCommand
|
||||
LeftIcon={props.LeftIcon}
|
||||
text={props.text}
|
||||
firstHotKey={props.firstHotKey}
|
||||
secondHotKey={props.secondHotKey}
|
||||
hotKeys={props.hotKeys}
|
||||
className={props.className}
|
||||
onClick={props.onClick}
|
||||
isSelected={false}
|
||||
@ -40,8 +38,7 @@ export const Default: Story = {
|
||||
export const Catalog: CatalogStory<Story, typeof MenuItemCommand> = {
|
||||
args: {
|
||||
text: 'Menu item',
|
||||
firstHotKey: '⌘',
|
||||
secondHotKey: '1',
|
||||
hotKeys: ['⌘', '1'],
|
||||
},
|
||||
argTypes: {
|
||||
className: { control: false },
|
||||
@ -85,8 +82,7 @@ export const Catalog: CatalogStory<Story, typeof MenuItemCommand> = {
|
||||
<MenuItemCommand
|
||||
LeftIcon={props.LeftIcon}
|
||||
text={props.text}
|
||||
firstHotKey={props.firstHotKey}
|
||||
secondHotKey={props.secondHotKey}
|
||||
hotKeys={props.hotKeys}
|
||||
className={props.className}
|
||||
onClick={props.onClick}
|
||||
isSelected={false}
|
||||
|
||||
Reference in New Issue
Block a user