diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx index 7081c310c5fb3..8efc00e27e1d5 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx @@ -1,8 +1,8 @@ import { MultipleRecordsActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/multiple-records/components/MultipleRecordsActionMenuEntrySetterEffect'; import { NoSelectionActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/no-selection/components/NoSelectionActionMenuEntrySetterEffect'; -import { ShowPageSingleRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/single-record/components/ShowPageSingleRecordActionMenuEntrySetterEffect'; import { SingleRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetterEffect'; import { WorkflowRunRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionMenuEntrySetter'; +import { ActionViewType } from '@/action-menu/actions/types/ActionViewType'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; @@ -67,14 +67,16 @@ const ActionEffects = ({ contextStoreTargetedRecordsRule.selectedRecordIds.length === 1 && ( <> {contextStoreCurrentViewType === ContextStoreViewType.ShowPage && ( - )} {(contextStoreCurrentViewType === ContextStoreViewType.Table || contextStoreCurrentViewType === ContextStoreViewType.Kanban) && ( )} {isWorkflowEnabled && ( diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetterEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetterEffect.tsx index 296459dc5751f..3280eb62cb065 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetterEffect.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetterEffect.tsx @@ -1,78 +1,23 @@ -import { getActionConfig } from '@/action-menu/actions/record-actions/single-record/utils/getActionConfig'; -import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn'; -import { wrapActionInCallbacks } from '@/action-menu/actions/utils/wrapActionInCallbacks'; -import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; +import { ActionViewType } from '@/action-menu/actions/types/ActionViewType'; import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; -import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { useActionMenuEntriesWithCallbacks } from '@/action-menu/hooks/useActionMenuEntriesWithCallbacks'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { useContext, useEffect } from 'react'; -import { isDefined } from 'twenty-ui'; +import { useEffect } from 'react'; export const SingleRecordActionMenuEntrySetterEffect = ({ objectMetadataItem, + viewType, }: { objectMetadataItem: ObjectMetadataItem; + viewType: ActionViewType; }) => { - const isPageHeaderV2Enabled = useIsFeatureEnabled( - 'IS_PAGE_HEADER_V2_ENABLED', - ); - - const actionConfig = getActionConfig( - objectMetadataItem, - isPageHeaderV2Enabled, - ); - const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); - const contextStoreTargetedRecordsRule = useRecoilComponentValueV2( - contextStoreTargetedRecordsRuleComponentState, + const { actionMenuEntries } = useActionMenuEntriesWithCallbacks( + objectMetadataItem, + viewType, ); - const selectedRecordId = - contextStoreTargetedRecordsRule.mode === 'selection' - ? contextStoreTargetedRecordsRule.selectedRecordIds[0] - : undefined; - - if (!isDefined(selectedRecordId)) { - throw new Error('Selected record ID is required'); - } - - const { onActionStartedCallback, onActionExecutedCallback } = - useContext(ActionMenuContext); - - const actionMenuEntries = Object.values(actionConfig ?? {}) - .filter((action) => - action.availableOn?.includes( - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, - ), - ) - .map((action) => { - const { shouldBeRegistered, onClick, ConfirmationModal } = - action.actionHook({ - recordId: selectedRecordId, - objectMetadataItem, - }); - - if (!shouldBeRegistered) { - return undefined; - } - - const wrappedAction = wrapActionInCallbacks({ - action: { - ...action, - onClick, - ConfirmationModal, - }, - onActionStartedCallback, - onActionExecutedCallback, - }); - - return wrappedAction; - }) - .filter(isDefined); - useEffect(() => { for (const action of actionMenuEntries) { addActionMenuEntry(action); diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/constants/DefaultSingleRecordActionsConfigV1.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/constants/DefaultSingleRecordActionsConfigV1.ts index 778fdea06a733..ba65842250234 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/constants/DefaultSingleRecordActionsConfigV1.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/constants/DefaultSingleRecordActionsConfigV1.ts @@ -2,7 +2,7 @@ import { useAddToFavoritesSingleRecordAction } from '@/action-menu/actions/recor import { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction'; import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction'; import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey'; -import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn'; +import { ActionViewType } from '@/action-menu/actions/types/ActionViewType'; import { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook'; import { ActionMenuEntry, @@ -25,8 +25,8 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record< position: 0, Icon: IconHeart, availableOn: [ - ActionAvailableOn.SHOW_PAGE, - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ], actionHook: useAddToFavoritesSingleRecordAction, }, @@ -38,8 +38,8 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record< position: 1, Icon: IconHeartOff, availableOn: [ - ActionAvailableOn.SHOW_PAGE, - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ], actionHook: useRemoveFromFavoritesSingleRecordAction, }, @@ -53,8 +53,8 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record< accent: 'danger', isPinned: true, availableOn: [ - ActionAvailableOn.SHOW_PAGE, - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ], actionHook: useDeleteSingleRecordAction, }, diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/constants/DefaultSingleRecordActionsConfigV2.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/constants/DefaultSingleRecordActionsConfigV2.ts index 1e3e89530c831..6e41b853e4950 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/constants/DefaultSingleRecordActionsConfigV2.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/constants/DefaultSingleRecordActionsConfigV2.ts @@ -6,7 +6,7 @@ import { useNavigateToNextRecordSingleRecordAction } from '@/action-menu/actions import { useNavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToPreviousRecordSingleRecordAction'; import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction'; import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey'; -import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn'; +import { ActionViewType } from '@/action-menu/actions/types/ActionViewType'; import { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook'; import { ActionMenuEntry, @@ -38,7 +38,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record< position: 0, isPinned: false, Icon: IconFileExport, - availableOn: [ActionAvailableOn.SHOW_PAGE], + availableOn: [ActionViewType.SHOW_PAGE], actionHook: useExportNoteAction, }, addToFavoritesSingleRecord: { @@ -51,8 +51,8 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record< isPinned: true, Icon: IconHeart, availableOn: [ - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, - ActionAvailableOn.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, ], actionHook: useAddToFavoritesSingleRecordAction, }, @@ -66,8 +66,8 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record< position: 2, Icon: IconHeartOff, availableOn: [ - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, - ActionAvailableOn.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, ], actionHook: useRemoveFromFavoritesSingleRecordAction, }, @@ -82,8 +82,8 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record< accent: 'danger', isPinned: true, availableOn: [ - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, - ActionAvailableOn.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, ], actionHook: useDeleteSingleRecordAction, }, @@ -98,8 +98,8 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record< accent: 'danger', isPinned: true, availableOn: [ - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, - ActionAvailableOn.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, ], actionHook: useDestroySingleRecordAction, }, @@ -112,7 +112,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record< position: 5, isPinned: true, Icon: IconChevronUp, - availableOn: [ActionAvailableOn.SHOW_PAGE], + availableOn: [ActionViewType.SHOW_PAGE], actionHook: useNavigateToPreviousRecordSingleRecordAction, }, navigateToNextRecord: { @@ -124,7 +124,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record< position: 6, isPinned: true, Icon: IconChevronDown, - availableOn: [ActionAvailableOn.SHOW_PAGE], + availableOn: [ActionViewType.SHOW_PAGE], actionHook: useNavigateToNextRecordSingleRecordAction, }, }; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/constants/WorkflowSingleRecordActionsConfig.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/constants/WorkflowSingleRecordActionsConfig.ts index b672ec5e82c88..f6d85ae3b5ee9 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/constants/WorkflowSingleRecordActionsConfig.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-actions/constants/WorkflowSingleRecordActionsConfig.ts @@ -14,7 +14,7 @@ import { useSeeRunsWorkflowSingleRecordAction } from '@/action-menu/actions/reco import { useSeeVersionsWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeVersionsWorkflowSingleRecordAction'; import { useTestWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useTestWorkflowSingleRecordAction'; import { WorkflowSingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/workflow-actions/types/WorkflowSingleRecordActionsKeys'; -import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn'; +import { ActionViewType } from '@/action-menu/actions/types/ActionViewType'; import { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook'; import { ActionMenuEntry, @@ -51,8 +51,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record< type: ActionMenuEntryType.Standard, scope: ActionMenuEntryScope.RecordSelection, availableOn: [ - ActionAvailableOn.SHOW_PAGE, - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ], actionHook: useActivateDraftWorkflowSingleRecordAction, }, @@ -66,8 +66,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record< type: ActionMenuEntryType.Standard, scope: ActionMenuEntryScope.RecordSelection, availableOn: [ - ActionAvailableOn.SHOW_PAGE, - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ], actionHook: useActivateLastPublishedVersionWorkflowSingleRecordAction, }, @@ -81,8 +81,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record< type: ActionMenuEntryType.Standard, scope: ActionMenuEntryScope.RecordSelection, availableOn: [ - ActionAvailableOn.SHOW_PAGE, - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ], actionHook: useDeactivateWorkflowSingleRecordAction, }, @@ -96,8 +96,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record< type: ActionMenuEntryType.Standard, scope: ActionMenuEntryScope.RecordSelection, availableOn: [ - ActionAvailableOn.SHOW_PAGE, - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ], actionHook: useDiscardDraftWorkflowSingleRecordAction, }, @@ -111,8 +111,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record< type: ActionMenuEntryType.Standard, scope: ActionMenuEntryScope.RecordSelection, availableOn: [ - ActionAvailableOn.SHOW_PAGE, - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ], actionHook: useSeeActiveVersionWorkflowSingleRecordAction, }, @@ -126,8 +126,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record< type: ActionMenuEntryType.Standard, scope: ActionMenuEntryScope.RecordSelection, availableOn: [ - ActionAvailableOn.SHOW_PAGE, - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ], actionHook: useSeeRunsWorkflowSingleRecordAction, }, @@ -141,8 +141,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record< type: ActionMenuEntryType.Standard, scope: ActionMenuEntryScope.RecordSelection, availableOn: [ - ActionAvailableOn.SHOW_PAGE, - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ], actionHook: useSeeVersionsWorkflowSingleRecordAction, }, @@ -156,8 +156,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record< type: ActionMenuEntryType.Standard, scope: ActionMenuEntryScope.RecordSelection, availableOn: [ - ActionAvailableOn.SHOW_PAGE, - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ], actionHook: useTestWorkflowSingleRecordAction, }, @@ -169,7 +169,7 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record< shortLabel: '', position: 9, Icon: IconChevronUp, - availableOn: [ActionAvailableOn.SHOW_PAGE], + availableOn: [ActionViewType.SHOW_PAGE], actionHook: useNavigateToPreviousRecordSingleRecordAction, }, navigateToNextRecord: { @@ -180,7 +180,7 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record< shortLabel: '', position: 10, Icon: IconChevronDown, - availableOn: [ActionAvailableOn.SHOW_PAGE], + availableOn: [ActionViewType.SHOW_PAGE], actionHook: useNavigateToNextRecordSingleRecordAction, }, addToFavoritesSingleRecord: { @@ -193,8 +193,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record< isPinned: false, Icon: IconHeart, availableOn: [ - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, - ActionAvailableOn.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, ], actionHook: useAddToFavoritesSingleRecordAction, }, @@ -208,8 +208,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record< position: 12, Icon: IconHeartOff, availableOn: [ - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, - ActionAvailableOn.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, ], actionHook: useRemoveFromFavoritesSingleRecordAction, }, @@ -224,8 +224,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record< accent: 'danger', isPinned: false, availableOn: [ - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, - ActionAvailableOn.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, ], actionHook: useDeleteSingleRecordAction, }, @@ -240,8 +240,8 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record< accent: 'danger', isPinned: false, availableOn: [ - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, - ActionAvailableOn.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, ], actionHook: useDestroySingleRecordAction, }, diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-run-actions/constants/WorkflowRunsSingleRecordActionsConfig.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-run-actions/constants/WorkflowRunsSingleRecordActionsConfig.ts index 65b97bf750b7b..7f6f246332072 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-run-actions/constants/WorkflowRunsSingleRecordActionsConfig.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-run-actions/constants/WorkflowRunsSingleRecordActionsConfig.ts @@ -3,7 +3,7 @@ import { useNavigateToNextRecordSingleRecordAction } from '@/action-menu/actions import { useNavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToPreviousRecordSingleRecordAction'; import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction'; import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey'; -import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn'; +import { ActionViewType } from '@/action-menu/actions/types/ActionViewType'; import { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook'; import { ActionMenuEntry, @@ -33,8 +33,8 @@ export const WORKFLOW_RUNS_SINGLE_RECORD_ACTIONS_CONFIG: Record< isPinned: true, Icon: IconHeart, availableOn: [ - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, - ActionAvailableOn.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, ], actionHook: useAddToFavoritesSingleRecordAction, }, @@ -48,8 +48,8 @@ export const WORKFLOW_RUNS_SINGLE_RECORD_ACTIONS_CONFIG: Record< position: 1, Icon: IconHeartOff, availableOn: [ - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, - ActionAvailableOn.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, ], actionHook: useRemoveFromFavoritesSingleRecordAction, }, @@ -62,7 +62,7 @@ export const WORKFLOW_RUNS_SINGLE_RECORD_ACTIONS_CONFIG: Record< position: 2, isPinned: true, Icon: IconChevronUp, - availableOn: [ActionAvailableOn.SHOW_PAGE], + availableOn: [ActionViewType.SHOW_PAGE], actionHook: useNavigateToPreviousRecordSingleRecordAction, }, navigateToNextRecord: { @@ -74,7 +74,7 @@ export const WORKFLOW_RUNS_SINGLE_RECORD_ACTIONS_CONFIG: Record< position: 3, isPinned: true, Icon: IconChevronDown, - availableOn: [ActionAvailableOn.SHOW_PAGE], + availableOn: [ActionViewType.SHOW_PAGE], actionHook: useNavigateToNextRecordSingleRecordAction, }, }; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/constants/WorkflowVersionsSingleRecordActionsConfig.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/constants/WorkflowVersionsSingleRecordActionsConfig.ts index 10065a3bb4d24..95f531329704b 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/constants/WorkflowVersionsSingleRecordActionsConfig.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/workflow-version-actions/constants/WorkflowVersionsSingleRecordActionsConfig.ts @@ -7,7 +7,7 @@ import { useSeeRunsWorkflowVersionSingleRecordAction } from '@/action-menu/actio import { useSeeVersionsWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeVersionsWorkflowVersionSingleRecordAction'; import { useUseAsDraftWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useUseAsDraftWorkflowVersionSingleRecordAction'; import { WorkflowVersionSingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/types/WorkflowVersionSingleRecordActionsKeys'; -import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn'; +import { ActionViewType } from '@/action-menu/actions/types/ActionViewType'; import { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook'; import { ActionMenuEntry, @@ -40,8 +40,8 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record< scope: ActionMenuEntryScope.RecordSelection, Icon: IconPencil, availableOn: [ - ActionAvailableOn.SHOW_PAGE, - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ], actionHook: useUseAsDraftWorkflowVersionSingleRecordAction, }, @@ -54,8 +54,8 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record< scope: ActionMenuEntryScope.RecordSelection, Icon: IconHistoryToggle, availableOn: [ - ActionAvailableOn.SHOW_PAGE, - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ], actionHook: useSeeRunsWorkflowVersionSingleRecordAction, }, @@ -68,8 +68,8 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record< scope: ActionMenuEntryScope.RecordSelection, Icon: IconHistory, availableOn: [ - ActionAvailableOn.SHOW_PAGE, - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ], actionHook: useSeeVersionsWorkflowVersionSingleRecordAction, }, @@ -81,7 +81,7 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record< shortLabel: '', position: 4, Icon: IconChevronUp, - availableOn: [ActionAvailableOn.SHOW_PAGE], + availableOn: [ActionViewType.SHOW_PAGE], actionHook: useNavigateToPreviousRecordSingleRecordAction, }, navigateToNextRecord: { @@ -92,7 +92,7 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record< shortLabel: '', position: 5, Icon: IconChevronDown, - availableOn: [ActionAvailableOn.SHOW_PAGE], + availableOn: [ActionViewType.SHOW_PAGE], actionHook: useNavigateToNextRecordSingleRecordAction, }, addToFavoritesSingleRecord: { @@ -105,8 +105,8 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record< isPinned: false, Icon: IconHeart, availableOn: [ - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, - ActionAvailableOn.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, ], actionHook: useAddToFavoritesSingleRecordAction, }, @@ -120,8 +120,8 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record< position: 7, Icon: IconHeartOff, availableOn: [ - ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION, - ActionAvailableOn.SHOW_PAGE, + ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, + ActionViewType.SHOW_PAGE, ], actionHook: useRemoveFromFavoritesSingleRecordAction, }, diff --git a/packages/twenty-front/src/modules/action-menu/actions/types/ActionAvailableOn.ts b/packages/twenty-front/src/modules/action-menu/actions/types/ActionViewType.ts similarity index 87% rename from packages/twenty-front/src/modules/action-menu/actions/types/ActionAvailableOn.ts rename to packages/twenty-front/src/modules/action-menu/actions/types/ActionViewType.ts index fafc2a1e3a256..36c42382ef1c7 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/types/ActionAvailableOn.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/types/ActionViewType.ts @@ -1,4 +1,4 @@ -export enum ActionAvailableOn { +export enum ActionViewType { INDEX_PAGE_BULK_SELECTION = 'INDEX_PAGE_BULK_SELECTION', INDEX_PAGE_SINGLE_RECORD_SELECTION = 'INDEX_PAGE_SINGLE_RECORD_SELECTION', INDEX_PAGE_NO_SELECTION = 'INDEX_PAGE_NO_SELECTION', diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/ShowPageSingleRecordActionMenuEntrySetterEffect.tsx b/packages/twenty-front/src/modules/action-menu/hooks/useActionMenuEntriesWithCallbacks.ts similarity index 70% rename from packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/ShowPageSingleRecordActionMenuEntrySetterEffect.tsx rename to packages/twenty-front/src/modules/action-menu/hooks/useActionMenuEntriesWithCallbacks.ts index c97b1d1df112e..719b7dc408c76 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/ShowPageSingleRecordActionMenuEntrySetterEffect.tsx +++ b/packages/twenty-front/src/modules/action-menu/hooks/useActionMenuEntriesWithCallbacks.ts @@ -1,20 +1,18 @@ import { getActionConfig } from '@/action-menu/actions/record-actions/single-record/utils/getActionConfig'; -import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn'; +import { ActionViewType } from '@/action-menu/actions/types/ActionViewType'; import { wrapActionInCallbacks } from '@/action-menu/actions/utils/wrapActionInCallbacks'; import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; -import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { useContext, useEffect } from 'react'; +import { useContext } from 'react'; import { isDefined } from 'twenty-ui'; -export const ShowPageSingleRecordActionMenuEntrySetterEffect = ({ - objectMetadataItem, -}: { - objectMetadataItem: ObjectMetadataItem; -}) => { +export const useActionMenuEntriesWithCallbacks = ( + objectMetadataItem: ObjectMetadataItem, + viewType: ActionViewType, +) => { const isPageHeaderV2Enabled = useIsFeatureEnabled( 'IS_PAGE_HEADER_V2_ENABLED', ); @@ -24,8 +22,6 @@ export const ShowPageSingleRecordActionMenuEntrySetterEffect = ({ isPageHeaderV2Enabled, ); - const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); - const contextStoreTargetedRecordsRule = useRecoilComponentValueV2( contextStoreTargetedRecordsRuleComponentState, ); @@ -38,13 +34,12 @@ export const ShowPageSingleRecordActionMenuEntrySetterEffect = ({ if (!isDefined(selectedRecordId)) { throw new Error('Selected record ID is required'); } + const { onActionStartedCallback, onActionExecutedCallback } = useContext(ActionMenuContext); const actionMenuEntries = Object.values(actionConfig ?? {}) - .filter((action) => - action.availableOn?.includes(ActionAvailableOn.SHOW_PAGE), - ) + .filter((action) => action.availableOn?.includes(viewType)) .map((action) => { const { shouldBeRegistered, onClick, ConfirmationModal } = action.actionHook({ @@ -70,17 +65,5 @@ export const ShowPageSingleRecordActionMenuEntrySetterEffect = ({ }) .filter(isDefined); - useEffect(() => { - for (const action of actionMenuEntries) { - addActionMenuEntry(action); - } - - return () => { - for (const action of actionMenuEntries) { - removeActionMenuEntry(action.key); - } - }; - }, [actionMenuEntries, addActionMenuEntry, removeActionMenuEntry]); - - return null; + return { actionMenuEntries }; }; diff --git a/packages/twenty-front/src/modules/action-menu/types/ActionMenuEntry.ts b/packages/twenty-front/src/modules/action-menu/types/ActionMenuEntry.ts index 2b955d0ca475c..a6df7bbc56d3c 100644 --- a/packages/twenty-front/src/modules/action-menu/types/ActionMenuEntry.ts +++ b/packages/twenty-front/src/modules/action-menu/types/ActionMenuEntry.ts @@ -1,4 +1,4 @@ -import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn'; +import { ActionViewType } from '@/action-menu/actions/types/ActionViewType'; import { ConfirmationModalProps } from '@/ui/layout/modal/components/ConfirmationModal'; import { MouseEvent, ReactElement } from 'react'; import { IconComponent, MenuItemAccent } from 'twenty-ui'; @@ -23,7 +23,7 @@ export type ActionMenuEntry = { Icon: IconComponent; isPinned?: boolean; accent?: MenuItemAccent; - availableOn?: ActionAvailableOn[]; + availableOn?: ActionViewType[]; onClick?: (event?: MouseEvent) => void; ConfirmationModal?: ReactElement; }; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx index 2d06ba743fea9..c0cf3815d5151 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx @@ -1,77 +1,29 @@ -import { useOpenCopilotRightDrawer } from '@/activities/copilot/right-drawer/hooks/useOpenCopilotRightDrawer'; -import { copilotQueryState } from '@/activities/copilot/right-drawer/states/copilotQueryState'; -import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer'; -import { Note } from '@/activities/types/Note'; -import { Task } from '@/activities/types/Task'; import { CommandGroup } from '@/command-menu/components/CommandGroup'; +import { CommandMenuDefaultSelectionEffect } from '@/command-menu/components/CommandMenuDefaultSelectionEffect'; import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem'; import { CommandMenuTopBar } from '@/command-menu/components/CommandMenuTopBar'; import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight'; import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; -import { commandMenuCommandsComponentSelector } from '@/command-menu/states/commandMenuCommandsSelector'; +import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys'; +import { useMatchingCommandMenuCommands } from '@/command-menu/hooks/useMatchingCommandMenuCommands'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; -import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; -import { - Command, - CommandScope, - CommandType, -} from '@/command-menu/types/Command'; -import { Company } from '@/companies/types/Company'; -import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; -import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; -import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu'; -import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainName'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { useMultiObjectSearch } from '@/object-record/relation-picker/hooks/useMultiObjectSearch'; -import { useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap } from '@/object-record/relation-picker/hooks/useMultiObjectSearchQueryResultFormattedAsObjectRecordsMap'; -import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables'; import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import styled from '@emotion/styled'; -import { isNonEmptyString } from '@sniptt/guards'; -import isEmpty from 'lodash.isempty'; -import { useMemo, useRef } from 'react'; -import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; -import { Key } from 'ts-key-enum'; -import { - Avatar, - IconCheckbox, - IconComponent, - IconNotes, - IconSparkles, - isDefined, -} from 'twenty-ui'; -import { useDebounce } from 'use-debounce'; -import { getLogoUrlFromDomainName } from '~/utils'; -import { capitalize } from '~/utils/string/capitalize'; +import { useRef } from 'react'; +import { useRecoilState } from 'recoil'; +import { isDefined } from 'twenty-ui'; const MOBILE_NAVIGATION_BAR_HEIGHT = 64; type CommandGroupConfig = { heading: string; items?: any[]; - renderItem: (item: any) => { - id: string; - Icon?: IconComponent; - label: string; - to?: string; - onClick?: () => void; - key?: string; - firstHotKey?: string; - secondHotKey?: string; - shouldCloseCommandMenuOnClick?: boolean; - }; }; const StyledCommandMenu = styled.div` @@ -122,705 +74,183 @@ const StyledEmpty = styled.div` `; export const CommandMenu = () => { - const { toggleCommandMenu, onItemClick, closeCommandMenu } = useCommandMenu(); + const { onItemClick, closeCommandMenu } = useCommandMenu(); const commandMenuRef = useRef(null); - const openActivityRightDrawer = useOpenActivityRightDrawer({ - objectNameSingular: CoreObjectNameSingular.Note, - }); - const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState); + const [commandMenuSearch, setCommandMenuSearch] = useRecoilState( commandMenuSearchState, ); - const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300); // 200ms - 500ms - const { closeKeyboardShortcutMenu } = useKeyboardShortcutMenu(); - - const setContextStoreTargetedRecordsRule = useSetRecoilComponentStateV2( - contextStoreTargetedRecordsRuleComponentState, - ); - - const setContextStoreNumberOfSelectedRecords = useSetRecoilComponentStateV2( - contextStoreNumberOfSelectedRecordsComponentState, - ); const isMobile = useIsMobile(); - const commandMenuCommands = useRecoilComponentValueV2( - commandMenuCommandsComponentSelector, - ); - - useScopedHotkeys( - 'ctrl+k,meta+k', - () => { - closeKeyboardShortcutMenu(); - toggleCommandMenu(); - }, - AppHotkeyScope.CommandMenu, - [toggleCommandMenu], - ); - - useScopedHotkeys( - [Key.Escape], - () => { - closeCommandMenu(); - }, - AppHotkeyScope.CommandMenuOpen, - [closeCommandMenu], - ); - - useScopedHotkeys( - [Key.Backspace, Key.Delete], - () => { - if (!isNonEmptyString(commandMenuSearch)) { - setContextStoreTargetedRecordsRule({ - mode: 'selection', - selectedRecordIds: [], - }); + useCommandMenuHotKeys(); - 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({ - skip: !isCommandMenuOpened, - objectNameSingular: CoreObjectNameSingular.Note, - filter: deferredCommandMenuSearch - ? makeOrFilterVariables([ - { title: { ilike: `%${deferredCommandMenuSearch}%` } }, - { body: { ilike: `%${deferredCommandMenuSearch}%` } }, - ]) - : undefined, - limit: 3, - }); - - const { loading: isTasksLoading, records: tasks } = useFindManyRecords({ - skip: !isCommandMenuOpened, - objectNameSingular: CoreObjectNameSingular.Task, - filter: deferredCommandMenuSearch - ? makeOrFilterVariables([ - { title: { ilike: `%${deferredCommandMenuSearch}%` } }, - { body: { ilike: `%${deferredCommandMenuSearch}%` } }, - ]) - : undefined, - limit: 3, + useListenClickOutside({ + refs: [commandMenuRef], + callback: closeCommandMenu, + listenerId: 'COMMAND_MENU_LISTENER_ID', + hotkeyScope: AppHotkeyScope.CommandMenuOpen, }); - 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; - }, [ + const { + isNoResults, + isLoading, + copilotCommands, + matchingStandardActionRecordSelectionCommands, + matchingWorkflowRunRecordSelectionCommands, + matchingStandardActionGlobalCommands, + matchingWorkflowRunGlobalCommands, + matchingNavigateCommand, 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({ - refs: [commandMenuRef], - callback: closeCommandMenu, - listenerId: 'COMMAND_MENU_LISTENER_ID', - hotkeyScope: AppHotkeyScope.CommandMenuOpen, + customObjectCommands, + } = useMatchingCommandMenuCommands({ + commandMenuSearch, }); - 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 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 selectableItems = copilotCommands + .concat(matchingStandardActionRecordSelectionCommands) + .concat(matchingWorkflowRunRecordSelectionCommands) + .concat(matchingStandardActionGlobalCommands) + .concat(matchingWorkflowRunGlobalCommands) + .concat(matchingNavigateCommand) + .concat(peopleCommands) + .concat(companyCommands) + .concat(opportunityCommands) + .concat(noteCommands) + .concat(tasksCommands) + .concat(customObjectCommands) + .filter(isDefined); + + const selectableItemIds = selectableItems.map((item) => item.id); const commandGroups: CommandGroupConfig[] = [ { - heading: 'Navigate', - items: matchingNavigateCommand, - renderItem: (command) => ({ - id: command.id, - Icon: command.Icon, - label: command.label, - to: command.to, - onClick: command.onCommandClick, - firstHotKey: command.firstHotKey, - secondHotKey: command.secondHotKey, - shouldCloseCommandMenuOnClick: command.shouldCloseCommandMenuOnClick, - }), + heading: 'Copilot', + items: copilotCommands, + }, + { + heading: 'Record Selection', + items: matchingStandardActionRecordSelectionCommands, + }, + { + heading: 'Workflow Record Selection', + items: matchingWorkflowRunRecordSelectionCommands, + }, + { + heading: 'View', + items: matchingStandardActionGlobalCommands, }, { - heading: 'Other', - items: matchingCreateCommand, - renderItem: (command) => ({ - id: command.id, - Icon: command.Icon, - label: command.label, - to: command.to, - onClick: command.onCommandClick, - firstHotKey: command.firstHotKey, - secondHotKey: command.secondHotKey, - shouldCloseCommandMenuOnClick: command.shouldCloseCommandMenuOnClick, - }), + heading: 'Workflows', + items: matchingWorkflowRunGlobalCommands, + }, + { + heading: 'Navigate', + items: matchingNavigateCommand, }, { heading: 'People', - items: people, - renderItem: (person) => ({ - id: person.id, - label: `${person.name.firstName} ${person.name.lastName}`, - to: `object/person/${person.id}`, - Icon: () => ( - - ), - firstHotKey: person.firstHotKey, - secondHotKey: person.secondHotKey, - shouldCloseCommandMenuOnClick: true, - }), + items: peopleCommands, }, { heading: 'Companies', - items: companies, - renderItem: (company) => ({ - id: company.id, - label: company.name, - to: `object/company/${company.id}`, - Icon: () => ( - - ), - firstHotKey: company.firstHotKey, - secondHotKey: company.secondHotKey, - shouldCloseCommandMenuOnClick: true, - }), + items: companyCommands, }, { heading: 'Opportunities', - items: opportunities, - renderItem: (opportunity) => ({ - id: opportunity.id, - label: opportunity.name ?? '', - to: `object/opportunity/${opportunity.id}`, - Icon: () => ( - - ), - shouldCloseCommandMenuOnClick: true, - }), + items: opportunityCommands, }, { heading: 'Notes', - items: notes, - renderItem: (note) => ({ - id: note.id, - Icon: IconNotes, - label: note.title ?? '', - onClick: () => openActivityRightDrawer(note.id), - shouldCloseCommandMenuOnClick: true, - }), + items: noteCommands, }, { heading: 'Tasks', - items: tasks, - renderItem: (task) => ({ - id: task.id, - Icon: IconCheckbox, - label: task.title ?? '', - onClick: () => openActivityRightDrawer(task.id), - shouldCloseCommandMenuOnClick: true, - }), + items: tasksCommands, + }, + { + heading: 'Custom Objects', + items: customObjectCommands, }, - ...Object.entries(customObjectRecordsMap).map( - ([customObjectNamePlural, objectRecords]): CommandGroupConfig => ({ - heading: capitalize(customObjectNamePlural), - items: objectRecords, - renderItem: (objectRecord) => ({ - key: objectRecord.record.id, - id: objectRecord.record.id, - label: objectRecord.recordIdentifier.name, - to: `object/${objectRecord.objectMetadataItem.nameSingular}/${objectRecord.record.id}`, - Icon: () => ( - - ), - shouldCloseCommandMenuOnClick: true, - }), - }), - ), ]; return ( <> - {isCommandMenuOpened && ( - - - - - - { - const command = [ - ...copilotCommands, - ...commandMenuCommands, - ...otherCommands, - ].find((cmd) => cmd.id === itemId); - - if (isDefined(command)) { - const { - to, - onCommandClick, - shouldCloseCommandMenuOnClick, - } = command; - - onItemClick({ - shouldCloseCommandMenuOnClick, - onClick: onCommandClick, - to, - }); - } - }} - > - {isNoResults && !isLoading && ( - No results found - )} - {isCopilotEnabled && ( - - - 2 - ? `"${deferredCommandMenuSearch}"` - : '' - }`} - onClick={copilotCommand.onCommandClick} - firstHotKey={copilotCommand.firstHotKey} - secondHotKey={copilotCommand.secondHotKey} - /> - - - )} - - {matchingStandardActionRecordSelectionCommands?.map( - (standardActionrecordSelectionCommand) => ( - - - - ), - )} - {matchingWorkflowRunRecordSelectionCommands?.map( - (workflowRunRecordSelectionCommand) => ( - - - - ), - )} - - {matchingStandardActionGlobalCommands?.length > 0 && ( - - {matchingStandardActionGlobalCommands?.map( - (standardActionGlobalCommand) => ( - - - - ), - )} - - )} - {matchingWorkflowRunGlobalCommands?.length > 0 && ( - - {matchingWorkflowRunGlobalCommands?.map( - (workflowRunGlobalCommand) => ( - + + + + + + + { + const command = selectableItems.find( + (item) => item.id === itemId, + ); + + if (isDefined(command)) { + const { + to, + onCommandClick, + shouldCloseCommandMenuOnClick, + } = command; + + onItemClick({ + shouldCloseCommandMenuOnClick, + onClick: onCommandClick, + to, + }); + } + }} + > + {isNoResults && !isLoading && ( + No results found + )} + {commandGroups.map(({ heading, items }) => + items?.length ? ( + + {items.map((item) => { + return ( + - ), - )} + ); + })} - )} - - {commandGroups.map(({ heading, items, renderItem }) => - items?.length ? ( - - {items.map((item) => { - const { - id, - Icon, - label, - to, - onClick, - key, - firstHotKey, - secondHotKey, - shouldCloseCommandMenuOnClick, - } = renderItem(item); - return ( - - - - ); - })} - - ) : null, - )} - - - - - - )} + ) : null, + )} + + + + + ); }; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx new file mode 100644 index 0000000000000..3575d20c9a0ad --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContainer.tsx @@ -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 ( + + + + + {isWorkflowEnabled && } + + {isCommandMenuOpened && } + + + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuDefaultSelectionEffect.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuDefaultSelectionEffect.tsx new file mode 100644 index 0000000000000..e12233a8de4ad --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuDefaultSelectionEffect.tsx @@ -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; +}; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx index f9549c7372e92..2ae691d15c490 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx +++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx @@ -4,9 +4,7 @@ import { MemoryRouter } from 'react-router-dom'; import { RecoilRoot, useRecoilValue } from 'recoil'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; -import { commandMenuCommandsComponentSelector } from '@/command-menu/states/commandMenuCommandsSelector'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; const Wrapper = ({ children }: { children: React.ReactNode }) => ( @@ -24,15 +22,10 @@ const renderHooks = () => { () => { const commandMenu = useCommandMenu(); const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState); - const commandMenuCommands = useRecoilComponentValueV2( - commandMenuCommandsComponentSelector, - 'command-menu', - ); return { commandMenu, isCommandMenuOpened, - commandMenuCommands, }; }, { diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts index f8d178cd3920f..3b32e5a6e1c8a 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts @@ -9,6 +9,7 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { isDefined } from '~/utils/isDefined'; +import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; @@ -126,6 +127,21 @@ export const useCommandMenu = () => { ); } + const actionMenuEntries = snapshot + .getLoadable( + actionMenuEntriesComponentState.atomFamily({ + instanceId: mainContextStoreComponentInstanceId, + }), + ) + .getValue(); + + set( + actionMenuEntriesComponentState.atomFamily({ + instanceId: 'command-menu', + }), + actionMenuEntries, + ); + setIsCommandMenuOpened(true); setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.CommandMenuOpen); }, @@ -188,6 +204,13 @@ export const useCommandMenu = () => { null, ); + set( + actionMenuEntriesComponentState.atomFamily({ + instanceId: 'command-menu', + }), + new Map(), + ); + if (isCommandMenuOpened) { setIsCommandMenuOpened(false); resetSelectedItem(); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCommands.tsx b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCommands.tsx new file mode 100644 index 0000000000000..42f71648cc73b --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuCommands.tsx @@ -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({ + skip: !isCommandMenuOpened, + objectNameSingular: CoreObjectNameSingular.Note, + filter: deferredCommandMenuSearch + ? makeOrFilterVariables([ + { title: { ilike: `%${deferredCommandMenuSearch}%` } }, + { body: { ilike: `%${deferredCommandMenuSearch}%` } }, + ]) + : undefined, + limit: 3, + }); + + const { loading: isTasksLoading, records: tasks } = useFindManyRecords({ + 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: () => ( + + ), + })), + [people], + ); + + const companyCommands = useMemo( + () => + companies?.map((company) => ({ + id: company.id, + label: company.name ?? '', + to: `object/company/${company.id}`, + shouldCloseCommandMenuOnClick: true, + Icon: () => ( + + ), + })), + [companies], + ); + + const opportunityCommands = useMemo( + () => + opportunities?.map(({ id, name }) => ({ + id, + label: name ?? '', + to: `object/opportunity/${id}`, + shouldCloseCommandMenuOnClick: true, + Icon: () => ( + + ), + })), + [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: () => ( + + ), + })), + ); + }); + + return customObjectCommandsArray; + }, [customObjectRecordsMap]); + + const isLoading = loading || isNotesLoading || isTasksLoading; + + return { + copilotCommands, + navigateCommands, + actionRecordSelectionCommands, + actionGlobalCommands, + workflowRunRecordSelectionCommands, + workflowRunGlobalCommands, + peopleCommands, + companyCommands, + opportunityCommands, + noteCommands, + tasksCommands, + customObjectCommands, + isLoading, + }; +}; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts new file mode 100644 index 0000000000000..f905b29ceec67 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenuHotKeys.ts @@ -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, + }, + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useMatchCommands.ts b/packages/twenty-front/src/modules/command-menu/hooks/useMatchCommands.ts new file mode 100644 index 0000000000000..42bf653a3d008 --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/useMatchCommands.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useMatchingCommandMenuCommands.ts b/packages/twenty-front/src/modules/command-menu/hooks/useMatchingCommandMenuCommands.ts new file mode 100644 index 0000000000000..b4607ada7f02d --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/hooks/useMatchingCommandMenuCommands.ts @@ -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, + }; +}; diff --git a/packages/twenty-front/src/modules/command-menu/states/commandMenuCommandsSelector.ts b/packages/twenty-front/src/modules/command-menu/states/commandMenuCommandsSelector.ts deleted file mode 100644 index bff43955cca80..0000000000000 --- a/packages/twenty-front/src/modules/command-menu/states/commandMenuCommandsSelector.ts +++ /dev/null @@ -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); - }, -}); diff --git a/packages/twenty-front/src/modules/command-menu/utils/computeCommandMenuCommands.ts b/packages/twenty-front/src/modules/command-menu/utils/computeCommandMenuCommands.ts deleted file mode 100644 index 706bdd3c33a61..0000000000000 --- a/packages/twenty-front/src/modules/command-menu/utils/computeCommandMenuCommands.ts +++ /dev/null @@ -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]; -}; diff --git a/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx index dc5b37ce1cf51..d395310999761 100644 --- a/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx +++ b/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx @@ -1,12 +1,5 @@ -import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter'; -import { RecordAgnosticActionsSetterEffect } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionsSetterEffect'; -import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals'; -import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; -import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { AuthModal } from '@/auth/components/AuthModal'; -import { CommandMenu } from '@/command-menu/components/CommandMenu'; -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; -import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; +import { CommandMenuContainer } from '@/command-menu/components/CommandMenuContainer'; import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary'; import { KeyboardShortcutMenu } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenu'; import { AppNavigationDrawer } from '@/navigation/components/AppNavigationDrawer'; @@ -18,8 +11,7 @@ import { SignInBackgroundMockPage } from '@/sign-in-background-mock/components/S import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal'; import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { css, Global, useTheme } from '@emotion/react'; +import { Global, css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { AnimatePresence, LayoutGroup, motion } from 'framer-motion'; import { Outlet } from 'react-router-dom'; @@ -77,9 +69,6 @@ export const DefaultLayout = () => { const theme = useTheme(); const windowsWidth = useScreenSize().width; const showAuthModal = useShowAuthModal(); - const { toggleCommandMenu } = useCommandMenu(); - - const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED'); return ( <> @@ -93,25 +82,7 @@ export const DefaultLayout = () => { {!showAuthModal && ( <> - - - - - {isWorkflowEnabled && } - - - - - + )} diff --git a/packages/twenty-front/src/modules/ui/layout/selectable-list/components/SelectableList.tsx b/packages/twenty-front/src/modules/ui/layout/selectable-list/components/SelectableList.tsx index 96badbceef291..12efb1b612f26 100644 --- a/packages/twenty-front/src/modules/ui/layout/selectable-list/components/SelectableList.tsx +++ b/packages/twenty-front/src/modules/ui/layout/selectable-list/components/SelectableList.tsx @@ -26,7 +26,7 @@ export const SelectableList = ({ }: SelectableListProps) => { useSelectableListHotKeys(selectableListId, hotkeyScope); - const { setSelectableItemIds, setSelectableListOnEnter } = + const { setSelectableItemIds, setSelectableListOnEnter, setSelectedItemId } = useSelectableList(selectableListId); useEffect(() => { @@ -47,7 +47,12 @@ export const SelectableList = ({ if (isDefined(selectableItemIdArray)) { setSelectableItemIds(arrayToChunks(selectableItemIdArray, 1)); } - }, [selectableItemIdArray, selectableItemIdMatrix, setSelectableItemIds]); + }, [ + selectableItemIdArray, + selectableItemIdMatrix, + setSelectableItemIds, + setSelectedItemId, + ]); return ( diff --git a/packages/twenty-front/src/modules/ui/layout/selectable-list/hooks/useSelectableList.ts b/packages/twenty-front/src/modules/ui/layout/selectable-list/hooks/useSelectableList.ts index 3dadd58d1c2ed..e48ac7babde33 100644 --- a/packages/twenty-front/src/modules/ui/layout/selectable-list/hooks/useSelectableList.ts +++ b/packages/twenty-front/src/modules/ui/layout/selectable-list/hooks/useSelectableList.ts @@ -33,11 +33,23 @@ export const useSelectableList = (selectableListId?: string) => { [selectedItemIdState, isSelectedItemIdSelector], ); + const setSelectedItemId = useRecoilCallback( + ({ set }) => + (itemId: string) => { + resetSelectedItem(); + set(selectedItemIdState, itemId); + set(isSelectedItemIdSelector(itemId), true); + }, + [resetSelectedItem, selectedItemIdState, isSelectedItemIdSelector], + ); + return { selectableListId: scopeId, setSelectableItemIds, isSelectedItemIdSelector, setSelectableListOnEnter, resetSelectedItem, + setSelectedItemId, + selectedItemIdState, }; };