Command menu refactoring (#9257)
Refactored the `CommandMenu` component to make it more readable and easier to refactor. The file was way too big so I introduced a few hooks and eliminated code duplication. Introduced: - `useMatchCommands` hook to match commands with the search - `useCommandMenuCommands` which returns all command menu commands - `useMatchingCommandMenuCommands` to return the commands matched with the search - `CommandMenuContainer` to simplify the `DefaultLayout` - Unmounted the `CommandMenu` when it wasn't opened to improve performances I also introduced a new behavior: Automatically select the first item when opening the command menu: https://github.com/user-attachments/assets/4b683d49-570e-47c9-8939-99f42ed8691c
This commit is contained in:
@ -1,8 +1,8 @@
|
||||
import { MultipleRecordsActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/multiple-records/components/MultipleRecordsActionMenuEntrySetterEffect';
|
||||
import { NoSelectionActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/no-selection/components/NoSelectionActionMenuEntrySetterEffect';
|
||||
import { ShowPageSingleRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/single-record/components/ShowPageSingleRecordActionMenuEntrySetterEffect';
|
||||
import { SingleRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetterEffect';
|
||||
import { WorkflowRunRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionMenuEntrySetter';
|
||||
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
|
||||
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
@ -67,14 +67,16 @@ const ActionEffects = ({
|
||||
contextStoreTargetedRecordsRule.selectedRecordIds.length === 1 && (
|
||||
<>
|
||||
{contextStoreCurrentViewType === ContextStoreViewType.ShowPage && (
|
||||
<ShowPageSingleRecordActionMenuEntrySetterEffect
|
||||
<SingleRecordActionMenuEntrySetterEffect
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
viewType={ActionViewType.SHOW_PAGE}
|
||||
/>
|
||||
)}
|
||||
{(contextStoreCurrentViewType === ContextStoreViewType.Table ||
|
||||
contextStoreCurrentViewType === ContextStoreViewType.Kanban) && (
|
||||
<SingleRecordActionMenuEntrySetterEffect
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
viewType={ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION}
|
||||
/>
|
||||
)}
|
||||
{isWorkflowEnabled && (
|
||||
|
||||
@ -1,78 +1,23 @@
|
||||
import { getActionConfig } from '@/action-menu/actions/record-actions/single-record/utils/getActionConfig';
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn';
|
||||
import { wrapActionInCallbacks } from '@/action-menu/actions/utils/wrapActionInCallbacks';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { useActionMenuEntriesWithCallbacks } from '@/action-menu/hooks/useActionMenuEntriesWithCallbacks';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const SingleRecordActionMenuEntrySetterEffect = ({
|
||||
objectMetadataItem,
|
||||
viewType,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
viewType: ActionViewType;
|
||||
}) => {
|
||||
const isPageHeaderV2Enabled = useIsFeatureEnabled(
|
||||
'IS_PAGE_HEADER_V2_ENABLED',
|
||||
);
|
||||
|
||||
const actionConfig = getActionConfig(
|
||||
objectMetadataItem,
|
||||
isPageHeaderV2Enabled,
|
||||
);
|
||||
|
||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
||||
|
||||
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
const { actionMenuEntries } = useActionMenuEntriesWithCallbacks(
|
||||
objectMetadataItem,
|
||||
viewType,
|
||||
);
|
||||
|
||||
const selectedRecordId =
|
||||
contextStoreTargetedRecordsRule.mode === 'selection'
|
||||
? contextStoreTargetedRecordsRule.selectedRecordIds[0]
|
||||
: undefined;
|
||||
|
||||
if (!isDefined(selectedRecordId)) {
|
||||
throw new Error('Selected record ID is required');
|
||||
}
|
||||
|
||||
const { onActionStartedCallback, onActionExecutedCallback } =
|
||||
useContext(ActionMenuContext);
|
||||
|
||||
const actionMenuEntries = Object.values(actionConfig ?? {})
|
||||
.filter((action) =>
|
||||
action.availableOn?.includes(
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
),
|
||||
)
|
||||
.map((action) => {
|
||||
const { shouldBeRegistered, onClick, ConfirmationModal } =
|
||||
action.actionHook({
|
||||
recordId: selectedRecordId,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
if (!shouldBeRegistered) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const wrappedAction = wrapActionInCallbacks({
|
||||
action: {
|
||||
...action,
|
||||
onClick,
|
||||
ConfirmationModal,
|
||||
},
|
||||
onActionStartedCallback,
|
||||
onActionExecutedCallback,
|
||||
});
|
||||
|
||||
return wrappedAction;
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
useEffect(() => {
|
||||
for (const action of actionMenuEntries) {
|
||||
addActionMenuEntry(action);
|
||||
|
||||
@ -2,7 +2,7 @@ import { useAddToFavoritesSingleRecordAction } from '@/action-menu/actions/recor
|
||||
import { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction';
|
||||
import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction';
|
||||
import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey';
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn';
|
||||
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
|
||||
import { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import {
|
||||
ActionMenuEntry,
|
||||
@ -25,8 +25,8 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
|
||||
position: 0,
|
||||
Icon: IconHeart,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useAddToFavoritesSingleRecordAction,
|
||||
},
|
||||
@ -38,8 +38,8 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
|
||||
position: 1,
|
||||
Icon: IconHeartOff,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
||||
},
|
||||
@ -53,8 +53,8 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
|
||||
accent: 'danger',
|
||||
isPinned: true,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useDeleteSingleRecordAction,
|
||||
},
|
||||
|
||||
@ -6,7 +6,7 @@ import { useNavigateToNextRecordSingleRecordAction } from '@/action-menu/actions
|
||||
import { useNavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToPreviousRecordSingleRecordAction';
|
||||
import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction';
|
||||
import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey';
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn';
|
||||
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
|
||||
import { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import {
|
||||
ActionMenuEntry,
|
||||
@ -38,7 +38,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
||||
position: 0,
|
||||
isPinned: false,
|
||||
Icon: IconFileExport,
|
||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
actionHook: useExportNoteAction,
|
||||
},
|
||||
addToFavoritesSingleRecord: {
|
||||
@ -51,8 +51,8 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
||||
isPinned: true,
|
||||
Icon: IconHeart,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useAddToFavoritesSingleRecordAction,
|
||||
},
|
||||
@ -66,8 +66,8 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
||||
position: 2,
|
||||
Icon: IconHeartOff,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
||||
},
|
||||
@ -82,8 +82,8 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
||||
accent: 'danger',
|
||||
isPinned: true,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useDeleteSingleRecordAction,
|
||||
},
|
||||
@ -98,8 +98,8 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
||||
accent: 'danger',
|
||||
isPinned: true,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useDestroySingleRecordAction,
|
||||
},
|
||||
@ -112,7 +112,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
||||
position: 5,
|
||||
isPinned: true,
|
||||
Icon: IconChevronUp,
|
||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
actionHook: useNavigateToPreviousRecordSingleRecordAction,
|
||||
},
|
||||
navigateToNextRecord: {
|
||||
@ -124,7 +124,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
||||
position: 6,
|
||||
isPinned: true,
|
||||
Icon: IconChevronDown,
|
||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
actionHook: useNavigateToNextRecordSingleRecordAction,
|
||||
},
|
||||
};
|
||||
|
||||
@ -14,7 +14,7 @@ import { useSeeRunsWorkflowSingleRecordAction } from '@/action-menu/actions/reco
|
||||
import { useSeeVersionsWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeVersionsWorkflowSingleRecordAction';
|
||||
import { useTestWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useTestWorkflowSingleRecordAction';
|
||||
import { WorkflowSingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/workflow-actions/types/WorkflowSingleRecordActionsKeys';
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn';
|
||||
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
|
||||
import { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import {
|
||||
ActionMenuEntry,
|
||||
@ -51,8 +51,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useActivateDraftWorkflowSingleRecordAction,
|
||||
},
|
||||
@ -66,8 +66,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useActivateLastPublishedVersionWorkflowSingleRecordAction,
|
||||
},
|
||||
@ -81,8 +81,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useDeactivateWorkflowSingleRecordAction,
|
||||
},
|
||||
@ -96,8 +96,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useDiscardDraftWorkflowSingleRecordAction,
|
||||
},
|
||||
@ -111,8 +111,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useSeeActiveVersionWorkflowSingleRecordAction,
|
||||
},
|
||||
@ -126,8 +126,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useSeeRunsWorkflowSingleRecordAction,
|
||||
},
|
||||
@ -141,8 +141,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useSeeVersionsWorkflowSingleRecordAction,
|
||||
},
|
||||
@ -156,8 +156,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useTestWorkflowSingleRecordAction,
|
||||
},
|
||||
@ -169,7 +169,7 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
shortLabel: '',
|
||||
position: 9,
|
||||
Icon: IconChevronUp,
|
||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
actionHook: useNavigateToPreviousRecordSingleRecordAction,
|
||||
},
|
||||
navigateToNextRecord: {
|
||||
@ -180,7 +180,7 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
shortLabel: '',
|
||||
position: 10,
|
||||
Icon: IconChevronDown,
|
||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
actionHook: useNavigateToNextRecordSingleRecordAction,
|
||||
},
|
||||
addToFavoritesSingleRecord: {
|
||||
@ -193,8 +193,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
isPinned: false,
|
||||
Icon: IconHeart,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useAddToFavoritesSingleRecordAction,
|
||||
},
|
||||
@ -208,8 +208,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
position: 12,
|
||||
Icon: IconHeartOff,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
||||
},
|
||||
@ -224,8 +224,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
accent: 'danger',
|
||||
isPinned: false,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useDeleteSingleRecordAction,
|
||||
},
|
||||
@ -240,8 +240,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
accent: 'danger',
|
||||
isPinned: false,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useDestroySingleRecordAction,
|
||||
},
|
||||
|
||||
@ -3,7 +3,7 @@ import { useNavigateToNextRecordSingleRecordAction } from '@/action-menu/actions
|
||||
import { useNavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToPreviousRecordSingleRecordAction';
|
||||
import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction';
|
||||
import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey';
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn';
|
||||
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
|
||||
import { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import {
|
||||
ActionMenuEntry,
|
||||
@ -33,8 +33,8 @@ export const WORKFLOW_RUNS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
isPinned: true,
|
||||
Icon: IconHeart,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useAddToFavoritesSingleRecordAction,
|
||||
},
|
||||
@ -48,8 +48,8 @@ export const WORKFLOW_RUNS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
position: 1,
|
||||
Icon: IconHeartOff,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
||||
},
|
||||
@ -62,7 +62,7 @@ export const WORKFLOW_RUNS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
position: 2,
|
||||
isPinned: true,
|
||||
Icon: IconChevronUp,
|
||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
actionHook: useNavigateToPreviousRecordSingleRecordAction,
|
||||
},
|
||||
navigateToNextRecord: {
|
||||
@ -74,7 +74,7 @@ export const WORKFLOW_RUNS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
position: 3,
|
||||
isPinned: true,
|
||||
Icon: IconChevronDown,
|
||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
actionHook: useNavigateToNextRecordSingleRecordAction,
|
||||
},
|
||||
};
|
||||
|
||||
@ -7,7 +7,7 @@ import { useSeeRunsWorkflowVersionSingleRecordAction } from '@/action-menu/actio
|
||||
import { useSeeVersionsWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeVersionsWorkflowVersionSingleRecordAction';
|
||||
import { useUseAsDraftWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useUseAsDraftWorkflowVersionSingleRecordAction';
|
||||
import { WorkflowVersionSingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/types/WorkflowVersionSingleRecordActionsKeys';
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn';
|
||||
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
|
||||
import { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import {
|
||||
ActionMenuEntry,
|
||||
@ -40,8 +40,8 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
Icon: IconPencil,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useUseAsDraftWorkflowVersionSingleRecordAction,
|
||||
},
|
||||
@ -54,8 +54,8 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
Icon: IconHistoryToggle,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useSeeRunsWorkflowVersionSingleRecordAction,
|
||||
},
|
||||
@ -68,8 +68,8 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
Icon: IconHistory,
|
||||
availableOn: [
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useSeeVersionsWorkflowVersionSingleRecordAction,
|
||||
},
|
||||
@ -81,7 +81,7 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
shortLabel: '',
|
||||
position: 4,
|
||||
Icon: IconChevronUp,
|
||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
actionHook: useNavigateToPreviousRecordSingleRecordAction,
|
||||
},
|
||||
navigateToNextRecord: {
|
||||
@ -92,7 +92,7 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
shortLabel: '',
|
||||
position: 5,
|
||||
Icon: IconChevronDown,
|
||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
||||
availableOn: [ActionViewType.SHOW_PAGE],
|
||||
actionHook: useNavigateToNextRecordSingleRecordAction,
|
||||
},
|
||||
addToFavoritesSingleRecord: {
|
||||
@ -105,8 +105,8 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
isPinned: false,
|
||||
Icon: IconHeart,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useAddToFavoritesSingleRecordAction,
|
||||
},
|
||||
@ -120,8 +120,8 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
position: 7,
|
||||
Icon: IconHeartOff,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionViewType.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
||||
},
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export enum ActionAvailableOn {
|
||||
export enum ActionViewType {
|
||||
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,20 +1,18 @@
|
||||
import { getActionConfig } from '@/action-menu/actions/record-actions/single-record/utils/getActionConfig';
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn';
|
||||
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
|
||||
import { wrapActionInCallbacks } from '@/action-menu/actions/utils/wrapActionInCallbacks';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const ShowPageSingleRecordActionMenuEntrySetterEffect = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
export const useActionMenuEntriesWithCallbacks = (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
viewType: ActionViewType,
|
||||
) => {
|
||||
const isPageHeaderV2Enabled = useIsFeatureEnabled(
|
||||
'IS_PAGE_HEADER_V2_ENABLED',
|
||||
);
|
||||
@ -24,8 +22,6 @@ export const ShowPageSingleRecordActionMenuEntrySetterEffect = ({
|
||||
isPageHeaderV2Enabled,
|
||||
);
|
||||
|
||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
||||
|
||||
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
);
|
||||
@ -38,13 +34,12 @@ export const ShowPageSingleRecordActionMenuEntrySetterEffect = ({
|
||||
if (!isDefined(selectedRecordId)) {
|
||||
throw new Error('Selected record ID is required');
|
||||
}
|
||||
|
||||
const { onActionStartedCallback, onActionExecutedCallback } =
|
||||
useContext(ActionMenuContext);
|
||||
|
||||
const actionMenuEntries = Object.values(actionConfig ?? {})
|
||||
.filter((action) =>
|
||||
action.availableOn?.includes(ActionAvailableOn.SHOW_PAGE),
|
||||
)
|
||||
.filter((action) => action.availableOn?.includes(viewType))
|
||||
.map((action) => {
|
||||
const { shouldBeRegistered, onClick, ConfirmationModal } =
|
||||
action.actionHook({
|
||||
@ -70,17 +65,5 @@ export const ShowPageSingleRecordActionMenuEntrySetterEffect = ({
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
useEffect(() => {
|
||||
for (const action of actionMenuEntries) {
|
||||
addActionMenuEntry(action);
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (const action of actionMenuEntries) {
|
||||
removeActionMenuEntry(action.key);
|
||||
}
|
||||
};
|
||||
}, [actionMenuEntries, addActionMenuEntry, removeActionMenuEntry]);
|
||||
|
||||
return null;
|
||||
return { actionMenuEntries };
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn';
|
||||
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
|
||||
import { ConfirmationModalProps } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { MouseEvent, ReactElement } from 'react';
|
||||
import { IconComponent, MenuItemAccent } from 'twenty-ui';
|
||||
@ -23,7 +23,7 @@ export type ActionMenuEntry = {
|
||||
Icon: IconComponent;
|
||||
isPinned?: boolean;
|
||||
accent?: MenuItemAccent;
|
||||
availableOn?: ActionAvailableOn[];
|
||||
availableOn?: ActionViewType[];
|
||||
onClick?: (event?: MouseEvent<HTMLElement>) => void;
|
||||
ConfirmationModal?: ReactElement<ConfirmationModalProps>;
|
||||
};
|
||||
|
||||
@ -1,77 +1,29 @@
|
||||
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 { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||
import { CommandMenuDefaultSelectionEffect } from '@/command-menu/components/CommandMenuDefaultSelectionEffect';
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { CommandMenuTopBar } from '@/command-menu/components/CommandMenuTopBar';
|
||||
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 { commandMenuCommandsComponentSelector } from '@/command-menu/states/commandMenuCommandsSelector';
|
||||
import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
|
||||
import { useMatchingCommandMenuCommands } from '@/command-menu/hooks/useMatchingCommandMenuCommands';
|
||||
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 { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu';
|
||||
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 { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
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 { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import {
|
||||
Avatar,
|
||||
IconCheckbox,
|
||||
IconComponent,
|
||||
IconNotes,
|
||||
IconSparkles,
|
||||
isDefined,
|
||||
} from 'twenty-ui';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { getLogoUrlFromDomainName } from '~/utils';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
import { useRef } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
const MOBILE_NAVIGATION_BAR_HEIGHT = 64;
|
||||
|
||||
type CommandGroupConfig = {
|
||||
heading: string;
|
||||
items?: any[];
|
||||
renderItem: (item: any) => {
|
||||
id: string;
|
||||
Icon?: IconComponent;
|
||||
label: string;
|
||||
to?: string;
|
||||
onClick?: () => void;
|
||||
key?: string;
|
||||
firstHotKey?: string;
|
||||
secondHotKey?: string;
|
||||
shouldCloseCommandMenuOnClick?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const StyledCommandMenu = styled.div`
|
||||
@ -122,304 +74,16 @@ const StyledEmpty = styled.div`
|
||||
`;
|
||||
|
||||
export const CommandMenu = () => {
|
||||
const { toggleCommandMenu, onItemClick, closeCommandMenu } = useCommandMenu();
|
||||
const { onItemClick, closeCommandMenu } = useCommandMenu();
|
||||
const commandMenuRef = useRef<HTMLDivElement>(null);
|
||||
const openActivityRightDrawer = useOpenActivityRightDrawer({
|
||||
objectNameSingular: CoreObjectNameSingular.Note,
|
||||
});
|
||||
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
|
||||
|
||||
const [commandMenuSearch, setCommandMenuSearch] = useRecoilState(
|
||||
commandMenuSearchState,
|
||||
);
|
||||
const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300); // 200ms - 500ms
|
||||
const { closeKeyboardShortcutMenu } = useKeyboardShortcutMenu();
|
||||
|
||||
const setContextStoreTargetedRecordsRule = useSetRecoilComponentStateV2(
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
);
|
||||
|
||||
const setContextStoreNumberOfSelectedRecords = useSetRecoilComponentStateV2(
|
||||
contextStoreNumberOfSelectedRecordsComponentState,
|
||||
);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const commandMenuCommands = useRecoilComponentValueV2(
|
||||
commandMenuCommandsComponentSelector,
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
'ctrl+k,meta+k',
|
||||
() => {
|
||||
closeKeyboardShortcutMenu();
|
||||
toggleCommandMenu();
|
||||
},
|
||||
AppHotkeyScope.CommandMenu,
|
||||
[toggleCommandMenu],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Escape],
|
||||
() => {
|
||||
closeCommandMenu();
|
||||
},
|
||||
AppHotkeyScope.CommandMenuOpen,
|
||||
[closeCommandMenu],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Backspace, Key.Delete],
|
||||
() => {
|
||||
if (!isNonEmptyString(commandMenuSearch)) {
|
||||
setContextStoreTargetedRecordsRule({
|
||||
mode: 'selection',
|
||||
selectedRecordIds: [],
|
||||
});
|
||||
|
||||
setContextStoreNumberOfSelectedRecords(0);
|
||||
}
|
||||
},
|
||||
AppHotkeyScope.CommandMenuOpen,
|
||||
[closeCommandMenu],
|
||||
{
|
||||
preventDefault: false,
|
||||
},
|
||||
);
|
||||
|
||||
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 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 peopleCommands = useMemo(
|
||||
() =>
|
||||
people?.map(({ id, name: { firstName, lastName } }) => ({
|
||||
id,
|
||||
label: `${firstName} ${lastName}`,
|
||||
to: `object/person/${id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
})),
|
||||
[people],
|
||||
);
|
||||
|
||||
const companyCommands = useMemo(
|
||||
() =>
|
||||
companies?.map(({ id, name }) => ({
|
||||
id,
|
||||
label: name ?? '',
|
||||
to: `object/company/${id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
})),
|
||||
[companies],
|
||||
);
|
||||
|
||||
const opportunityCommands = useMemo(
|
||||
() =>
|
||||
opportunities?.map(({ id, name }) => ({
|
||||
id,
|
||||
label: name ?? '',
|
||||
to: `object/opportunity/${id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
})),
|
||||
[opportunities],
|
||||
);
|
||||
|
||||
const noteCommands = useMemo(
|
||||
() =>
|
||||
notes?.map((note) => ({
|
||||
id: note.id,
|
||||
label: note.title ?? '',
|
||||
to: '',
|
||||
onCommandClick: () => openActivityRightDrawer(note.id),
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
})),
|
||||
[notes, openActivityRightDrawer],
|
||||
);
|
||||
|
||||
const tasksCommands = useMemo(
|
||||
() =>
|
||||
tasks?.map((task) => ({
|
||||
id: task.id,
|
||||
label: task.title ?? '',
|
||||
to: '',
|
||||
onCommandClick: () => openActivityRightDrawer(task.id),
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
})),
|
||||
[tasks, openActivityRightDrawer],
|
||||
);
|
||||
|
||||
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,
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
return customObjectCommandsArray;
|
||||
}, [customObjectRecordsMap]);
|
||||
|
||||
const otherCommands = useMemo(() => {
|
||||
const commandsArray: Command[] = [];
|
||||
if (peopleCommands?.length > 0) {
|
||||
commandsArray.push(...(peopleCommands as Command[]));
|
||||
}
|
||||
if (companyCommands?.length > 0) {
|
||||
commandsArray.push(...(companyCommands as Command[]));
|
||||
}
|
||||
if (opportunityCommands?.length > 0) {
|
||||
commandsArray.push(...(opportunityCommands as Command[]));
|
||||
}
|
||||
if (noteCommands?.length > 0) {
|
||||
commandsArray.push(...(noteCommands as Command[]));
|
||||
}
|
||||
if (tasksCommands?.length > 0) {
|
||||
commandsArray.push(...(tasksCommands as Command[]));
|
||||
}
|
||||
if (customObjectCommands?.length > 0) {
|
||||
commandsArray.push(...(customObjectCommands as Command[]));
|
||||
}
|
||||
return commandsArray;
|
||||
}, [
|
||||
peopleCommands,
|
||||
companyCommands,
|
||||
opportunityCommands,
|
||||
noteCommands,
|
||||
customObjectCommands,
|
||||
tasksCommands,
|
||||
]);
|
||||
|
||||
const checkInShortcuts = (cmd: Command, search: string) => {
|
||||
return (cmd.firstHotKey + (cmd.secondHotKey ?? ''))
|
||||
.toLowerCase()
|
||||
.includes(search.toLowerCase());
|
||||
};
|
||||
|
||||
const checkInLabels = (cmd: Command, search: string) => {
|
||||
if (isNonEmptyString(cmd.label)) {
|
||||
return cmd.label.toLowerCase().includes(search.toLowerCase());
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const matchingNavigateCommand = commandMenuCommands.filter(
|
||||
(cmd) =>
|
||||
(deferredCommandMenuSearch.length > 0
|
||||
? checkInShortcuts(cmd, deferredCommandMenuSearch) ||
|
||||
checkInLabels(cmd, deferredCommandMenuSearch)
|
||||
: true) && cmd.type === CommandType.Navigate,
|
||||
);
|
||||
|
||||
const matchingCreateCommand = commandMenuCommands.filter(
|
||||
(cmd) =>
|
||||
(deferredCommandMenuSearch.length > 0
|
||||
? checkInShortcuts(cmd, deferredCommandMenuSearch) ||
|
||||
checkInLabels(cmd, deferredCommandMenuSearch)
|
||||
: true) && cmd.type === CommandType.Create,
|
||||
);
|
||||
|
||||
const matchingStandardActionRecordSelectionCommands =
|
||||
commandMenuCommands.filter(
|
||||
(cmd) =>
|
||||
(deferredCommandMenuSearch.length > 0
|
||||
? checkInShortcuts(cmd, deferredCommandMenuSearch) ||
|
||||
checkInLabels(cmd, deferredCommandMenuSearch)
|
||||
: true) &&
|
||||
cmd.type === CommandType.StandardAction &&
|
||||
cmd.scope === CommandScope.RecordSelection,
|
||||
);
|
||||
|
||||
const matchingStandardActionGlobalCommands = commandMenuCommands.filter(
|
||||
(cmd) =>
|
||||
(deferredCommandMenuSearch.length > 0
|
||||
? checkInShortcuts(cmd, deferredCommandMenuSearch) ||
|
||||
checkInLabels(cmd, deferredCommandMenuSearch)
|
||||
: true) &&
|
||||
cmd.type === CommandType.StandardAction &&
|
||||
cmd.scope === CommandScope.Global,
|
||||
);
|
||||
|
||||
const matchingWorkflowRunRecordSelectionCommands = commandMenuCommands.filter(
|
||||
(cmd) =>
|
||||
(deferredCommandMenuSearch.length > 0
|
||||
? checkInShortcuts(cmd, deferredCommandMenuSearch) ||
|
||||
checkInLabels(cmd, deferredCommandMenuSearch)
|
||||
: true) &&
|
||||
cmd.type === CommandType.WorkflowRun &&
|
||||
cmd.scope === CommandScope.RecordSelection,
|
||||
);
|
||||
|
||||
const matchingWorkflowRunGlobalCommands = commandMenuCommands.filter(
|
||||
(cmd) =>
|
||||
(deferredCommandMenuSearch.length > 0
|
||||
? checkInShortcuts(cmd, deferredCommandMenuSearch) ||
|
||||
checkInLabels(cmd, deferredCommandMenuSearch)
|
||||
: true) &&
|
||||
cmd.type === CommandType.WorkflowRun &&
|
||||
cmd.scope === CommandScope.Global,
|
||||
);
|
||||
useCommandMenuHotKeys();
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [commandMenuRef],
|
||||
@ -428,399 +92,165 @@ export const CommandMenu = () => {
|
||||
hotkeyScope: AppHotkeyScope.CommandMenuOpen,
|
||||
});
|
||||
|
||||
const isCopilotEnabled = useIsFeatureEnabled('IS_COPILOT_ENABLED');
|
||||
const setCopilotQuery = useSetRecoilState(copilotQueryState);
|
||||
const openCopilotRightDrawer = useOpenCopilotRightDrawer();
|
||||
const {
|
||||
isNoResults,
|
||||
isLoading,
|
||||
copilotCommands,
|
||||
matchingStandardActionRecordSelectionCommands,
|
||||
matchingWorkflowRunRecordSelectionCommands,
|
||||
matchingStandardActionGlobalCommands,
|
||||
matchingWorkflowRunGlobalCommands,
|
||||
matchingNavigateCommand,
|
||||
peopleCommands,
|
||||
companyCommands,
|
||||
opportunityCommands,
|
||||
noteCommands,
|
||||
tasksCommands,
|
||||
customObjectCommands,
|
||||
} = useMatchingCommandMenuCommands({
|
||||
commandMenuSearch,
|
||||
});
|
||||
|
||||
const copilotCommand: Command = {
|
||||
id: 'copilot',
|
||||
to: '', // TODO
|
||||
Icon: IconSparkles,
|
||||
label: 'Open Copilot',
|
||||
type: CommandType.Navigate,
|
||||
onCommandClick: () => {
|
||||
setCopilotQuery(deferredCommandMenuSearch);
|
||||
openCopilotRightDrawer();
|
||||
},
|
||||
};
|
||||
const selectableItems = copilotCommands
|
||||
.concat(matchingStandardActionRecordSelectionCommands)
|
||||
.concat(matchingWorkflowRunRecordSelectionCommands)
|
||||
.concat(matchingStandardActionGlobalCommands)
|
||||
.concat(matchingWorkflowRunGlobalCommands)
|
||||
.concat(matchingNavigateCommand)
|
||||
.concat(peopleCommands)
|
||||
.concat(companyCommands)
|
||||
.concat(opportunityCommands)
|
||||
.concat(noteCommands)
|
||||
.concat(tasksCommands)
|
||||
.concat(customObjectCommands)
|
||||
.filter(isDefined);
|
||||
|
||||
const copilotCommands: Command[] = isCopilotEnabled ? [copilotCommand] : [];
|
||||
|
||||
const selectableItemIds = copilotCommands
|
||||
.map((cmd) => cmd.id)
|
||||
.concat(matchingStandardActionRecordSelectionCommands.map((cmd) => cmd.id))
|
||||
.concat(matchingWorkflowRunRecordSelectionCommands.map((cmd) => cmd.id))
|
||||
.concat(matchingStandardActionGlobalCommands.map((cmd) => cmd.id))
|
||||
.concat(matchingWorkflowRunGlobalCommands.map((cmd) => cmd.id))
|
||||
.concat(matchingCreateCommand.map((cmd) => cmd.id))
|
||||
.concat(matchingNavigateCommand.map((cmd) => cmd.id))
|
||||
.concat(people?.map((person) => person.id))
|
||||
.concat(companies?.map((company) => company.id))
|
||||
.concat(opportunities?.map((opportunity) => opportunity.id))
|
||||
.concat(notes?.map((note) => note.id))
|
||||
.concat(tasks?.map((task) => task.id))
|
||||
.concat(
|
||||
Object.values(customObjectRecordsMap)
|
||||
?.map((objectRecords) =>
|
||||
objectRecords.map((objectRecord) => objectRecord.record.id),
|
||||
)
|
||||
.flat() ?? [],
|
||||
);
|
||||
|
||||
const isNoResults =
|
||||
!matchingStandardActionRecordSelectionCommands.length &&
|
||||
!matchingWorkflowRunRecordSelectionCommands.length &&
|
||||
!matchingStandardActionGlobalCommands.length &&
|
||||
!matchingWorkflowRunGlobalCommands.length &&
|
||||
!matchingCreateCommand.length &&
|
||||
!matchingNavigateCommand.length &&
|
||||
!people?.length &&
|
||||
!companies?.length &&
|
||||
!notes?.length &&
|
||||
!tasks?.length &&
|
||||
!opportunities?.length &&
|
||||
isEmpty(customObjectRecordsMap);
|
||||
|
||||
const isLoading = loading || isNotesLoading || isTasksLoading;
|
||||
const selectableItemIds = selectableItems.map((item) => item.id);
|
||||
|
||||
const commandGroups: CommandGroupConfig[] = [
|
||||
{
|
||||
heading: 'Navigate',
|
||||
items: matchingNavigateCommand,
|
||||
renderItem: (command) => ({
|
||||
id: command.id,
|
||||
Icon: command.Icon,
|
||||
label: command.label,
|
||||
to: command.to,
|
||||
onClick: command.onCommandClick,
|
||||
firstHotKey: command.firstHotKey,
|
||||
secondHotKey: command.secondHotKey,
|
||||
shouldCloseCommandMenuOnClick: command.shouldCloseCommandMenuOnClick,
|
||||
}),
|
||||
heading: 'Copilot',
|
||||
items: copilotCommands,
|
||||
},
|
||||
{
|
||||
heading: 'Other',
|
||||
items: matchingCreateCommand,
|
||||
renderItem: (command) => ({
|
||||
id: command.id,
|
||||
Icon: command.Icon,
|
||||
label: command.label,
|
||||
to: command.to,
|
||||
onClick: command.onCommandClick,
|
||||
firstHotKey: command.firstHotKey,
|
||||
secondHotKey: command.secondHotKey,
|
||||
shouldCloseCommandMenuOnClick: command.shouldCloseCommandMenuOnClick,
|
||||
}),
|
||||
heading: 'Record Selection',
|
||||
items: matchingStandardActionRecordSelectionCommands,
|
||||
},
|
||||
{
|
||||
heading: 'Workflow Record Selection',
|
||||
items: matchingWorkflowRunRecordSelectionCommands,
|
||||
},
|
||||
{
|
||||
heading: 'View',
|
||||
items: matchingStandardActionGlobalCommands,
|
||||
},
|
||||
{
|
||||
heading: 'Workflows',
|
||||
items: matchingWorkflowRunGlobalCommands,
|
||||
},
|
||||
{
|
||||
heading: 'Navigate',
|
||||
items: matchingNavigateCommand,
|
||||
},
|
||||
{
|
||||
heading: 'People',
|
||||
items: people,
|
||||
renderItem: (person) => ({
|
||||
id: person.id,
|
||||
label: `${person.name.firstName} ${person.name.lastName}`,
|
||||
to: `object/person/${person.id}`,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
type="rounded"
|
||||
avatarUrl={null}
|
||||
placeholderColorSeed={person.id}
|
||||
placeholder={`${person.name.firstName} ${person.name.lastName}`}
|
||||
/>
|
||||
),
|
||||
firstHotKey: person.firstHotKey,
|
||||
secondHotKey: person.secondHotKey,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
}),
|
||||
items: peopleCommands,
|
||||
},
|
||||
{
|
||||
heading: 'Companies',
|
||||
items: companies,
|
||||
renderItem: (company) => ({
|
||||
id: company.id,
|
||||
label: company.name,
|
||||
to: `object/company/${company.id}`,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
placeholderColorSeed={company.id}
|
||||
placeholder={company.name}
|
||||
avatarUrl={getLogoUrlFromDomainName(
|
||||
getCompanyDomainName(company as Company),
|
||||
)}
|
||||
/>
|
||||
),
|
||||
firstHotKey: company.firstHotKey,
|
||||
secondHotKey: company.secondHotKey,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
}),
|
||||
items: companyCommands,
|
||||
},
|
||||
{
|
||||
heading: 'Opportunities',
|
||||
items: opportunities,
|
||||
renderItem: (opportunity) => ({
|
||||
id: opportunity.id,
|
||||
label: opportunity.name ?? '',
|
||||
to: `object/opportunity/${opportunity.id}`,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
type="rounded"
|
||||
avatarUrl={null}
|
||||
placeholderColorSeed={opportunity.id}
|
||||
placeholder={opportunity.name ?? ''}
|
||||
/>
|
||||
),
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
}),
|
||||
items: opportunityCommands,
|
||||
},
|
||||
{
|
||||
heading: 'Notes',
|
||||
items: notes,
|
||||
renderItem: (note) => ({
|
||||
id: note.id,
|
||||
Icon: IconNotes,
|
||||
label: note.title ?? '',
|
||||
onClick: () => openActivityRightDrawer(note.id),
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
}),
|
||||
items: noteCommands,
|
||||
},
|
||||
{
|
||||
heading: 'Tasks',
|
||||
items: tasks,
|
||||
renderItem: (task) => ({
|
||||
id: task.id,
|
||||
Icon: IconCheckbox,
|
||||
label: task.title ?? '',
|
||||
onClick: () => openActivityRightDrawer(task.id),
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
}),
|
||||
items: tasksCommands,
|
||||
},
|
||||
{
|
||||
heading: 'Custom Objects',
|
||||
items: customObjectCommands,
|
||||
},
|
||||
...Object.entries(customObjectRecordsMap).map(
|
||||
([customObjectNamePlural, objectRecords]): CommandGroupConfig => ({
|
||||
heading: capitalize(customObjectNamePlural),
|
||||
items: objectRecords,
|
||||
renderItem: (objectRecord) => ({
|
||||
key: objectRecord.record.id,
|
||||
id: objectRecord.record.id,
|
||||
label: objectRecord.recordIdentifier.name,
|
||||
to: `object/${objectRecord.objectMetadataItem.nameSingular}/${objectRecord.record.id}`,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
type="rounded"
|
||||
avatarUrl={null}
|
||||
placeholderColorSeed={objectRecord.id}
|
||||
placeholder={objectRecord.recordIdentifier.name ?? ''}
|
||||
/>
|
||||
),
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{isCommandMenuOpened && (
|
||||
<StyledCommandMenu ref={commandMenuRef} className="command-menu">
|
||||
<CommandMenuTopBar
|
||||
commandMenuSearch={commandMenuSearch}
|
||||
setCommandMenuSearch={setCommandMenuSearch}
|
||||
/>
|
||||
<StyledList>
|
||||
<ScrollWrapper
|
||||
contextProviderName="commandMenu"
|
||||
componentInstanceId={`scroll-wrapper-command-menu`}
|
||||
>
|
||||
<StyledInnerList isMobile={isMobile}>
|
||||
<SelectableList
|
||||
selectableListId="command-menu-list"
|
||||
selectableItemIdArray={selectableItemIds}
|
||||
hotkeyScope={AppHotkeyScope.CommandMenu}
|
||||
onEnter={(itemId) => {
|
||||
const command = [
|
||||
...copilotCommands,
|
||||
...commandMenuCommands,
|
||||
...otherCommands,
|
||||
].find((cmd) => cmd.id === itemId);
|
||||
<CommandMenuDefaultSelectionEffect
|
||||
selectableItemIds={selectableItemIds}
|
||||
/>
|
||||
<StyledCommandMenu ref={commandMenuRef} className="command-menu">
|
||||
<CommandMenuTopBar
|
||||
commandMenuSearch={commandMenuSearch}
|
||||
setCommandMenuSearch={setCommandMenuSearch}
|
||||
/>
|
||||
<StyledList>
|
||||
<ScrollWrapper
|
||||
contextProviderName="commandMenu"
|
||||
componentInstanceId={`scroll-wrapper-command-menu`}
|
||||
>
|
||||
<StyledInnerList isMobile={isMobile}>
|
||||
<SelectableList
|
||||
selectableListId="command-menu-list"
|
||||
selectableItemIdArray={selectableItemIds}
|
||||
hotkeyScope={AppHotkeyScope.CommandMenu}
|
||||
onEnter={(itemId) => {
|
||||
const command = selectableItems.find(
|
||||
(item) => item.id === itemId,
|
||||
);
|
||||
|
||||
if (isDefined(command)) {
|
||||
const {
|
||||
to,
|
||||
onCommandClick,
|
||||
shouldCloseCommandMenuOnClick,
|
||||
} = command;
|
||||
if (isDefined(command)) {
|
||||
const {
|
||||
to,
|
||||
onCommandClick,
|
||||
shouldCloseCommandMenuOnClick,
|
||||
} = command;
|
||||
|
||||
onItemClick({
|
||||
shouldCloseCommandMenuOnClick,
|
||||
onClick: onCommandClick,
|
||||
to,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isNoResults && !isLoading && (
|
||||
<StyledEmpty>No results found</StyledEmpty>
|
||||
)}
|
||||
{isCopilotEnabled && (
|
||||
<CommandGroup heading="Copilot">
|
||||
<SelectableItem itemId={copilotCommand.id}>
|
||||
<CommandMenuItem
|
||||
id={copilotCommand.id}
|
||||
Icon={copilotCommand.Icon}
|
||||
label={`${copilotCommand.label} ${
|
||||
deferredCommandMenuSearch.length > 2
|
||||
? `"${deferredCommandMenuSearch}"`
|
||||
: ''
|
||||
}`}
|
||||
onClick={copilotCommand.onCommandClick}
|
||||
firstHotKey={copilotCommand.firstHotKey}
|
||||
secondHotKey={copilotCommand.secondHotKey}
|
||||
/>
|
||||
</SelectableItem>
|
||||
</CommandGroup>
|
||||
)}
|
||||
<CommandGroup heading="Record Selection">
|
||||
{matchingStandardActionRecordSelectionCommands?.map(
|
||||
(standardActionrecordSelectionCommand) => (
|
||||
<SelectableItem
|
||||
itemId={standardActionrecordSelectionCommand.id}
|
||||
key={standardActionrecordSelectionCommand.id}
|
||||
>
|
||||
<CommandMenuItem
|
||||
id={standardActionrecordSelectionCommand.id}
|
||||
label={standardActionrecordSelectionCommand.label}
|
||||
Icon={standardActionrecordSelectionCommand.Icon}
|
||||
onClick={
|
||||
standardActionrecordSelectionCommand.onCommandClick
|
||||
}
|
||||
firstHotKey={
|
||||
standardActionrecordSelectionCommand.firstHotKey
|
||||
}
|
||||
secondHotKey={
|
||||
standardActionrecordSelectionCommand.secondHotKey
|
||||
}
|
||||
/>
|
||||
</SelectableItem>
|
||||
),
|
||||
)}
|
||||
{matchingWorkflowRunRecordSelectionCommands?.map(
|
||||
(workflowRunRecordSelectionCommand) => (
|
||||
<SelectableItem
|
||||
itemId={workflowRunRecordSelectionCommand.id}
|
||||
key={workflowRunRecordSelectionCommand.id}
|
||||
>
|
||||
<CommandMenuItem
|
||||
id={workflowRunRecordSelectionCommand.id}
|
||||
label={workflowRunRecordSelectionCommand.label}
|
||||
Icon={workflowRunRecordSelectionCommand.Icon}
|
||||
onClick={
|
||||
workflowRunRecordSelectionCommand.onCommandClick
|
||||
}
|
||||
firstHotKey={
|
||||
workflowRunRecordSelectionCommand.firstHotKey
|
||||
}
|
||||
secondHotKey={
|
||||
workflowRunRecordSelectionCommand.secondHotKey
|
||||
}
|
||||
/>
|
||||
</SelectableItem>
|
||||
),
|
||||
)}
|
||||
</CommandGroup>
|
||||
{matchingStandardActionGlobalCommands?.length > 0 && (
|
||||
<CommandGroup heading="View">
|
||||
{matchingStandardActionGlobalCommands?.map(
|
||||
(standardActionGlobalCommand) => (
|
||||
<SelectableItem
|
||||
itemId={standardActionGlobalCommand.id}
|
||||
key={standardActionGlobalCommand.id}
|
||||
>
|
||||
onItemClick({
|
||||
shouldCloseCommandMenuOnClick,
|
||||
onClick: onCommandClick,
|
||||
to,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{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
|
||||
id={standardActionGlobalCommand.id}
|
||||
label={standardActionGlobalCommand.label}
|
||||
Icon={standardActionGlobalCommand.Icon}
|
||||
onClick={
|
||||
standardActionGlobalCommand.onCommandClick
|
||||
}
|
||||
firstHotKey={
|
||||
standardActionGlobalCommand.firstHotKey
|
||||
}
|
||||
secondHotKey={
|
||||
standardActionGlobalCommand.secondHotKey
|
||||
}
|
||||
/>
|
||||
</SelectableItem>
|
||||
),
|
||||
)}
|
||||
</CommandGroup>
|
||||
)}
|
||||
{matchingWorkflowRunGlobalCommands?.length > 0 && (
|
||||
<CommandGroup heading="Workflows">
|
||||
{matchingWorkflowRunGlobalCommands?.map(
|
||||
(workflowRunGlobalCommand) => (
|
||||
<SelectableItem
|
||||
itemId={workflowRunGlobalCommand.id}
|
||||
key={workflowRunGlobalCommand.id}
|
||||
>
|
||||
<CommandMenuItem
|
||||
id={workflowRunGlobalCommand.id}
|
||||
label={workflowRunGlobalCommand.label}
|
||||
Icon={workflowRunGlobalCommand.Icon}
|
||||
onClick={workflowRunGlobalCommand.onCommandClick}
|
||||
firstHotKey={workflowRunGlobalCommand.firstHotKey}
|
||||
secondHotKey={
|
||||
workflowRunGlobalCommand.secondHotKey
|
||||
}
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
Icon={item.Icon}
|
||||
label={item.label}
|
||||
to={item.to}
|
||||
onClick={item.onClick}
|
||||
firstHotKey={item.firstHotKey}
|
||||
secondHotKey={item.secondHotKey}
|
||||
shouldCloseCommandMenuOnClick={
|
||||
workflowRunGlobalCommand.shouldCloseCommandMenuOnClick
|
||||
item.shouldCloseCommandMenuOnClick
|
||||
}
|
||||
/>
|
||||
</SelectableItem>
|
||||
),
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
)}
|
||||
|
||||
{commandGroups.map(({ heading, items, renderItem }) =>
|
||||
items?.length ? (
|
||||
<CommandGroup heading={heading} key={heading}>
|
||||
{items.map((item) => {
|
||||
const {
|
||||
id,
|
||||
Icon,
|
||||
label,
|
||||
to,
|
||||
onClick,
|
||||
key,
|
||||
firstHotKey,
|
||||
secondHotKey,
|
||||
shouldCloseCommandMenuOnClick,
|
||||
} = renderItem(item);
|
||||
return (
|
||||
<SelectableItem itemId={id} key={id}>
|
||||
<CommandMenuItem
|
||||
key={key}
|
||||
id={id}
|
||||
Icon={Icon}
|
||||
label={label}
|
||||
to={to}
|
||||
onClick={onClick}
|
||||
firstHotKey={firstHotKey}
|
||||
secondHotKey={secondHotKey}
|
||||
shouldCloseCommandMenuOnClick={
|
||||
shouldCloseCommandMenuOnClick
|
||||
}
|
||||
/>
|
||||
</SelectableItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
) : null,
|
||||
)}
|
||||
</SelectableList>
|
||||
</StyledInnerList>
|
||||
</ScrollWrapper>
|
||||
</StyledList>
|
||||
</StyledCommandMenu>
|
||||
)}
|
||||
) : null,
|
||||
)}
|
||||
</SelectableList>
|
||||
</StyledInnerList>
|
||||
</ScrollWrapper>
|
||||
</StyledList>
|
||||
</StyledCommandMenu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -0,0 +1,54 @@
|
||||
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
|
||||
import { RecordAgnosticActionsSetterEffect } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionsSetterEffect';
|
||||
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { CommandMenu } from '@/command-menu/components/CommandMenu';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
export const CommandMenuContainer = () => {
|
||||
const { toggleCommandMenu } = useCommandMenu();
|
||||
const { closeKeyboardShortcutMenu } = useKeyboardShortcutMenu();
|
||||
|
||||
const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED');
|
||||
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
|
||||
|
||||
useScopedHotkeys(
|
||||
'ctrl+k,meta+k',
|
||||
() => {
|
||||
closeKeyboardShortcutMenu();
|
||||
toggleCommandMenu();
|
||||
},
|
||||
AppHotkeyScope.CommandMenu,
|
||||
[toggleCommandMenu],
|
||||
);
|
||||
|
||||
return (
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'command-menu' }}
|
||||
>
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'command-menu' }}
|
||||
>
|
||||
<ActionMenuContext.Provider
|
||||
value={{
|
||||
isInRightDrawer: false,
|
||||
onActionExecutedCallback: toggleCommandMenu,
|
||||
}}
|
||||
>
|
||||
<RecordActionMenuEntriesSetter />
|
||||
{isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />}
|
||||
<ActionMenuConfirmationModals />
|
||||
{isCommandMenuOpened && <CommandMenu />}
|
||||
</ActionMenuContext.Provider>
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,27 @@
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const CommandMenuDefaultSelectionEffect = ({
|
||||
selectableItemIds,
|
||||
}: {
|
||||
selectableItemIds: string[];
|
||||
}) => {
|
||||
const { setSelectedItemId, selectedItemIdState } =
|
||||
useSelectableList('command-menu-list');
|
||||
|
||||
const selectedItemId = useRecoilValue(selectedItemIdState);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDefined(selectedItemId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectableItemIds.length > 0) {
|
||||
setSelectedItemId(selectableItemIds[0]);
|
||||
}
|
||||
}, [selectableItemIds, selectedItemId, setSelectedItemId]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@ -4,9 +4,7 @@ import { MemoryRouter } from 'react-router-dom';
|
||||
import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { commandMenuCommandsComponentSelector } from '@/command-menu/states/commandMenuCommandsSelector';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
@ -24,15 +22,10 @@ const renderHooks = () => {
|
||||
() => {
|
||||
const commandMenu = useCommandMenu();
|
||||
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
|
||||
const commandMenuCommands = useRecoilComponentValueV2(
|
||||
commandMenuCommandsComponentSelector,
|
||||
'command-menu',
|
||||
);
|
||||
|
||||
return {
|
||||
commandMenu,
|
||||
isCommandMenuOpened,
|
||||
commandMenuCommands,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
||||
@ -9,6 +9,7 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH
|
||||
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
|
||||
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
@ -126,6 +127,21 @@ export const useCommandMenu = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const actionMenuEntries = snapshot
|
||||
.getLoadable(
|
||||
actionMenuEntriesComponentState.atomFamily({
|
||||
instanceId: mainContextStoreComponentInstanceId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
set(
|
||||
actionMenuEntriesComponentState.atomFamily({
|
||||
instanceId: 'command-menu',
|
||||
}),
|
||||
actionMenuEntries,
|
||||
);
|
||||
|
||||
setIsCommandMenuOpened(true);
|
||||
setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.CommandMenuOpen);
|
||||
},
|
||||
@ -188,6 +204,13 @@ export const useCommandMenu = () => {
|
||||
null,
|
||||
);
|
||||
|
||||
set(
|
||||
actionMenuEntriesComponentState.atomFamily({
|
||||
instanceId: 'command-menu',
|
||||
}),
|
||||
new Map(),
|
||||
);
|
||||
|
||||
if (isCommandMenuOpened) {
|
||||
setIsCommandMenuOpened(false);
|
||||
resetSelectedItem();
|
||||
|
||||
@ -0,0 +1,314 @@
|
||||
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
|
||||
import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} 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 { useDebounce } from 'use-debounce';
|
||||
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
|
||||
|
||||
const isCopilotEnabled = useIsFeatureEnabled('IS_COPILOT_ENABLED');
|
||||
const setCopilotQuery = useSetRecoilState(copilotQueryState);
|
||||
const openCopilotRightDrawer = useOpenCopilotRightDrawer();
|
||||
|
||||
const copilotCommand: Command = {
|
||||
id: 'copilot',
|
||||
to: '', // TODO
|
||||
Icon: IconSparkles,
|
||||
label: 'Open Copilot',
|
||||
type: CommandType.Navigate,
|
||||
onCommandClick: () => {
|
||||
setCopilotQuery(deferredCommandMenuSearch);
|
||||
openCopilotRightDrawer();
|
||||
},
|
||||
};
|
||||
|
||||
const copilotCommands: Command[] = isCopilotEnabled ? [copilotCommand] : [];
|
||||
|
||||
const navigateCommands = Object.values(COMMAND_MENU_NAVIGATE_COMMANDS);
|
||||
|
||||
const actionRecordSelectionCommands: Command[] = actionMenuEntries
|
||||
?.filter(
|
||||
(actionMenuEntry) =>
|
||||
actionMenuEntry.type === ActionMenuEntryType.Standard &&
|
||||
actionMenuEntry.scope === ActionMenuEntryScope.RecordSelection,
|
||||
)
|
||||
?.map((actionMenuEntry) => ({
|
||||
id: actionMenuEntry.key,
|
||||
label: actionMenuEntry.label,
|
||||
Icon: actionMenuEntry.Icon,
|
||||
onCommandClick: actionMenuEntry.onClick,
|
||||
type: CommandType.StandardAction,
|
||||
scope: CommandScope.RecordSelection,
|
||||
}));
|
||||
|
||||
const actionGlobalCommands: Command[] = actionMenuEntries
|
||||
?.filter(
|
||||
(actionMenuEntry) =>
|
||||
actionMenuEntry.type === ActionMenuEntryType.Standard &&
|
||||
actionMenuEntry.scope === ActionMenuEntryScope.Global,
|
||||
)
|
||||
?.map((actionMenuEntry) => ({
|
||||
id: actionMenuEntry.key,
|
||||
label: actionMenuEntry.label,
|
||||
Icon: actionMenuEntry.Icon,
|
||||
onCommandClick: actionMenuEntry.onClick,
|
||||
type: CommandType.StandardAction,
|
||||
scope: CommandScope.Global,
|
||||
}));
|
||||
|
||||
const workflowRunRecordSelectionCommands: Command[] = actionMenuEntries
|
||||
?.filter(
|
||||
(actionMenuEntry) =>
|
||||
actionMenuEntry.type === ActionMenuEntryType.WorkflowRun &&
|
||||
actionMenuEntry.scope === ActionMenuEntryScope.RecordSelection,
|
||||
)
|
||||
?.map((actionMenuEntry) => ({
|
||||
id: actionMenuEntry.key,
|
||||
label: actionMenuEntry.label,
|
||||
Icon: actionMenuEntry.Icon,
|
||||
onCommandClick: actionMenuEntry.onClick,
|
||||
type: CommandType.WorkflowRun,
|
||||
scope: CommandScope.RecordSelection,
|
||||
}));
|
||||
|
||||
const workflowRunGlobalCommands: Command[] = actionMenuEntries
|
||||
?.filter(
|
||||
(actionMenuEntry) =>
|
||||
actionMenuEntry.type === ActionMenuEntryType.WorkflowRun &&
|
||||
actionMenuEntry.scope === ActionMenuEntryScope.Global,
|
||||
)
|
||||
?.map((actionMenuEntry) => ({
|
||||
id: actionMenuEntry.key,
|
||||
label: actionMenuEntry.label,
|
||||
Icon: actionMenuEntry.Icon,
|
||||
onCommandClick: actionMenuEntry.onClick,
|
||||
type: CommandType.WorkflowRun,
|
||||
scope: CommandScope.Global,
|
||||
}));
|
||||
|
||||
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 } }) => ({
|
||||
id,
|
||||
label: `${firstName} ${lastName}`,
|
||||
to: `object/person/${id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
Icon: () => (
|
||||
<Avatar
|
||||
type="rounded"
|
||||
avatarUrl={null}
|
||||
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={null}
|
||||
placeholderColorSeed={objectRecord.record.id}
|
||||
placeholder={objectRecord.recordIdentifier.name ?? ''}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
return customObjectCommandsArray;
|
||||
}, [customObjectRecordsMap]);
|
||||
|
||||
const isLoading = loading || isNotesLoading || isTasksLoading;
|
||||
|
||||
return {
|
||||
copilotCommands,
|
||||
navigateCommands,
|
||||
actionRecordSelectionCommands,
|
||||
actionGlobalCommands,
|
||||
workflowRunRecordSelectionCommands,
|
||||
workflowRunGlobalCommands,
|
||||
peopleCommands,
|
||||
companyCommands,
|
||||
opportunityCommands,
|
||||
noteCommands,
|
||||
tasksCommands,
|
||||
customObjectCommands,
|
||||
isLoading,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,54 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
export const useCommandMenuHotKeys = () => {
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
|
||||
const commandMenuSearch = useRecoilValue(commandMenuSearchState);
|
||||
|
||||
const setContextStoreTargetedRecordsRule = useSetRecoilComponentStateV2(
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
'command-menu',
|
||||
);
|
||||
|
||||
const setContextStoreNumberOfSelectedRecords = useSetRecoilComponentStateV2(
|
||||
contextStoreNumberOfSelectedRecordsComponentState,
|
||||
'command-menu',
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Escape],
|
||||
() => {
|
||||
closeCommandMenu();
|
||||
},
|
||||
AppHotkeyScope.CommandMenuOpen,
|
||||
[closeCommandMenu],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Backspace, Key.Delete],
|
||||
() => {
|
||||
if (!isNonEmptyString(commandMenuSearch)) {
|
||||
setContextStoreTargetedRecordsRule({
|
||||
mode: 'selection',
|
||||
selectedRecordIds: [],
|
||||
});
|
||||
|
||||
setContextStoreNumberOfSelectedRecords(0);
|
||||
}
|
||||
},
|
||||
AppHotkeyScope.CommandMenuOpen,
|
||||
[closeCommandMenu],
|
||||
{
|
||||
preventDefault: false,
|
||||
},
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,37 @@
|
||||
import { Command } from '@/command-menu/types/Command';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
export const useMatchCommands = ({
|
||||
commandMenuSearch,
|
||||
}: {
|
||||
commandMenuSearch: string;
|
||||
}) => {
|
||||
const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300); // 200ms - 500ms
|
||||
|
||||
const checkInShortcuts = (cmd: Command, search: string) => {
|
||||
return (cmd.firstHotKey + (cmd.secondHotKey ?? ''))
|
||||
.toLowerCase()
|
||||
.includes(search.toLowerCase());
|
||||
};
|
||||
|
||||
const checkInLabels = (cmd: Command, search: string) => {
|
||||
if (isNonEmptyString(cmd.label)) {
|
||||
return cmd.label.toLowerCase().includes(search.toLowerCase());
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const matchCommands = (commands: Command[]) => {
|
||||
return commands.filter((cmd) =>
|
||||
deferredCommandMenuSearch.length > 0
|
||||
? checkInShortcuts(cmd, deferredCommandMenuSearch) ||
|
||||
checkInLabels(cmd, deferredCommandMenuSearch)
|
||||
: true,
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
matchCommands,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,73 @@
|
||||
import { useCommandMenuCommands } from '@/command-menu/hooks/useCommandMenuCommands';
|
||||
import { useMatchCommands } from '@/command-menu/hooks/useMatchCommands';
|
||||
|
||||
export const useMatchingCommandMenuCommands = ({
|
||||
commandMenuSearch,
|
||||
}: {
|
||||
commandMenuSearch: string;
|
||||
}) => {
|
||||
const { matchCommands } = useMatchCommands({ commandMenuSearch });
|
||||
|
||||
const {
|
||||
copilotCommands,
|
||||
navigateCommands,
|
||||
actionRecordSelectionCommands,
|
||||
actionGlobalCommands,
|
||||
workflowRunRecordSelectionCommands,
|
||||
workflowRunGlobalCommands,
|
||||
peopleCommands,
|
||||
companyCommands,
|
||||
opportunityCommands,
|
||||
noteCommands,
|
||||
tasksCommands,
|
||||
customObjectCommands,
|
||||
isLoading,
|
||||
} = useCommandMenuCommands();
|
||||
|
||||
const matchingNavigateCommand = matchCommands(navigateCommands);
|
||||
|
||||
const matchingStandardActionRecordSelectionCommands = matchCommands(
|
||||
actionRecordSelectionCommands,
|
||||
);
|
||||
|
||||
const matchingStandardActionGlobalCommands =
|
||||
matchCommands(actionGlobalCommands);
|
||||
|
||||
const matchingWorkflowRunRecordSelectionCommands = matchCommands(
|
||||
workflowRunRecordSelectionCommands,
|
||||
);
|
||||
|
||||
const matchingWorkflowRunGlobalCommands = matchCommands(
|
||||
workflowRunGlobalCommands,
|
||||
);
|
||||
|
||||
const isNoResults =
|
||||
!matchingStandardActionRecordSelectionCommands.length &&
|
||||
!matchingWorkflowRunRecordSelectionCommands.length &&
|
||||
!matchingStandardActionGlobalCommands.length &&
|
||||
!matchingWorkflowRunGlobalCommands.length &&
|
||||
!matchingNavigateCommand.length &&
|
||||
!peopleCommands?.length &&
|
||||
!companyCommands?.length &&
|
||||
!opportunityCommands?.length &&
|
||||
!noteCommands?.length &&
|
||||
!tasksCommands?.length &&
|
||||
!customObjectCommands?.length;
|
||||
|
||||
return {
|
||||
isNoResults,
|
||||
isLoading,
|
||||
copilotCommands,
|
||||
matchingStandardActionRecordSelectionCommands,
|
||||
matchingWorkflowRunRecordSelectionCommands,
|
||||
matchingStandardActionGlobalCommands,
|
||||
matchingWorkflowRunGlobalCommands,
|
||||
matchingNavigateCommand,
|
||||
peopleCommands,
|
||||
companyCommands,
|
||||
opportunityCommands,
|
||||
noteCommands,
|
||||
tasksCommands,
|
||||
customObjectCommands,
|
||||
};
|
||||
};
|
||||
@ -1,23 +0,0 @@
|
||||
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { Command } from '@/command-menu/types/Command';
|
||||
import { computeCommandMenuCommands } from '@/command-menu/utils/computeCommandMenuCommands';
|
||||
import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2';
|
||||
|
||||
export const commandMenuCommandsComponentSelector = createComponentSelectorV2<
|
||||
Command[]
|
||||
>({
|
||||
key: 'commandMenuCommandsComponentSelector',
|
||||
componentInstanceContext: ActionMenuComponentInstanceContext,
|
||||
get:
|
||||
({ instanceId }) =>
|
||||
({ get }) => {
|
||||
const actionMenuEntries = get(
|
||||
actionMenuEntriesComponentSelector.selectorFamily({
|
||||
instanceId,
|
||||
}),
|
||||
);
|
||||
|
||||
return computeCommandMenuCommands(actionMenuEntries);
|
||||
},
|
||||
});
|
||||
@ -1,53 +0,0 @@
|
||||
import {
|
||||
ActionMenuEntry,
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { COMMAND_MENU_NAVIGATE_COMMANDS } from '@/command-menu/constants/CommandMenuNavigateCommands';
|
||||
import {
|
||||
Command,
|
||||
CommandScope,
|
||||
CommandType,
|
||||
} from '@/command-menu/types/Command';
|
||||
|
||||
export const computeCommandMenuCommands = (
|
||||
actionMenuEntries: ActionMenuEntry[],
|
||||
): Command[] => {
|
||||
const navigateCommands = Object.values(COMMAND_MENU_NAVIGATE_COMMANDS);
|
||||
|
||||
const actionCommands: Command[] = actionMenuEntries
|
||||
?.filter(
|
||||
(actionMenuEntry) =>
|
||||
actionMenuEntry.type === ActionMenuEntryType.Standard,
|
||||
)
|
||||
?.map((actionMenuEntry) => ({
|
||||
id: actionMenuEntry.key,
|
||||
label: actionMenuEntry.label,
|
||||
Icon: actionMenuEntry.Icon,
|
||||
onCommandClick: actionMenuEntry.onClick,
|
||||
type: CommandType.StandardAction,
|
||||
scope:
|
||||
actionMenuEntry.scope === ActionMenuEntryScope.RecordSelection
|
||||
? CommandScope.RecordSelection
|
||||
: CommandScope.Global,
|
||||
}));
|
||||
|
||||
const workflowRunCommands: Command[] = actionMenuEntries
|
||||
?.filter(
|
||||
(actionMenuEntry) =>
|
||||
actionMenuEntry.type === ActionMenuEntryType.WorkflowRun,
|
||||
)
|
||||
?.map((actionMenuEntry) => ({
|
||||
id: actionMenuEntry.key,
|
||||
label: actionMenuEntry.label,
|
||||
Icon: actionMenuEntry.Icon,
|
||||
onCommandClick: actionMenuEntry.onClick,
|
||||
type: CommandType.WorkflowRun,
|
||||
scope:
|
||||
actionMenuEntry.scope === ActionMenuEntryScope.RecordSelection
|
||||
? CommandScope.RecordSelection
|
||||
: CommandScope.Global,
|
||||
}));
|
||||
|
||||
return [...navigateCommands, ...actionCommands, ...workflowRunCommands];
|
||||
};
|
||||
@ -1,12 +1,5 @@
|
||||
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
|
||||
import { RecordAgnosticActionsSetterEffect } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionsSetterEffect';
|
||||
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { AuthModal } from '@/auth/components/AuthModal';
|
||||
import { CommandMenu } from '@/command-menu/components/CommandMenu';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { CommandMenuContainer } from '@/command-menu/components/CommandMenuContainer';
|
||||
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
|
||||
import { KeyboardShortcutMenu } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenu';
|
||||
import { AppNavigationDrawer } from '@/navigation/components/AppNavigationDrawer';
|
||||
@ -18,8 +11,7 @@ import { SignInBackgroundMockPage } from '@/sign-in-background-mock/components/S
|
||||
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
|
||||
import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { css, Global, useTheme } from '@emotion/react';
|
||||
import { Global, css, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { AnimatePresence, LayoutGroup, motion } from 'framer-motion';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
@ -77,9 +69,6 @@ export const DefaultLayout = () => {
|
||||
const theme = useTheme();
|
||||
const windowsWidth = useScreenSize().width;
|
||||
const showAuthModal = useShowAuthModal();
|
||||
const { toggleCommandMenu } = useCommandMenu();
|
||||
|
||||
const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED');
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -93,25 +82,7 @@ export const DefaultLayout = () => {
|
||||
<StyledLayout>
|
||||
{!showAuthModal && (
|
||||
<>
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'command-menu' }}
|
||||
>
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'command-menu' }}
|
||||
>
|
||||
<ActionMenuContext.Provider
|
||||
value={{
|
||||
isInRightDrawer: false,
|
||||
onActionExecutedCallback: toggleCommandMenu,
|
||||
}}
|
||||
>
|
||||
<RecordActionMenuEntriesSetter />
|
||||
{isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />}
|
||||
<ActionMenuConfirmationModals />
|
||||
<CommandMenu />
|
||||
</ActionMenuContext.Provider>
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
<CommandMenuContainer />
|
||||
<KeyboardShortcutMenu />
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -26,7 +26,7 @@ export const SelectableList = ({
|
||||
}: SelectableListProps) => {
|
||||
useSelectableListHotKeys(selectableListId, hotkeyScope);
|
||||
|
||||
const { setSelectableItemIds, setSelectableListOnEnter } =
|
||||
const { setSelectableItemIds, setSelectableListOnEnter, setSelectedItemId } =
|
||||
useSelectableList(selectableListId);
|
||||
|
||||
useEffect(() => {
|
||||
@ -47,7 +47,12 @@ export const SelectableList = ({
|
||||
if (isDefined(selectableItemIdArray)) {
|
||||
setSelectableItemIds(arrayToChunks(selectableItemIdArray, 1));
|
||||
}
|
||||
}, [selectableItemIdArray, selectableItemIdMatrix, setSelectableItemIds]);
|
||||
}, [
|
||||
selectableItemIdArray,
|
||||
selectableItemIdMatrix,
|
||||
setSelectableItemIds,
|
||||
setSelectedItemId,
|
||||
]);
|
||||
|
||||
return (
|
||||
<SelectableListScope selectableListScopeId={selectableListId}>
|
||||
|
||||
@ -33,11 +33,23 @@ export const useSelectableList = (selectableListId?: string) => {
|
||||
[selectedItemIdState, isSelectedItemIdSelector],
|
||||
);
|
||||
|
||||
const setSelectedItemId = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(itemId: string) => {
|
||||
resetSelectedItem();
|
||||
set(selectedItemIdState, itemId);
|
||||
set(isSelectedItemIdSelector(itemId), true);
|
||||
},
|
||||
[resetSelectedItem, selectedItemIdState, isSelectedItemIdSelector],
|
||||
);
|
||||
|
||||
return {
|
||||
selectableListId: scopeId,
|
||||
setSelectableItemIds,
|
||||
isSelectedItemIdSelector,
|
||||
setSelectableListOnEnter,
|
||||
resetSelectedItem,
|
||||
setSelectedItemId,
|
||||
selectedItemIdState,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user