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 { MultipleRecordsActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/multiple-records/components/MultipleRecordsActionMenuEntrySetterEffect';
|
||||||
import { NoSelectionActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/no-selection/components/NoSelectionActionMenuEntrySetterEffect';
|
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 { 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 { 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 { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
||||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||||
@ -67,14 +67,16 @@ const ActionEffects = ({
|
|||||||
contextStoreTargetedRecordsRule.selectedRecordIds.length === 1 && (
|
contextStoreTargetedRecordsRule.selectedRecordIds.length === 1 && (
|
||||||
<>
|
<>
|
||||||
{contextStoreCurrentViewType === ContextStoreViewType.ShowPage && (
|
{contextStoreCurrentViewType === ContextStoreViewType.ShowPage && (
|
||||||
<ShowPageSingleRecordActionMenuEntrySetterEffect
|
<SingleRecordActionMenuEntrySetterEffect
|
||||||
objectMetadataItem={objectMetadataItem}
|
objectMetadataItem={objectMetadataItem}
|
||||||
|
viewType={ActionViewType.SHOW_PAGE}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(contextStoreCurrentViewType === ContextStoreViewType.Table ||
|
{(contextStoreCurrentViewType === ContextStoreViewType.Table ||
|
||||||
contextStoreCurrentViewType === ContextStoreViewType.Kanban) && (
|
contextStoreCurrentViewType === ContextStoreViewType.Kanban) && (
|
||||||
<SingleRecordActionMenuEntrySetterEffect
|
<SingleRecordActionMenuEntrySetterEffect
|
||||||
objectMetadataItem={objectMetadataItem}
|
objectMetadataItem={objectMetadataItem}
|
||||||
|
viewType={ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isWorkflowEnabled && (
|
{isWorkflowEnabled && (
|
||||||
|
|||||||
@ -1,78 +1,23 @@
|
|||||||
import { getActionConfig } from '@/action-menu/actions/record-actions/single-record/utils/getActionConfig';
|
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
|
||||||
import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn';
|
|
||||||
import { wrapActionInCallbacks } from '@/action-menu/actions/utils/wrapActionInCallbacks';
|
|
||||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
|
||||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
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 { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useEffect } from 'react';
|
||||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
|
||||||
import { useContext, useEffect } from 'react';
|
|
||||||
import { isDefined } from 'twenty-ui';
|
|
||||||
|
|
||||||
export const SingleRecordActionMenuEntrySetterEffect = ({
|
export const SingleRecordActionMenuEntrySetterEffect = ({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
|
viewType,
|
||||||
}: {
|
}: {
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
viewType: ActionViewType;
|
||||||
}) => {
|
}) => {
|
||||||
const isPageHeaderV2Enabled = useIsFeatureEnabled(
|
|
||||||
'IS_PAGE_HEADER_V2_ENABLED',
|
|
||||||
);
|
|
||||||
|
|
||||||
const actionConfig = getActionConfig(
|
|
||||||
objectMetadataItem,
|
|
||||||
isPageHeaderV2Enabled,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
||||||
|
|
||||||
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
|
const { actionMenuEntries } = useActionMenuEntriesWithCallbacks(
|
||||||
contextStoreTargetedRecordsRuleComponentState,
|
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(() => {
|
useEffect(() => {
|
||||||
for (const action of actionMenuEntries) {
|
for (const action of actionMenuEntries) {
|
||||||
addActionMenuEntry(action);
|
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 { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction';
|
||||||
import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction';
|
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 { 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 { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||||
import {
|
import {
|
||||||
ActionMenuEntry,
|
ActionMenuEntry,
|
||||||
@ -25,8 +25,8 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
|
|||||||
position: 0,
|
position: 0,
|
||||||
Icon: IconHeart,
|
Icon: IconHeart,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
],
|
],
|
||||||
actionHook: useAddToFavoritesSingleRecordAction,
|
actionHook: useAddToFavoritesSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -38,8 +38,8 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
|
|||||||
position: 1,
|
position: 1,
|
||||||
Icon: IconHeartOff,
|
Icon: IconHeartOff,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
],
|
],
|
||||||
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -53,8 +53,8 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
|
|||||||
accent: 'danger',
|
accent: 'danger',
|
||||||
isPinned: true,
|
isPinned: true,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
],
|
],
|
||||||
actionHook: useDeleteSingleRecordAction,
|
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 { useNavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToPreviousRecordSingleRecordAction';
|
||||||
import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction';
|
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 { 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 { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||||
import {
|
import {
|
||||||
ActionMenuEntry,
|
ActionMenuEntry,
|
||||||
@ -38,7 +38,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
|||||||
position: 0,
|
position: 0,
|
||||||
isPinned: false,
|
isPinned: false,
|
||||||
Icon: IconFileExport,
|
Icon: IconFileExport,
|
||||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
availableOn: [ActionViewType.SHOW_PAGE],
|
||||||
actionHook: useExportNoteAction,
|
actionHook: useExportNoteAction,
|
||||||
},
|
},
|
||||||
addToFavoritesSingleRecord: {
|
addToFavoritesSingleRecord: {
|
||||||
@ -51,8 +51,8 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
|||||||
isPinned: true,
|
isPinned: true,
|
||||||
Icon: IconHeart,
|
Icon: IconHeart,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
],
|
],
|
||||||
actionHook: useAddToFavoritesSingleRecordAction,
|
actionHook: useAddToFavoritesSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -66,8 +66,8 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
|||||||
position: 2,
|
position: 2,
|
||||||
Icon: IconHeartOff,
|
Icon: IconHeartOff,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
],
|
],
|
||||||
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -82,8 +82,8 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
|||||||
accent: 'danger',
|
accent: 'danger',
|
||||||
isPinned: true,
|
isPinned: true,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
],
|
],
|
||||||
actionHook: useDeleteSingleRecordAction,
|
actionHook: useDeleteSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -98,8 +98,8 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
|||||||
accent: 'danger',
|
accent: 'danger',
|
||||||
isPinned: true,
|
isPinned: true,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
],
|
],
|
||||||
actionHook: useDestroySingleRecordAction,
|
actionHook: useDestroySingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -112,7 +112,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
|||||||
position: 5,
|
position: 5,
|
||||||
isPinned: true,
|
isPinned: true,
|
||||||
Icon: IconChevronUp,
|
Icon: IconChevronUp,
|
||||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
availableOn: [ActionViewType.SHOW_PAGE],
|
||||||
actionHook: useNavigateToPreviousRecordSingleRecordAction,
|
actionHook: useNavigateToPreviousRecordSingleRecordAction,
|
||||||
},
|
},
|
||||||
navigateToNextRecord: {
|
navigateToNextRecord: {
|
||||||
@ -124,7 +124,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
|||||||
position: 6,
|
position: 6,
|
||||||
isPinned: true,
|
isPinned: true,
|
||||||
Icon: IconChevronDown,
|
Icon: IconChevronDown,
|
||||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
availableOn: [ActionViewType.SHOW_PAGE],
|
||||||
actionHook: useNavigateToNextRecordSingleRecordAction,
|
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 { 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 { 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 { 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 { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||||
import {
|
import {
|
||||||
ActionMenuEntry,
|
ActionMenuEntry,
|
||||||
@ -51,8 +51,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
type: ActionMenuEntryType.Standard,
|
type: ActionMenuEntryType.Standard,
|
||||||
scope: ActionMenuEntryScope.RecordSelection,
|
scope: ActionMenuEntryScope.RecordSelection,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
],
|
],
|
||||||
actionHook: useActivateDraftWorkflowSingleRecordAction,
|
actionHook: useActivateDraftWorkflowSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -66,8 +66,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
type: ActionMenuEntryType.Standard,
|
type: ActionMenuEntryType.Standard,
|
||||||
scope: ActionMenuEntryScope.RecordSelection,
|
scope: ActionMenuEntryScope.RecordSelection,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
],
|
],
|
||||||
actionHook: useActivateLastPublishedVersionWorkflowSingleRecordAction,
|
actionHook: useActivateLastPublishedVersionWorkflowSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -81,8 +81,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
type: ActionMenuEntryType.Standard,
|
type: ActionMenuEntryType.Standard,
|
||||||
scope: ActionMenuEntryScope.RecordSelection,
|
scope: ActionMenuEntryScope.RecordSelection,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
],
|
],
|
||||||
actionHook: useDeactivateWorkflowSingleRecordAction,
|
actionHook: useDeactivateWorkflowSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -96,8 +96,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
type: ActionMenuEntryType.Standard,
|
type: ActionMenuEntryType.Standard,
|
||||||
scope: ActionMenuEntryScope.RecordSelection,
|
scope: ActionMenuEntryScope.RecordSelection,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
],
|
],
|
||||||
actionHook: useDiscardDraftWorkflowSingleRecordAction,
|
actionHook: useDiscardDraftWorkflowSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -111,8 +111,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
type: ActionMenuEntryType.Standard,
|
type: ActionMenuEntryType.Standard,
|
||||||
scope: ActionMenuEntryScope.RecordSelection,
|
scope: ActionMenuEntryScope.RecordSelection,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
],
|
],
|
||||||
actionHook: useSeeActiveVersionWorkflowSingleRecordAction,
|
actionHook: useSeeActiveVersionWorkflowSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -126,8 +126,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
type: ActionMenuEntryType.Standard,
|
type: ActionMenuEntryType.Standard,
|
||||||
scope: ActionMenuEntryScope.RecordSelection,
|
scope: ActionMenuEntryScope.RecordSelection,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
],
|
],
|
||||||
actionHook: useSeeRunsWorkflowSingleRecordAction,
|
actionHook: useSeeRunsWorkflowSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -141,8 +141,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
type: ActionMenuEntryType.Standard,
|
type: ActionMenuEntryType.Standard,
|
||||||
scope: ActionMenuEntryScope.RecordSelection,
|
scope: ActionMenuEntryScope.RecordSelection,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
],
|
],
|
||||||
actionHook: useSeeVersionsWorkflowSingleRecordAction,
|
actionHook: useSeeVersionsWorkflowSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -156,8 +156,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
type: ActionMenuEntryType.Standard,
|
type: ActionMenuEntryType.Standard,
|
||||||
scope: ActionMenuEntryScope.RecordSelection,
|
scope: ActionMenuEntryScope.RecordSelection,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
],
|
],
|
||||||
actionHook: useTestWorkflowSingleRecordAction,
|
actionHook: useTestWorkflowSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -169,7 +169,7 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
shortLabel: '',
|
shortLabel: '',
|
||||||
position: 9,
|
position: 9,
|
||||||
Icon: IconChevronUp,
|
Icon: IconChevronUp,
|
||||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
availableOn: [ActionViewType.SHOW_PAGE],
|
||||||
actionHook: useNavigateToPreviousRecordSingleRecordAction,
|
actionHook: useNavigateToPreviousRecordSingleRecordAction,
|
||||||
},
|
},
|
||||||
navigateToNextRecord: {
|
navigateToNextRecord: {
|
||||||
@ -180,7 +180,7 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
shortLabel: '',
|
shortLabel: '',
|
||||||
position: 10,
|
position: 10,
|
||||||
Icon: IconChevronDown,
|
Icon: IconChevronDown,
|
||||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
availableOn: [ActionViewType.SHOW_PAGE],
|
||||||
actionHook: useNavigateToNextRecordSingleRecordAction,
|
actionHook: useNavigateToNextRecordSingleRecordAction,
|
||||||
},
|
},
|
||||||
addToFavoritesSingleRecord: {
|
addToFavoritesSingleRecord: {
|
||||||
@ -193,8 +193,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
isPinned: false,
|
isPinned: false,
|
||||||
Icon: IconHeart,
|
Icon: IconHeart,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
],
|
],
|
||||||
actionHook: useAddToFavoritesSingleRecordAction,
|
actionHook: useAddToFavoritesSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -208,8 +208,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
position: 12,
|
position: 12,
|
||||||
Icon: IconHeartOff,
|
Icon: IconHeartOff,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
],
|
],
|
||||||
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -224,8 +224,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
accent: 'danger',
|
accent: 'danger',
|
||||||
isPinned: false,
|
isPinned: false,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
],
|
],
|
||||||
actionHook: useDeleteSingleRecordAction,
|
actionHook: useDeleteSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -240,8 +240,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
accent: 'danger',
|
accent: 'danger',
|
||||||
isPinned: false,
|
isPinned: false,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
],
|
],
|
||||||
actionHook: useDestroySingleRecordAction,
|
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 { useNavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToPreviousRecordSingleRecordAction';
|
||||||
import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction';
|
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 { 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 { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||||
import {
|
import {
|
||||||
ActionMenuEntry,
|
ActionMenuEntry,
|
||||||
@ -33,8 +33,8 @@ export const WORKFLOW_RUNS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
isPinned: true,
|
isPinned: true,
|
||||||
Icon: IconHeart,
|
Icon: IconHeart,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
],
|
],
|
||||||
actionHook: useAddToFavoritesSingleRecordAction,
|
actionHook: useAddToFavoritesSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -48,8 +48,8 @@ export const WORKFLOW_RUNS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
position: 1,
|
position: 1,
|
||||||
Icon: IconHeartOff,
|
Icon: IconHeartOff,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
],
|
],
|
||||||
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -62,7 +62,7 @@ export const WORKFLOW_RUNS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
position: 2,
|
position: 2,
|
||||||
isPinned: true,
|
isPinned: true,
|
||||||
Icon: IconChevronUp,
|
Icon: IconChevronUp,
|
||||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
availableOn: [ActionViewType.SHOW_PAGE],
|
||||||
actionHook: useNavigateToPreviousRecordSingleRecordAction,
|
actionHook: useNavigateToPreviousRecordSingleRecordAction,
|
||||||
},
|
},
|
||||||
navigateToNextRecord: {
|
navigateToNextRecord: {
|
||||||
@ -74,7 +74,7 @@ export const WORKFLOW_RUNS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
position: 3,
|
position: 3,
|
||||||
isPinned: true,
|
isPinned: true,
|
||||||
Icon: IconChevronDown,
|
Icon: IconChevronDown,
|
||||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
availableOn: [ActionViewType.SHOW_PAGE],
|
||||||
actionHook: useNavigateToNextRecordSingleRecordAction,
|
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 { 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 { 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 { 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 { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||||
import {
|
import {
|
||||||
ActionMenuEntry,
|
ActionMenuEntry,
|
||||||
@ -40,8 +40,8 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
scope: ActionMenuEntryScope.RecordSelection,
|
scope: ActionMenuEntryScope.RecordSelection,
|
||||||
Icon: IconPencil,
|
Icon: IconPencil,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
],
|
],
|
||||||
actionHook: useUseAsDraftWorkflowVersionSingleRecordAction,
|
actionHook: useUseAsDraftWorkflowVersionSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -54,8 +54,8 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
scope: ActionMenuEntryScope.RecordSelection,
|
scope: ActionMenuEntryScope.RecordSelection,
|
||||||
Icon: IconHistoryToggle,
|
Icon: IconHistoryToggle,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
],
|
],
|
||||||
actionHook: useSeeRunsWorkflowVersionSingleRecordAction,
|
actionHook: useSeeRunsWorkflowVersionSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -68,8 +68,8 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
scope: ActionMenuEntryScope.RecordSelection,
|
scope: ActionMenuEntryScope.RecordSelection,
|
||||||
Icon: IconHistory,
|
Icon: IconHistory,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
],
|
],
|
||||||
actionHook: useSeeVersionsWorkflowVersionSingleRecordAction,
|
actionHook: useSeeVersionsWorkflowVersionSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -81,7 +81,7 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
shortLabel: '',
|
shortLabel: '',
|
||||||
position: 4,
|
position: 4,
|
||||||
Icon: IconChevronUp,
|
Icon: IconChevronUp,
|
||||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
availableOn: [ActionViewType.SHOW_PAGE],
|
||||||
actionHook: useNavigateToPreviousRecordSingleRecordAction,
|
actionHook: useNavigateToPreviousRecordSingleRecordAction,
|
||||||
},
|
},
|
||||||
navigateToNextRecord: {
|
navigateToNextRecord: {
|
||||||
@ -92,7 +92,7 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
shortLabel: '',
|
shortLabel: '',
|
||||||
position: 5,
|
position: 5,
|
||||||
Icon: IconChevronDown,
|
Icon: IconChevronDown,
|
||||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
availableOn: [ActionViewType.SHOW_PAGE],
|
||||||
actionHook: useNavigateToNextRecordSingleRecordAction,
|
actionHook: useNavigateToNextRecordSingleRecordAction,
|
||||||
},
|
},
|
||||||
addToFavoritesSingleRecord: {
|
addToFavoritesSingleRecord: {
|
||||||
@ -105,8 +105,8 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
isPinned: false,
|
isPinned: false,
|
||||||
Icon: IconHeart,
|
Icon: IconHeart,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
],
|
],
|
||||||
actionHook: useAddToFavoritesSingleRecordAction,
|
actionHook: useAddToFavoritesSingleRecordAction,
|
||||||
},
|
},
|
||||||
@ -120,8 +120,8 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
|||||||
position: 7,
|
position: 7,
|
||||||
Icon: IconHeartOff,
|
Icon: IconHeartOff,
|
||||||
availableOn: [
|
availableOn: [
|
||||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||||
ActionAvailableOn.SHOW_PAGE,
|
ActionViewType.SHOW_PAGE,
|
||||||
],
|
],
|
||||||
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export enum ActionAvailableOn {
|
export enum ActionViewType {
|
||||||
INDEX_PAGE_BULK_SELECTION = 'INDEX_PAGE_BULK_SELECTION',
|
INDEX_PAGE_BULK_SELECTION = 'INDEX_PAGE_BULK_SELECTION',
|
||||||
INDEX_PAGE_SINGLE_RECORD_SELECTION = 'INDEX_PAGE_SINGLE_RECORD_SELECTION',
|
INDEX_PAGE_SINGLE_RECORD_SELECTION = 'INDEX_PAGE_SINGLE_RECORD_SELECTION',
|
||||||
INDEX_PAGE_NO_SELECTION = 'INDEX_PAGE_NO_SELECTION',
|
INDEX_PAGE_NO_SELECTION = 'INDEX_PAGE_NO_SELECTION',
|
||||||
@ -1,20 +1,18 @@
|
|||||||
import { getActionConfig } from '@/action-menu/actions/record-actions/single-record/utils/getActionConfig';
|
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 { wrapActionInCallbacks } from '@/action-menu/actions/utils/wrapActionInCallbacks';
|
||||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
|
||||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
import { useContext, useEffect } from 'react';
|
import { useContext } from 'react';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
export const ShowPageSingleRecordActionMenuEntrySetterEffect = ({
|
export const useActionMenuEntriesWithCallbacks = (
|
||||||
objectMetadataItem,
|
objectMetadataItem: ObjectMetadataItem,
|
||||||
}: {
|
viewType: ActionViewType,
|
||||||
objectMetadataItem: ObjectMetadataItem;
|
) => {
|
||||||
}) => {
|
|
||||||
const isPageHeaderV2Enabled = useIsFeatureEnabled(
|
const isPageHeaderV2Enabled = useIsFeatureEnabled(
|
||||||
'IS_PAGE_HEADER_V2_ENABLED',
|
'IS_PAGE_HEADER_V2_ENABLED',
|
||||||
);
|
);
|
||||||
@ -24,8 +22,6 @@ export const ShowPageSingleRecordActionMenuEntrySetterEffect = ({
|
|||||||
isPageHeaderV2Enabled,
|
isPageHeaderV2Enabled,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
|
||||||
|
|
||||||
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
|
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
|
||||||
contextStoreTargetedRecordsRuleComponentState,
|
contextStoreTargetedRecordsRuleComponentState,
|
||||||
);
|
);
|
||||||
@ -38,13 +34,12 @@ export const ShowPageSingleRecordActionMenuEntrySetterEffect = ({
|
|||||||
if (!isDefined(selectedRecordId)) {
|
if (!isDefined(selectedRecordId)) {
|
||||||
throw new Error('Selected record ID is required');
|
throw new Error('Selected record ID is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { onActionStartedCallback, onActionExecutedCallback } =
|
const { onActionStartedCallback, onActionExecutedCallback } =
|
||||||
useContext(ActionMenuContext);
|
useContext(ActionMenuContext);
|
||||||
|
|
||||||
const actionMenuEntries = Object.values(actionConfig ?? {})
|
const actionMenuEntries = Object.values(actionConfig ?? {})
|
||||||
.filter((action) =>
|
.filter((action) => action.availableOn?.includes(viewType))
|
||||||
action.availableOn?.includes(ActionAvailableOn.SHOW_PAGE),
|
|
||||||
)
|
|
||||||
.map((action) => {
|
.map((action) => {
|
||||||
const { shouldBeRegistered, onClick, ConfirmationModal } =
|
const { shouldBeRegistered, onClick, ConfirmationModal } =
|
||||||
action.actionHook({
|
action.actionHook({
|
||||||
@ -70,17 +65,5 @@ export const ShowPageSingleRecordActionMenuEntrySetterEffect = ({
|
|||||||
})
|
})
|
||||||
.filter(isDefined);
|
.filter(isDefined);
|
||||||
|
|
||||||
useEffect(() => {
|
return { actionMenuEntries };
|
||||||
for (const action of actionMenuEntries) {
|
|
||||||
addActionMenuEntry(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
for (const action of actionMenuEntries) {
|
|
||||||
removeActionMenuEntry(action.key);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [actionMenuEntries, addActionMenuEntry, removeActionMenuEntry]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
@ -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 { ConfirmationModalProps } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||||
import { MouseEvent, ReactElement } from 'react';
|
import { MouseEvent, ReactElement } from 'react';
|
||||||
import { IconComponent, MenuItemAccent } from 'twenty-ui';
|
import { IconComponent, MenuItemAccent } from 'twenty-ui';
|
||||||
@ -23,7 +23,7 @@ export type ActionMenuEntry = {
|
|||||||
Icon: IconComponent;
|
Icon: IconComponent;
|
||||||
isPinned?: boolean;
|
isPinned?: boolean;
|
||||||
accent?: MenuItemAccent;
|
accent?: MenuItemAccent;
|
||||||
availableOn?: ActionAvailableOn[];
|
availableOn?: ActionViewType[];
|
||||||
onClick?: (event?: MouseEvent<HTMLElement>) => void;
|
onClick?: (event?: MouseEvent<HTMLElement>) => void;
|
||||||
ConfirmationModal?: ReactElement<ConfirmationModalProps>;
|
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 { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||||
|
import { CommandMenuDefaultSelectionEffect } from '@/command-menu/components/CommandMenuDefaultSelectionEffect';
|
||||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||||
import { CommandMenuTopBar } from '@/command-menu/components/CommandMenuTopBar';
|
import { CommandMenuTopBar } from '@/command-menu/components/CommandMenuTopBar';
|
||||||
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
|
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
|
||||||
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
|
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
|
||||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||||
import { 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 { 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 { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
|
||||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
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 { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
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 styled from '@emotion/styled';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { useRef } from 'react';
|
||||||
import isEmpty from 'lodash.isempty';
|
import { useRecoilState } from 'recoil';
|
||||||
import { useMemo, useRef } from 'react';
|
import { isDefined } from 'twenty-ui';
|
||||||
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';
|
|
||||||
|
|
||||||
const MOBILE_NAVIGATION_BAR_HEIGHT = 64;
|
const MOBILE_NAVIGATION_BAR_HEIGHT = 64;
|
||||||
|
|
||||||
type CommandGroupConfig = {
|
type CommandGroupConfig = {
|
||||||
heading: string;
|
heading: string;
|
||||||
items?: any[];
|
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`
|
const StyledCommandMenu = styled.div`
|
||||||
@ -122,304 +74,16 @@ const StyledEmpty = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const CommandMenu = () => {
|
export const CommandMenu = () => {
|
||||||
const { toggleCommandMenu, onItemClick, closeCommandMenu } = useCommandMenu();
|
const { onItemClick, closeCommandMenu } = useCommandMenu();
|
||||||
const commandMenuRef = useRef<HTMLDivElement>(null);
|
const commandMenuRef = useRef<HTMLDivElement>(null);
|
||||||
const openActivityRightDrawer = useOpenActivityRightDrawer({
|
|
||||||
objectNameSingular: CoreObjectNameSingular.Note,
|
|
||||||
});
|
|
||||||
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
|
|
||||||
const [commandMenuSearch, setCommandMenuSearch] = useRecoilState(
|
const [commandMenuSearch, setCommandMenuSearch] = useRecoilState(
|
||||||
commandMenuSearchState,
|
commandMenuSearchState,
|
||||||
);
|
);
|
||||||
const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300); // 200ms - 500ms
|
|
||||||
const { closeKeyboardShortcutMenu } = useKeyboardShortcutMenu();
|
|
||||||
|
|
||||||
const setContextStoreTargetedRecordsRule = useSetRecoilComponentStateV2(
|
|
||||||
contextStoreTargetedRecordsRuleComponentState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const setContextStoreNumberOfSelectedRecords = useSetRecoilComponentStateV2(
|
|
||||||
contextStoreNumberOfSelectedRecordsComponentState,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const commandMenuCommands = useRecoilComponentValueV2(
|
useCommandMenuHotKeys();
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
useListenClickOutside({
|
useListenClickOutside({
|
||||||
refs: [commandMenuRef],
|
refs: [commandMenuRef],
|
||||||
@ -428,399 +92,165 @@ export const CommandMenu = () => {
|
|||||||
hotkeyScope: AppHotkeyScope.CommandMenuOpen,
|
hotkeyScope: AppHotkeyScope.CommandMenuOpen,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isCopilotEnabled = useIsFeatureEnabled('IS_COPILOT_ENABLED');
|
const {
|
||||||
const setCopilotQuery = useSetRecoilState(copilotQueryState);
|
isNoResults,
|
||||||
const openCopilotRightDrawer = useOpenCopilotRightDrawer();
|
isLoading,
|
||||||
|
copilotCommands,
|
||||||
|
matchingStandardActionRecordSelectionCommands,
|
||||||
|
matchingWorkflowRunRecordSelectionCommands,
|
||||||
|
matchingStandardActionGlobalCommands,
|
||||||
|
matchingWorkflowRunGlobalCommands,
|
||||||
|
matchingNavigateCommand,
|
||||||
|
peopleCommands,
|
||||||
|
companyCommands,
|
||||||
|
opportunityCommands,
|
||||||
|
noteCommands,
|
||||||
|
tasksCommands,
|
||||||
|
customObjectCommands,
|
||||||
|
} = useMatchingCommandMenuCommands({
|
||||||
|
commandMenuSearch,
|
||||||
|
});
|
||||||
|
|
||||||
const copilotCommand: Command = {
|
const selectableItems = copilotCommands
|
||||||
id: 'copilot',
|
.concat(matchingStandardActionRecordSelectionCommands)
|
||||||
to: '', // TODO
|
.concat(matchingWorkflowRunRecordSelectionCommands)
|
||||||
Icon: IconSparkles,
|
.concat(matchingStandardActionGlobalCommands)
|
||||||
label: 'Open Copilot',
|
.concat(matchingWorkflowRunGlobalCommands)
|
||||||
type: CommandType.Navigate,
|
.concat(matchingNavigateCommand)
|
||||||
onCommandClick: () => {
|
.concat(peopleCommands)
|
||||||
setCopilotQuery(deferredCommandMenuSearch);
|
.concat(companyCommands)
|
||||||
openCopilotRightDrawer();
|
.concat(opportunityCommands)
|
||||||
},
|
.concat(noteCommands)
|
||||||
};
|
.concat(tasksCommands)
|
||||||
|
.concat(customObjectCommands)
|
||||||
|
.filter(isDefined);
|
||||||
|
|
||||||
const copilotCommands: Command[] = isCopilotEnabled ? [copilotCommand] : [];
|
const selectableItemIds = selectableItems.map((item) => item.id);
|
||||||
|
|
||||||
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 commandGroups: CommandGroupConfig[] = [
|
const commandGroups: CommandGroupConfig[] = [
|
||||||
{
|
{
|
||||||
heading: 'Navigate',
|
heading: 'Copilot',
|
||||||
items: matchingNavigateCommand,
|
items: copilotCommands,
|
||||||
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: 'Other',
|
heading: 'Record Selection',
|
||||||
items: matchingCreateCommand,
|
items: matchingStandardActionRecordSelectionCommands,
|
||||||
renderItem: (command) => ({
|
},
|
||||||
id: command.id,
|
{
|
||||||
Icon: command.Icon,
|
heading: 'Workflow Record Selection',
|
||||||
label: command.label,
|
items: matchingWorkflowRunRecordSelectionCommands,
|
||||||
to: command.to,
|
},
|
||||||
onClick: command.onCommandClick,
|
{
|
||||||
firstHotKey: command.firstHotKey,
|
heading: 'View',
|
||||||
secondHotKey: command.secondHotKey,
|
items: matchingStandardActionGlobalCommands,
|
||||||
shouldCloseCommandMenuOnClick: command.shouldCloseCommandMenuOnClick,
|
},
|
||||||
}),
|
{
|
||||||
|
heading: 'Workflows',
|
||||||
|
items: matchingWorkflowRunGlobalCommands,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: 'Navigate',
|
||||||
|
items: matchingNavigateCommand,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: 'People',
|
heading: 'People',
|
||||||
items: people,
|
items: peopleCommands,
|
||||||
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,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: 'Companies',
|
heading: 'Companies',
|
||||||
items: companies,
|
items: companyCommands,
|
||||||
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,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: 'Opportunities',
|
heading: 'Opportunities',
|
||||||
items: opportunities,
|
items: opportunityCommands,
|
||||||
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,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: 'Notes',
|
heading: 'Notes',
|
||||||
items: notes,
|
items: noteCommands,
|
||||||
renderItem: (note) => ({
|
|
||||||
id: note.id,
|
|
||||||
Icon: IconNotes,
|
|
||||||
label: note.title ?? '',
|
|
||||||
onClick: () => openActivityRightDrawer(note.id),
|
|
||||||
shouldCloseCommandMenuOnClick: true,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: 'Tasks',
|
heading: 'Tasks',
|
||||||
items: tasks,
|
items: tasksCommands,
|
||||||
renderItem: (task) => ({
|
},
|
||||||
id: task.id,
|
{
|
||||||
Icon: IconCheckbox,
|
heading: 'Custom Objects',
|
||||||
label: task.title ?? '',
|
items: customObjectCommands,
|
||||||
onClick: () => openActivityRightDrawer(task.id),
|
|
||||||
shouldCloseCommandMenuOnClick: true,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
...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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{isCommandMenuOpened && (
|
<CommandMenuDefaultSelectionEffect
|
||||||
<StyledCommandMenu ref={commandMenuRef} className="command-menu">
|
selectableItemIds={selectableItemIds}
|
||||||
<CommandMenuTopBar
|
/>
|
||||||
commandMenuSearch={commandMenuSearch}
|
<StyledCommandMenu ref={commandMenuRef} className="command-menu">
|
||||||
setCommandMenuSearch={setCommandMenuSearch}
|
<CommandMenuTopBar
|
||||||
/>
|
commandMenuSearch={commandMenuSearch}
|
||||||
<StyledList>
|
setCommandMenuSearch={setCommandMenuSearch}
|
||||||
<ScrollWrapper
|
/>
|
||||||
contextProviderName="commandMenu"
|
<StyledList>
|
||||||
componentInstanceId={`scroll-wrapper-command-menu`}
|
<ScrollWrapper
|
||||||
>
|
contextProviderName="commandMenu"
|
||||||
<StyledInnerList isMobile={isMobile}>
|
componentInstanceId={`scroll-wrapper-command-menu`}
|
||||||
<SelectableList
|
>
|
||||||
selectableListId="command-menu-list"
|
<StyledInnerList isMobile={isMobile}>
|
||||||
selectableItemIdArray={selectableItemIds}
|
<SelectableList
|
||||||
hotkeyScope={AppHotkeyScope.CommandMenu}
|
selectableListId="command-menu-list"
|
||||||
onEnter={(itemId) => {
|
selectableItemIdArray={selectableItemIds}
|
||||||
const command = [
|
hotkeyScope={AppHotkeyScope.CommandMenu}
|
||||||
...copilotCommands,
|
onEnter={(itemId) => {
|
||||||
...commandMenuCommands,
|
const command = selectableItems.find(
|
||||||
...otherCommands,
|
(item) => item.id === itemId,
|
||||||
].find((cmd) => cmd.id === itemId);
|
);
|
||||||
|
|
||||||
if (isDefined(command)) {
|
if (isDefined(command)) {
|
||||||
const {
|
const {
|
||||||
to,
|
to,
|
||||||
onCommandClick,
|
onCommandClick,
|
||||||
shouldCloseCommandMenuOnClick,
|
shouldCloseCommandMenuOnClick,
|
||||||
} = command;
|
} = command;
|
||||||
|
|
||||||
onItemClick({
|
onItemClick({
|
||||||
shouldCloseCommandMenuOnClick,
|
shouldCloseCommandMenuOnClick,
|
||||||
onClick: onCommandClick,
|
onClick: onCommandClick,
|
||||||
to,
|
to,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isNoResults && !isLoading && (
|
{isNoResults && !isLoading && (
|
||||||
<StyledEmpty>No results found</StyledEmpty>
|
<StyledEmpty>No results found</StyledEmpty>
|
||||||
)}
|
)}
|
||||||
{isCopilotEnabled && (
|
{commandGroups.map(({ heading, items }) =>
|
||||||
<CommandGroup heading="Copilot">
|
items?.length ? (
|
||||||
<SelectableItem itemId={copilotCommand.id}>
|
<CommandGroup heading={heading} key={heading}>
|
||||||
<CommandMenuItem
|
{items.map((item) => {
|
||||||
id={copilotCommand.id}
|
return (
|
||||||
Icon={copilotCommand.Icon}
|
<SelectableItem itemId={item.id} key={item.id}>
|
||||||
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}
|
|
||||||
>
|
|
||||||
<CommandMenuItem
|
<CommandMenuItem
|
||||||
id={standardActionGlobalCommand.id}
|
key={item.id}
|
||||||
label={standardActionGlobalCommand.label}
|
id={item.id}
|
||||||
Icon={standardActionGlobalCommand.Icon}
|
Icon={item.Icon}
|
||||||
onClick={
|
label={item.label}
|
||||||
standardActionGlobalCommand.onCommandClick
|
to={item.to}
|
||||||
}
|
onClick={item.onClick}
|
||||||
firstHotKey={
|
firstHotKey={item.firstHotKey}
|
||||||
standardActionGlobalCommand.firstHotKey
|
secondHotKey={item.secondHotKey}
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
shouldCloseCommandMenuOnClick={
|
shouldCloseCommandMenuOnClick={
|
||||||
workflowRunGlobalCommand.shouldCloseCommandMenuOnClick
|
item.shouldCloseCommandMenuOnClick
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SelectableItem>
|
</SelectableItem>
|
||||||
),
|
);
|
||||||
)}
|
})}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
)}
|
) : null,
|
||||||
|
)}
|
||||||
{commandGroups.map(({ heading, items, renderItem }) =>
|
</SelectableList>
|
||||||
items?.length ? (
|
</StyledInnerList>
|
||||||
<CommandGroup heading={heading} key={heading}>
|
</ScrollWrapper>
|
||||||
{items.map((item) => {
|
</StyledList>
|
||||||
const {
|
</StyledCommandMenu>
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 { RecoilRoot, useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||||
import { commandMenuCommandsComponentSelector } from '@/command-menu/states/commandMenuCommandsSelector';
|
|
||||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
|
||||||
|
|
||||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
<RecoilRoot>
|
<RecoilRoot>
|
||||||
@ -24,15 +22,10 @@ const renderHooks = () => {
|
|||||||
() => {
|
() => {
|
||||||
const commandMenu = useCommandMenu();
|
const commandMenu = useCommandMenu();
|
||||||
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
|
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
|
||||||
const commandMenuCommands = useRecoilComponentValueV2(
|
|
||||||
commandMenuCommandsComponentSelector,
|
|
||||||
'command-menu',
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
commandMenu,
|
commandMenu,
|
||||||
isCommandMenuOpened,
|
isCommandMenuOpened,
|
||||||
commandMenuCommands,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH
|
|||||||
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
|
||||||
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
||||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
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);
|
setIsCommandMenuOpened(true);
|
||||||
setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.CommandMenuOpen);
|
setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.CommandMenuOpen);
|
||||||
},
|
},
|
||||||
@ -188,6 +204,13 @@ export const useCommandMenu = () => {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
set(
|
||||||
|
actionMenuEntriesComponentState.atomFamily({
|
||||||
|
instanceId: 'command-menu',
|
||||||
|
}),
|
||||||
|
new Map(),
|
||||||
|
);
|
||||||
|
|
||||||
if (isCommandMenuOpened) {
|
if (isCommandMenuOpened) {
|
||||||
setIsCommandMenuOpened(false);
|
setIsCommandMenuOpened(false);
|
||||||
resetSelectedItem();
|
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 { AuthModal } from '@/auth/components/AuthModal';
|
||||||
import { CommandMenu } from '@/command-menu/components/CommandMenu';
|
import { CommandMenuContainer } from '@/command-menu/components/CommandMenuContainer';
|
||||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
|
||||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
|
||||||
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
|
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
|
||||||
import { KeyboardShortcutMenu } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenu';
|
import { KeyboardShortcutMenu } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenu';
|
||||||
import { AppNavigationDrawer } from '@/navigation/components/AppNavigationDrawer';
|
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 { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
|
||||||
import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths';
|
import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths';
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
import { Global, css, useTheme } from '@emotion/react';
|
||||||
import { css, Global, useTheme } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { AnimatePresence, LayoutGroup, motion } from 'framer-motion';
|
import { AnimatePresence, LayoutGroup, motion } from 'framer-motion';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
@ -77,9 +69,6 @@ export const DefaultLayout = () => {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const windowsWidth = useScreenSize().width;
|
const windowsWidth = useScreenSize().width;
|
||||||
const showAuthModal = useShowAuthModal();
|
const showAuthModal = useShowAuthModal();
|
||||||
const { toggleCommandMenu } = useCommandMenu();
|
|
||||||
|
|
||||||
const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -93,25 +82,7 @@ export const DefaultLayout = () => {
|
|||||||
<StyledLayout>
|
<StyledLayout>
|
||||||
{!showAuthModal && (
|
{!showAuthModal && (
|
||||||
<>
|
<>
|
||||||
<ContextStoreComponentInstanceContext.Provider
|
<CommandMenuContainer />
|
||||||
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>
|
|
||||||
<KeyboardShortcutMenu />
|
<KeyboardShortcutMenu />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export const SelectableList = ({
|
|||||||
}: SelectableListProps) => {
|
}: SelectableListProps) => {
|
||||||
useSelectableListHotKeys(selectableListId, hotkeyScope);
|
useSelectableListHotKeys(selectableListId, hotkeyScope);
|
||||||
|
|
||||||
const { setSelectableItemIds, setSelectableListOnEnter } =
|
const { setSelectableItemIds, setSelectableListOnEnter, setSelectedItemId } =
|
||||||
useSelectableList(selectableListId);
|
useSelectableList(selectableListId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -47,7 +47,12 @@ export const SelectableList = ({
|
|||||||
if (isDefined(selectableItemIdArray)) {
|
if (isDefined(selectableItemIdArray)) {
|
||||||
setSelectableItemIds(arrayToChunks(selectableItemIdArray, 1));
|
setSelectableItemIds(arrayToChunks(selectableItemIdArray, 1));
|
||||||
}
|
}
|
||||||
}, [selectableItemIdArray, selectableItemIdMatrix, setSelectableItemIds]);
|
}, [
|
||||||
|
selectableItemIdArray,
|
||||||
|
selectableItemIdMatrix,
|
||||||
|
setSelectableItemIds,
|
||||||
|
setSelectedItemId,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectableListScope selectableListScopeId={selectableListId}>
|
<SelectableListScope selectableListScopeId={selectableListId}>
|
||||||
|
|||||||
@ -33,11 +33,23 @@ export const useSelectableList = (selectableListId?: string) => {
|
|||||||
[selectedItemIdState, isSelectedItemIdSelector],
|
[selectedItemIdState, isSelectedItemIdSelector],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setSelectedItemId = useRecoilCallback(
|
||||||
|
({ set }) =>
|
||||||
|
(itemId: string) => {
|
||||||
|
resetSelectedItem();
|
||||||
|
set(selectedItemIdState, itemId);
|
||||||
|
set(isSelectedItemIdSelector(itemId), true);
|
||||||
|
},
|
||||||
|
[resetSelectedItem, selectedItemIdState, isSelectedItemIdSelector],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectableListId: scopeId,
|
selectableListId: scopeId,
|
||||||
setSelectableItemIds,
|
setSelectableItemIds,
|
||||||
isSelectedItemIdSelector,
|
isSelectedItemIdSelector,
|
||||||
setSelectableListOnEnter,
|
setSelectableListOnEnter,
|
||||||
resetSelectedItem,
|
resetSelectedItem,
|
||||||
|
setSelectedItemId,
|
||||||
|
selectedItemIdState,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user