From a9cb1e9b0d81fa62e749dd1f948ae8dd1d5bca8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:08:27 +0100 Subject: [PATCH] Refactor actions (#8761) Closes #8737 - Refactored actions by creating hooks to add the possibility to register actions programatically. - Small fixes from #8610 review - Fixed shortcuts display inside the command menu - Removed `actionMenuEntriesComponentState` and introduced `actionMenuEntriesComponentSelector` --- .../GlobalActionMenuEntriesSetter.tsx | 8 -- .../RecordActionMenuEntriesSetter.tsx | 46 ++----- ...ipleRecordsActionMenuEntrySetterEffect.tsx | 24 ++++ .../hooks/useDeleteMultipleRecordsAction.tsx} | 50 ++----- .../hooks/useExportMultipleRecordsAction.tsx} | 37 ++--- .../hooks/useMultipleRecordsActions.tsx | 40 ++++++ ...NoSelectionActionMenuEntrySetterEffect.tsx | 26 ++++ .../hooks/useExportMultipleRecordsAction.tsx | 51 +++++++ .../hooks/useNoSelectionRecordActions.tsx | 29 ++++ ...ingleRecordActionMenuEntrySetterEffect.tsx | 24 ++++ .../hooks/useDeleteSingleRecordAction.tsx | 128 ++++++++++++++++++ .../useManageFavoritesSingleRecordAction.tsx} | 30 ++-- .../hooks/useSingleRecordActions.tsx | 50 +++++++ .../useWorkflowRunRecordActions.tsx} | 31 ++--- .../RecordAgnosticActionsSetterEffect.tsx | 17 +++ .../hooks/useGlobalActions.ts | 23 ++++ .../hooks/useWorkflowRunActions.tsx} | 26 ++-- .../components/RecordIndexActionMenu.tsx | 4 +- .../components/RecordShowActionMenu.tsx | 4 +- .../RecordShowRightDrawerActionMenu.tsx | 4 +- .../effect-components/PageChangeEffect.tsx | 42 ------ .../command-menu/components/CommandMenu.tsx | 56 +++++++- .../components/CommandMenuCommandsEffect.tsx | 20 --- .../CommandMenuContextRecordChip.tsx | 74 ++-------- .../CommandMenuContextRecordChipAvatars.tsx | 55 ++++++++ .../components/CommandMenuTopBar.tsx | 14 +- .../__stories__/CommandMenu.stories.tsx | 55 +------- ...ands.ts => CommandMenuNavigateCommands.ts} | 2 +- .../hooks/__tests__/useCommandMenu.test.tsx | 69 +--------- .../command-menu/hooks/useCommandMenu.ts | 39 ------ .../states/commandMenuCommandsSelector.ts | 23 ++++ .../states/commandMenuCommandsState.ts | 15 -- .../utils/computeCommandMenuCommands.ts | 6 +- ...xtStoreComponentInstanceIdSetterEffect.tsx | 9 +- .../ContextStoreInstanceIdDefaultValue.tsx | 1 + ...textStoreCurrentObjectMetadataIdOrThrow.ts | 13 +- ...eFindManyRecordsSelectedInContextStore.ts} | 2 +- .../mainContextStoreComponentInstanceId.ts | 3 +- .../layout/page/components/DefaultLayout.tsx | 6 +- .../twenty-front/src/testing/graphqlMocks.ts | 5 + 40 files changed, 682 insertions(+), 479 deletions(-) delete mode 100644 packages/twenty-front/src/modules/action-menu/actions/global-actions/components/GlobalActionMenuEntriesSetter.tsx create mode 100644 packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/MultipleRecordsActionMenuEntrySetterEffect.tsx rename packages/twenty-front/src/modules/action-menu/actions/record-actions/{components/DeleteRecordsActionEffect.tsx => multiple-records/hooks/useDeleteMultipleRecordsAction.tsx} (79%) rename packages/twenty-front/src/modules/action-menu/actions/record-actions/{components/ExportRecordsActionEffect.tsx => multiple-records/hooks/useExportMultipleRecordsAction.tsx} (54%) create mode 100644 packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useMultipleRecordsActions.tsx create mode 100644 packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/components/NoSelectionActionMenuEntrySetterEffect.tsx create mode 100644 packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/hooks/useExportMultipleRecordsAction.tsx create mode 100644 packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/hooks/useNoSelectionRecordActions.tsx create mode 100644 packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetterEffect.tsx create mode 100644 packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction.tsx rename packages/twenty-front/src/modules/action-menu/actions/record-actions/{components/ManageFavoritesActionEffect.tsx => single-record/hooks/useManageFavoritesSingleRecordAction.tsx} (85%) create mode 100644 packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useSingleRecordActions.tsx rename packages/twenty-front/src/modules/action-menu/actions/record-actions/workflow-run-record-actions/{components/WorkflowRunRecordActionEffect.tsx => hooks/useWorkflowRunRecordActions.tsx} (85%) create mode 100644 packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionsSetterEffect.tsx create mode 100644 packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/hooks/useGlobalActions.ts rename packages/twenty-front/src/modules/action-menu/actions/{global-actions/workflow-run-actions/components/WorkflowRunActionEffect.tsx => record-agnostic-actions/workflow-run-actions/hooks/useWorkflowRunActions.tsx} (80%) delete mode 100644 packages/twenty-front/src/modules/command-menu/components/CommandMenuCommandsEffect.tsx create mode 100644 packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordChipAvatars.tsx rename packages/twenty-front/src/modules/command-menu/constants/{CommandMenuCommands.ts => CommandMenuNavigateCommands.ts} (94%) create mode 100644 packages/twenty-front/src/modules/command-menu/states/commandMenuCommandsSelector.ts delete mode 100644 packages/twenty-front/src/modules/command-menu/states/commandMenuCommandsState.ts create mode 100644 packages/twenty-front/src/modules/context-store/constants/ContextStoreInstanceIdDefaultValue.tsx rename packages/twenty-front/src/modules/context-store/hooks/{useContextStoreSelectedRecords.ts => useFindManyRecordsSelectedInContextStore.ts} (96%) diff --git a/packages/twenty-front/src/modules/action-menu/actions/global-actions/components/GlobalActionMenuEntriesSetter.tsx b/packages/twenty-front/src/modules/action-menu/actions/global-actions/components/GlobalActionMenuEntriesSetter.tsx deleted file mode 100644 index 35dc9b070..000000000 --- a/packages/twenty-front/src/modules/action-menu/actions/global-actions/components/GlobalActionMenuEntriesSetter.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { WorkflowRunActionEffect } from '@/action-menu/actions/global-actions/workflow-run-actions/components/WorkflowRunActionEffect'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; - -export const GlobalActionMenuEntriesSetter = () => { - const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED'); - - return <>{isWorkflowEnabled && }; -}; 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 417fa1dd1..b35bf3972 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,26 +1,12 @@ -import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect'; -import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect'; -import { ManageFavoritesActionEffect } from '@/action-menu/actions/record-actions/components/ManageFavoritesActionEffect'; -import { WorkflowRunRecordActionEffect } from '@/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionEffect'; +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 { SingleRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetterEffect'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { isDefined } from 'twenty-ui'; -const noSelectionRecordActionEffects = [ExportRecordsActionEffect]; - -const singleRecordActionEffects = [ - ManageFavoritesActionEffect, - DeleteRecordsActionEffect, -]; - -const multipleRecordActionEffects = [ - ExportRecordsActionEffect, - DeleteRecordsActionEffect, -]; - export const RecordActionMenuEntriesSetter = () => { const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2( contextStoreCurrentObjectMetadataIdComponentState, @@ -48,26 +34,20 @@ const ActionEffects = ({ contextStoreNumberOfSelectedRecordsComponentState, ); - const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED'); - - const actions = - contextStoreNumberOfSelectedRecords === 0 - ? noSelectionRecordActionEffects - : contextStoreNumberOfSelectedRecords === 1 - ? singleRecordActionEffects - : multipleRecordActionEffects; - return ( <> - {actions.map((ActionEffect, index) => ( - - ))} - {contextStoreNumberOfSelectedRecords === 1 && isWorkflowEnabled && ( - + )} + {contextStoreNumberOfSelectedRecords > 1 && ( + )} diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/MultipleRecordsActionMenuEntrySetterEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/MultipleRecordsActionMenuEntrySetterEffect.tsx new file mode 100644 index 000000000..67b96f1b7 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/components/MultipleRecordsActionMenuEntrySetterEffect.tsx @@ -0,0 +1,24 @@ +import { useMultipleRecordsActions } from '@/action-menu/actions/record-actions/multiple-records/hooks/useMultipleRecordsActions'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useEffect } from 'react'; + +export const MultipleRecordsActionMenuEntrySetterEffect = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; +}) => { + const { registerMultipleRecordsActions, unregisterMultipleRecordsActions } = + useMultipleRecordsActions({ + objectMetadataItem, + }); + + useEffect(() => { + registerMultipleRecordsActions(); + + return () => { + unregisterMultipleRecordsActions(); + }; + }, [registerMultipleRecordsActions, unregisterMultipleRecordsActions]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/DeleteRecordsActionEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction.tsx similarity index 79% rename from packages/twenty-front/src/modules/action-menu/actions/record-actions/components/DeleteRecordsActionEffect.tsx rename to packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction.tsx index 629d74f27..29be66860 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/DeleteRecordsActionEffect.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction.tsx @@ -18,10 +18,10 @@ import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTabl import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { useCallback, useContext, useEffect, useState } from 'react'; +import { useCallback, useContext, useState } from 'react'; import { IconTrash, isDefined } from 'twenty-ui'; -export const DeleteRecordsActionEffect = ({ +export const useDeleteMultipleRecordsAction = ({ position, objectMetadataItem, }: { @@ -106,12 +106,12 @@ export const DeleteRecordsActionEffect = ({ const { isInRightDrawer, onActionExecutedCallback } = useContext(ActionMenuContext); - useEffect(() => { + const registerDeleteMultipleRecordsAction = () => { if (canDelete) { addActionMenuEntry({ type: ActionMenuEntryType.Standard, scope: ActionMenuEntryScope.RecordSelection, - key: 'delete', + key: 'delete-multiple-records', label: 'Delete', position, Icon: IconTrash, @@ -124,16 +124,8 @@ export const DeleteRecordsActionEffect = ({ { handleDeleteClick(); onActionExecutedCallback?.(); @@ -141,31 +133,19 @@ export const DeleteRecordsActionEffect = ({ closeRightDrawer(); } }} - deleteButtonText={`Delete ${ - contextStoreNumberOfSelectedRecords > 1 ? 'Records' : 'Record' - }`} + deleteButtonText={'Delete Records'} /> ), }); - } else { - removeActionMenuEntry('delete'); } + }; - return () => { - removeActionMenuEntry('delete'); - }; - }, [ - addActionMenuEntry, - canDelete, - closeRightDrawer, - contextStoreNumberOfSelectedRecords, - handleDeleteClick, - isDeleteRecordsModalOpen, - isInRightDrawer, - onActionExecutedCallback, - position, - removeActionMenuEntry, - ]); + const unregisterDeleteMultipleRecordsAction = () => { + removeActionMenuEntry('delete-multiple-records'); + }; - return null; + return { + registerDeleteMultipleRecordsAction, + unregisterDeleteMultipleRecordsAction, + }; }; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ExportRecordsActionEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useExportMultipleRecordsAction.tsx similarity index 54% rename from packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ExportRecordsActionEffect.tsx rename to packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useExportMultipleRecordsAction.tsx index b4636a1d3..eacdf1e5e 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ExportRecordsActionEffect.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useExportMultipleRecordsAction.tsx @@ -1,7 +1,5 @@ import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; -import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { IconDatabaseExport } from 'twenty-ui'; import { @@ -12,9 +10,8 @@ import { displayedExportProgress, useExportRecords, } from '@/object-record/record-index/export/hooks/useExportRecords'; -import { useEffect } from 'react'; -export const ExportRecordsActionEffect = ({ +export const useExportMultipleRecordsAction = ({ position, objectMetadataItem, }: { @@ -22,9 +19,6 @@ export const ExportRecordsActionEffect = ({ objectMetadataItem: ObjectMetadataItem; }) => { const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); - const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2( - contextStoreNumberOfSelectedRecordsComponentState, - ); const { progress, download } = useExportRecords({ delayMs: 100, @@ -33,32 +27,25 @@ export const ExportRecordsActionEffect = ({ filename: `${objectMetadataItem.nameSingular}.csv`, }); - useEffect(() => { + const registerExportMultipleRecordsAction = () => { addActionMenuEntry({ type: ActionMenuEntryType.Standard, - scope: - contextStoreNumberOfSelectedRecords > 0 - ? ActionMenuEntryScope.RecordSelection - : ActionMenuEntryScope.Global, - key: 'export', + scope: ActionMenuEntryScope.RecordSelection, + key: 'export-multiple-records', position, label: displayedExportProgress(progress), Icon: IconDatabaseExport, accent: 'default', onClick: () => download(), }); + }; - return () => { - removeActionMenuEntry('export'); - }; - }, [ - contextStoreNumberOfSelectedRecords, - download, - progress, - addActionMenuEntry, - removeActionMenuEntry, - position, - ]); + const unregisterExportMultipleRecordsAction = () => { + removeActionMenuEntry('export-multiple-records'); + }; - return null; + return { + registerExportMultipleRecordsAction, + unregisterExportMultipleRecordsAction, + }; }; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useMultipleRecordsActions.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useMultipleRecordsActions.tsx new file mode 100644 index 000000000..78e459e23 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/multiple-records/hooks/useMultipleRecordsActions.tsx @@ -0,0 +1,40 @@ +import { useDeleteMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction'; +import { useExportViewNoSelectionRecordAction } from '@/action-menu/actions/record-actions/no-selection/hooks/useExportMultipleRecordsAction'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; + +export const useMultipleRecordsActions = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; +}) => { + const { + registerDeleteMultipleRecordsAction, + unregisterDeleteMultipleRecordsAction, + } = useDeleteMultipleRecordsAction({ + position: 0, + objectMetadataItem, + }); + + const { + registerExportViewNoSelectionRecordsAction, + unregisterExportViewNoSelectionRecordsAction, + } = useExportViewNoSelectionRecordAction({ + position: 1, + objectMetadataItem, + }); + + const registerMultipleRecordsActions = () => { + registerDeleteMultipleRecordsAction(); + registerExportViewNoSelectionRecordsAction(); + }; + + const unregisterMultipleRecordsActions = () => { + unregisterDeleteMultipleRecordsAction(); + unregisterExportViewNoSelectionRecordsAction(); + }; + + return { + registerMultipleRecordsActions, + unregisterMultipleRecordsActions, + }; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/components/NoSelectionActionMenuEntrySetterEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/components/NoSelectionActionMenuEntrySetterEffect.tsx new file mode 100644 index 000000000..2cba15829 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/components/NoSelectionActionMenuEntrySetterEffect.tsx @@ -0,0 +1,26 @@ +import { useNoSelectionRecordActions } from '@/action-menu/actions/record-actions/no-selection/hooks/useNoSelectionRecordActions'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useEffect } from 'react'; + +export const NoSelectionActionMenuEntrySetterEffect = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; +}) => { + const { + registerNoSelectionRecordActions, + unregisterNoSelectionRecordActions, + } = useNoSelectionRecordActions({ + objectMetadataItem, + }); + + useEffect(() => { + registerNoSelectionRecordActions(); + + return () => { + unregisterNoSelectionRecordActions(); + }; + }, [registerNoSelectionRecordActions, unregisterNoSelectionRecordActions]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/hooks/useExportMultipleRecordsAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/hooks/useExportMultipleRecordsAction.tsx new file mode 100644 index 000000000..d3a257144 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/hooks/useExportMultipleRecordsAction.tsx @@ -0,0 +1,51 @@ +import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { IconDatabaseExport } from 'twenty-ui'; + +import { + ActionMenuEntryScope, + ActionMenuEntryType, +} from '@/action-menu/types/ActionMenuEntry'; +import { + displayedExportProgress, + useExportRecords, +} from '@/object-record/record-index/export/hooks/useExportRecords'; + +export const useExportViewNoSelectionRecordAction = ({ + position, + objectMetadataItem, +}: { + position: number; + objectMetadataItem: ObjectMetadataItem; +}) => { + const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); + + const { progress, download } = useExportRecords({ + delayMs: 100, + objectMetadataItem, + recordIndexId: objectMetadataItem.namePlural, + filename: `${objectMetadataItem.nameSingular}.csv`, + }); + + const registerExportViewNoSelectionRecordsAction = () => { + addActionMenuEntry({ + type: ActionMenuEntryType.Standard, + scope: ActionMenuEntryScope.Global, + key: 'export-view-no-selection', + position, + label: displayedExportProgress(progress), + Icon: IconDatabaseExport, + accent: 'default', + onClick: () => download(), + }); + }; + + const unregisterExportViewNoSelectionRecordsAction = () => { + removeActionMenuEntry('export-view-no-selection'); + }; + + return { + registerExportViewNoSelectionRecordsAction, + unregisterExportViewNoSelectionRecordsAction, + }; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/hooks/useNoSelectionRecordActions.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/hooks/useNoSelectionRecordActions.tsx new file mode 100644 index 000000000..a647e449b --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/no-selection/hooks/useNoSelectionRecordActions.tsx @@ -0,0 +1,29 @@ +import { useExportViewNoSelectionRecordAction } from '@/action-menu/actions/record-actions/no-selection/hooks/useExportMultipleRecordsAction'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; + +export const useNoSelectionRecordActions = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; +}) => { + const { + registerExportViewNoSelectionRecordsAction, + unregisterExportViewNoSelectionRecordsAction, + } = useExportViewNoSelectionRecordAction({ + position: 0, + objectMetadataItem, + }); + + const registerNoSelectionRecordActions = () => { + registerExportViewNoSelectionRecordsAction(); + }; + + const unregisterNoSelectionRecordActions = () => { + unregisterExportViewNoSelectionRecordsAction(); + }; + + return { + registerNoSelectionRecordActions, + unregisterNoSelectionRecordActions, + }; +}; 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 new file mode 100644 index 000000000..ef190808b --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetterEffect.tsx @@ -0,0 +1,24 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useEffect } from 'react'; +import { useSingleRecordActions } from '../hooks/useSingleRecordActions'; + +export const SingleRecordActionMenuEntrySetterEffect = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; +}) => { + const { registerSingleRecordActions, unregisterSingleRecordActions } = + useSingleRecordActions({ + objectMetadataItem, + }); + + useEffect(() => { + registerSingleRecordActions(); + + return () => { + unregisterSingleRecordActions(); + }; + }, [registerSingleRecordActions, unregisterSingleRecordActions]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction.tsx new file mode 100644 index 000000000..0148468eb --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction.tsx @@ -0,0 +1,128 @@ +import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; +import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; +import { + ActionMenuEntryScope, + ActionMenuEntryType, +} from '@/action-menu/types/ActionMenuEntry'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite'; +import { useFavorites } from '@/favorites/hooks/useFavorites'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; +import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; +import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; +import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useCallback, useContext, useState } from 'react'; +import { IconTrash, isDefined } from 'twenty-ui'; + +export const useDeleteSingleRecordAction = ({ + position, + objectMetadataItem, +}: { + position: number; + objectMetadataItem: ObjectMetadataItem; +}) => { + const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); + + const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] = + useState(false); + + const { resetTableRowSelection } = useRecordTable({ + recordTableId: objectMetadataItem.namePlural, + }); + + const { deleteOneRecord } = useDeleteOneRecord({ + objectNameSingular: objectMetadataItem.nameSingular, + }); + + const { sortedFavorites: favorites } = useFavorites(); + const { deleteFavorite } = useDeleteFavorite(); + + const contextStoreTargetedRecordsRule = useRecoilComponentValueV2( + contextStoreTargetedRecordsRuleComponentState, + ); + + const { closeRightDrawer } = useRightDrawer(); + + const recordIdToDelete = + contextStoreTargetedRecordsRule.mode === 'selection' + ? contextStoreTargetedRecordsRule.selectedRecordIds?.[0] + : undefined; + + const handleDeleteClick = useCallback(async () => { + if (!isDefined(recordIdToDelete)) { + return; + } + + resetTableRowSelection(); + + const foundFavorite = favorites?.find( + (favorite) => favorite.recordId === recordIdToDelete, + ); + + if (isDefined(foundFavorite)) { + deleteFavorite(foundFavorite.id); + } + + await deleteOneRecord(recordIdToDelete); + }, [ + deleteFavorite, + deleteOneRecord, + favorites, + recordIdToDelete, + resetTableRowSelection, + ]); + + const isRemoteObject = objectMetadataItem.isRemote; + + const { isInRightDrawer, onActionExecutedCallback } = + useContext(ActionMenuContext); + + const registerDeleteSingleRecordAction = () => { + if (isRemoteObject || !isDefined(recordIdToDelete)) { + return; + } + + addActionMenuEntry({ + type: ActionMenuEntryType.Standard, + scope: ActionMenuEntryScope.RecordSelection, + key: 'delete-single-record', + label: 'Delete', + position, + Icon: IconTrash, + accent: 'danger', + isPinned: true, + onClick: () => { + setIsDeleteRecordsModalOpen(true); + }, + ConfirmationModal: ( + { + handleDeleteClick(); + onActionExecutedCallback?.(); + if (isInRightDrawer) { + closeRightDrawer(); + } + }} + deleteButtonText={'Delete Record'} + /> + ), + }); + }; + + const unregisterDeleteSingleRecordAction = () => { + removeActionMenuEntry('delete-single-record'); + }; + + return { + registerDeleteSingleRecordAction, + unregisterDeleteSingleRecordAction, + }; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ManageFavoritesActionEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useManageFavoritesSingleRecordAction.tsx similarity index 85% rename from packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ManageFavoritesActionEffect.tsx rename to packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useManageFavoritesSingleRecordAction.tsx index f1423b922..3687318ed 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ManageFavoritesActionEffect.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useManageFavoritesSingleRecordAction.tsx @@ -10,11 +10,10 @@ import { useFavorites } from '@/favorites/hooks/useFavorites'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { useEffect } from 'react'; import { useRecoilValue } from 'recoil'; import { IconHeart, IconHeartOff, isDefined } from 'twenty-ui'; -export const ManageFavoritesActionEffect = ({ +export const useManageFavoritesSingleRecordAction = ({ position, objectMetadataItem, }: { @@ -48,7 +47,7 @@ export const ManageFavoritesActionEffect = ({ const isFavorite = !!selectedRecordId && !!foundFavorite; - useEffect(() => { + const registerManageFavoritesSingleRecordAction = () => { if (!isDefined(objectMetadataItem) || objectMetadataItem.isRemote) { return; } @@ -56,7 +55,7 @@ export const ManageFavoritesActionEffect = ({ addActionMenuEntry({ type: ActionMenuEntryType.Standard, scope: ActionMenuEntryScope.RecordSelection, - key: 'manage-favorites', + key: 'manage-favorites-single-record', label: isFavorite ? 'Remove from favorites' : 'Add to favorites', position, Icon: isFavorite ? IconHeartOff : IconHeart, @@ -68,21 +67,14 @@ export const ManageFavoritesActionEffect = ({ } }, }); + }; - return () => { - removeActionMenuEntry('manage-favorites'); - }; - }, [ - addActionMenuEntry, - createFavorite, - deleteFavorite, - foundFavorite?.id, - isFavorite, - objectMetadataItem, - position, - removeActionMenuEntry, - selectedRecord, - ]); + const unregisterManageFavoritesSingleRecordAction = () => { + removeActionMenuEntry('manage-favorites-single-record'); + }; - return null; + return { + registerManageFavoritesSingleRecordAction, + unregisterManageFavoritesSingleRecordAction, + }; }; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useSingleRecordActions.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useSingleRecordActions.tsx new file mode 100644 index 000000000..58248f1dc --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/hooks/useSingleRecordActions.tsx @@ -0,0 +1,50 @@ +import { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction'; +import { useManageFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useManageFavoritesSingleRecordAction'; +import { useWorkflowRunRecordActions } from '@/action-menu/actions/record-actions/workflow-run-record-actions/hooks/useWorkflowRunRecordActions'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; + +export const useSingleRecordActions = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; +}) => { + const { + registerManageFavoritesSingleRecordAction, + unregisterManageFavoritesSingleRecordAction, + } = useManageFavoritesSingleRecordAction({ + position: 0, + objectMetadataItem, + }); + + const { + registerDeleteSingleRecordAction, + unregisterDeleteSingleRecordAction, + } = useDeleteSingleRecordAction({ + position: 1, + objectMetadataItem, + }); + + const { + registerWorkflowRunRecordActions, + unregisterWorkflowRunRecordActions, + } = useWorkflowRunRecordActions({ + objectMetadataItem, + }); + + const registerSingleRecordActions = () => { + registerManageFavoritesSingleRecordAction(); + registerDeleteSingleRecordAction(); + registerWorkflowRunRecordActions(); + }; + + const unregisterSingleRecordActions = () => { + unregisterManageFavoritesSingleRecordAction(); + unregisterDeleteSingleRecordAction(); + unregisterWorkflowRunRecordActions(); + }; + + return { + registerSingleRecordActions, + unregisterSingleRecordActions, + }; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/workflow-run-record-actions/hooks/useWorkflowRunRecordActions.tsx similarity index 85% rename from packages/twenty-front/src/modules/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionEffect.tsx rename to packages/twenty-front/src/modules/action-menu/actions/record-actions/workflow-run-record-actions/hooks/useWorkflowRunRecordActions.tsx index 9535571eb..677e9b392 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionEffect.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/workflow-run-record-actions/hooks/useWorkflowRunRecordActions.tsx @@ -13,12 +13,11 @@ import { useAllActiveWorkflowVersions } from '@/workflow/hooks/useAllActiveWorkf import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion'; import { useTheme } from '@emotion/react'; -import { useEffect } from 'react'; import { useRecoilValue } from 'recoil'; import { IconSettingsAutomation, isDefined } from 'twenty-ui'; import { capitalize } from '~/utils/string/capitalize'; -export const WorkflowRunRecordActionEffect = ({ +export const useWorkflowRunRecordActions = ({ objectMetadataItem, }: { objectMetadataItem: ObjectMetadataItem; @@ -49,7 +48,7 @@ export const WorkflowRunRecordActionEffect = ({ const theme = useTheme(); - useEffect(() => { + const registerWorkflowRunRecordActions = () => { if (!isDefined(objectMetadataItem) || objectMetadataItem.isRemote) { return; } @@ -88,22 +87,16 @@ export const WorkflowRunRecordActionEffect = ({ }, }); } + }; - return () => { - for (const activeWorkflowVersion of activeWorkflowVersions) { - removeActionMenuEntry(`workflow-run-${activeWorkflowVersion.id}`); - } - }; - }, [ - activeWorkflowVersions, - addActionMenuEntry, - enqueueSnackBar, - objectMetadataItem, - removeActionMenuEntry, - runWorkflowVersion, - selectedRecord, - theme.snackBar.success.color, - ]); + const unregisterWorkflowRunRecordActions = () => { + for (const activeWorkflowVersion of activeWorkflowVersions) { + removeActionMenuEntry(`workflow-run-${activeWorkflowVersion.id}`); + } + }; - return null; + return { + registerWorkflowRunRecordActions, + unregisterWorkflowRunRecordActions, + }; }; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionsSetterEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionsSetterEffect.tsx new file mode 100644 index 000000000..9652fd64c --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionsSetterEffect.tsx @@ -0,0 +1,17 @@ +import { useRecordAgnosticActions } from '@/action-menu/actions/record-agnostic-actions/hooks/useGlobalActions'; +import { useEffect } from 'react'; + +export const RecordAgnosticActionsSetterEffect = () => { + const { registerRecordAgnosticActions, unregisterRecordAgnosticActions } = + useRecordAgnosticActions(); + + useEffect(() => { + registerRecordAgnosticActions(); + + return () => { + unregisterRecordAgnosticActions(); + }; + }, [registerRecordAgnosticActions, unregisterRecordAgnosticActions]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/hooks/useGlobalActions.ts b/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/hooks/useGlobalActions.ts new file mode 100644 index 000000000..50d85dab5 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/hooks/useGlobalActions.ts @@ -0,0 +1,23 @@ +import { useWorkflowRunActions } from '@/action-menu/actions/record-agnostic-actions/workflow-run-actions/hooks/useWorkflowRunActions'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; + +export const useRecordAgnosticActions = () => { + const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED'); + + const { addWorkflowRunActions, removeWorkflowRunActions } = + useWorkflowRunActions(); + + const registerRecordAgnosticActions = () => { + if (isWorkflowEnabled) { + addWorkflowRunActions(); + } + }; + + const unregisterRecordAgnosticActions = () => { + if (isWorkflowEnabled) { + removeWorkflowRunActions(); + } + }; + + return { registerRecordAgnosticActions, unregisterRecordAgnosticActions }; +}; diff --git a/packages/twenty-front/src/modules/action-menu/actions/global-actions/workflow-run-actions/components/WorkflowRunActionEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/workflow-run-actions/hooks/useWorkflowRunActions.tsx similarity index 80% rename from packages/twenty-front/src/modules/action-menu/actions/global-actions/workflow-run-actions/components/WorkflowRunActionEffect.tsx rename to packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/workflow-run-actions/hooks/useWorkflowRunActions.tsx index 424b339af..3152bf709 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/global-actions/workflow-run-actions/components/WorkflowRunActionEffect.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-agnostic-actions/workflow-run-actions/hooks/useWorkflowRunActions.tsx @@ -9,11 +9,10 @@ import { useAllActiveWorkflowVersions } from '@/workflow/hooks/useAllActiveWorkf import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion'; import { useTheme } from '@emotion/react'; -import { useEffect } from 'react'; import { IconSettingsAutomation } from 'twenty-ui'; import { capitalize } from '~/utils/string/capitalize'; -export const WorkflowRunActionEffect = () => { +export const useWorkflowRunActions = () => { const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); const { records: activeWorkflowVersions } = useAllActiveWorkflowVersions({ @@ -26,7 +25,7 @@ export const WorkflowRunActionEffect = () => { const theme = useTheme(); - useEffect(() => { + const addWorkflowRunActions = () => { for (const [ index, activeWorkflowVersion, @@ -56,20 +55,13 @@ export const WorkflowRunActionEffect = () => { }, }); } + }; - return () => { - for (const activeWorkflowVersion of activeWorkflowVersions) { - removeActionMenuEntry(`workflow-run-${activeWorkflowVersion.id}`); - } - }; - }, [ - activeWorkflowVersions, - addActionMenuEntry, - enqueueSnackBar, - removeActionMenuEntry, - runWorkflowVersion, - theme.snackBar.success.color, - ]); + const removeWorkflowRunActions = () => { + for (const activeWorkflowVersion of activeWorkflowVersions) { + removeActionMenuEntry(`workflow-run-${activeWorkflowVersion.id}`); + } + }; - return null; + return { addWorkflowRunActions, removeWorkflowRunActions }; }; diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenu.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenu.tsx index baa2be8b9..bf0780d35 100644 --- a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenu.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenu.tsx @@ -1,5 +1,5 @@ -import { GlobalActionMenuEntriesSetter } from '@/action-menu/actions/global-actions/components/GlobalActionMenuEntriesSetter'; 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 { RecordIndexActionMenuBar } from '@/action-menu/components/RecordIndexActionMenuBar'; import { RecordIndexActionMenuDropdown } from '@/action-menu/components/RecordIndexActionMenuDropdown'; @@ -28,7 +28,7 @@ export const RecordIndexActionMenu = () => { - + )} diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenu.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenu.tsx index a4961ccf7..f42814899 100644 --- a/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenu.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenu.tsx @@ -1,5 +1,5 @@ -import { GlobalActionMenuEntriesSetter } from '@/action-menu/actions/global-actions/components/GlobalActionMenuEntriesSetter'; 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'; @@ -48,7 +48,7 @@ export const RecordShowActionMenu = ({ /> - + )} diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerActionMenu.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerActionMenu.tsx index ba964f27a..e6ccd1da4 100644 --- a/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerActionMenu.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/RecordShowRightDrawerActionMenu.tsx @@ -1,5 +1,5 @@ -import { GlobalActionMenuEntriesSetter } from '@/action-menu/actions/global-actions/components/GlobalActionMenuEntriesSetter'; 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 { RightDrawerActionMenuDropdown } from '@/action-menu/components/RightDrawerActionMenuDropdown'; import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; @@ -24,7 +24,7 @@ export const RecordShowRightDrawerActionMenu = () => { - + )} diff --git a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx index 3af2f15ad..3934d9040 100644 --- a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx +++ b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx @@ -2,25 +2,18 @@ import { useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useRecoilValue } from 'recoil'; -import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { setSessionId, useEventTracker, } from '@/analytics/hooks/useEventTracker'; import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken'; import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState'; -import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; -import { CommandType } from '@/command-menu/types/Command'; -import { useNonSystemActiveObjectMetadataItems } from '@/object-metadata/hooks/useNonSystemActiveObjectMetadataItems'; -import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { AppBasePath } from '@/types/AppBasePath'; import { AppPath } from '@/types/AppPath'; import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { SettingsPath } from '@/types/SettingsPath'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; -import { IconCheckbox } from 'twenty-ui'; import { useCleanRecoilState } from '~/hooks/useCleanRecoilState'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation'; @@ -45,14 +38,6 @@ export const PageChangeEffect = () => { const eventTracker = useEventTracker(); - const { addToCommandMenu, setObjectsInCommandMenu } = useCommandMenu(); - - const objectMetadataItems = useRecoilValue(objectMetadataItemsState); - - const openCreateActivity = useOpenCreateActivityDrawer({ - activityObjectNameSingular: CoreObjectNameSingular.Task, - }); - useEffect(() => { cleanRecoilState(); }, [cleanRecoilState]); @@ -150,33 +135,6 @@ export const PageChangeEffect = () => { } }, [isMatchingLocation, setHotkeyScope]); - const { nonSystemActiveObjectMetadataItems } = - useNonSystemActiveObjectMetadataItems(); - - useEffect(() => { - setObjectsInCommandMenu(nonSystemActiveObjectMetadataItems); - - addToCommandMenu([ - { - id: 'create-task', - to: '', - label: 'Create Task', - type: CommandType.Create, - Icon: IconCheckbox, - onCommandClick: () => - openCreateActivity({ - targetableObjects: [], - }), - }, - ]); - }, [ - nonSystemActiveObjectMetadataItems, - addToCommandMenu, - setObjectsInCommandMenu, - openCreateActivity, - objectMetadataItems, - ]); - useEffect(() => { setTimeout(() => { setSessionId(); 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 625611110..7cc7d1ac4 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx @@ -9,7 +9,7 @@ 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 { commandMenuCommandsState } from '@/command-menu/states/commandMenuCommandsState'; +import { commandMenuCommandsComponentSelector } from '@/command-menu/states/commandMenuCommandsSelector'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; import { @@ -35,6 +35,7 @@ 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'; @@ -67,6 +68,8 @@ type CommandGroupConfig = { to?: string; onClick?: () => void; key?: string; + firstHotKey?: string; + secondHotKey?: string; }; }; @@ -128,7 +131,6 @@ export const CommandMenu = () => { commandMenuSearchState, ); const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300); // 200ms - 500ms - const commandMenuCommands = useRecoilValue(commandMenuCommandsState); const { closeKeyboardShortcutMenu } = useKeyboardShortcutMenu(); const setContextStoreTargetedRecordsRule = useSetRecoilComponentStateV2( @@ -141,6 +143,10 @@ export const CommandMenu = () => { const isMobile = useIsMobile(); + const commandMenuCommands = useRecoilComponentValueV2( + commandMenuCommandsComponentSelector, + ); + useScopedHotkeys( 'ctrl+k,meta+k', () => { @@ -478,6 +484,8 @@ export const CommandMenu = () => { label: command.label, to: command.to, onClick: command.onCommandClick, + firstHotKey: command.firstHotKey, + secondHotKey: command.secondHotKey, }), }, { @@ -489,6 +497,8 @@ export const CommandMenu = () => { label: command.label, to: command.to, onClick: command.onCommandClick, + firstHotKey: command.firstHotKey, + secondHotKey: command.secondHotKey, }), }, { @@ -506,6 +516,8 @@ export const CommandMenu = () => { placeholder={`${person.name.firstName} ${person.name.lastName}`} /> ), + firstHotKey: person.firstHotKey, + secondHotKey: person.secondHotKey, }), }, { @@ -524,6 +536,8 @@ export const CommandMenu = () => { )} /> ), + firstHotKey: company.firstHotKey, + secondHotKey: company.secondHotKey, }), }, { @@ -628,6 +642,8 @@ export const CommandMenu = () => { : '' }`} onClick={copilotCommand.onCommandClick} + firstHotKey={copilotCommand.firstHotKey} + secondHotKey={copilotCommand.secondHotKey} /> @@ -646,6 +662,12 @@ export const CommandMenu = () => { onClick={ standardActionrecordSelectionCommand.onCommandClick } + firstHotKey={ + standardActionrecordSelectionCommand.firstHotKey + } + secondHotKey={ + standardActionrecordSelectionCommand.secondHotKey + } /> ), @@ -663,6 +685,12 @@ export const CommandMenu = () => { onClick={ workflowRunRecordSelectionCommand.onCommandClick } + firstHotKey={ + workflowRunRecordSelectionCommand.firstHotKey + } + secondHotKey={ + workflowRunRecordSelectionCommand.secondHotKey + } /> ), @@ -683,6 +711,12 @@ export const CommandMenu = () => { onClick={ standardActionGlobalCommand.onCommandClick } + firstHotKey={ + standardActionGlobalCommand.firstHotKey + } + secondHotKey={ + standardActionGlobalCommand.secondHotKey + } /> ), @@ -702,6 +736,10 @@ export const CommandMenu = () => { label={workflowRunGlobalCommand.label} Icon={workflowRunGlobalCommand.Icon} onClick={workflowRunGlobalCommand.onCommandClick} + firstHotKey={workflowRunGlobalCommand.firstHotKey} + secondHotKey={ + workflowRunGlobalCommand.secondHotKey + } /> ), @@ -713,8 +751,16 @@ export const CommandMenu = () => { items?.length ? ( {items.map((item) => { - const { id, Icon, label, to, onClick, key } = - renderItem(item); + const { + id, + Icon, + label, + to, + onClick, + key, + firstHotKey, + secondHotKey, + } = renderItem(item); return ( { label={label} to={to} onClick={onClick} + firstHotKey={firstHotKey} + secondHotKey={secondHotKey} /> ); diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuCommandsEffect.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuCommandsEffect.tsx deleted file mode 100644 index aa3e89b1a..000000000 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuCommandsEffect.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector'; -import { commandMenuCommandsState } from '@/command-menu/states/commandMenuCommandsState'; -import { computeCommandMenuCommands } from '@/command-menu/utils/computeCommandMenuCommands'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { useEffect } from 'react'; -import { useSetRecoilState } from 'recoil'; - -export const CommandMenuCommandsEffect = () => { - const actionMenuEntries = useRecoilComponentValueV2( - actionMenuEntriesComponentSelector, - ); - - const setCommands = useSetRecoilState(commandMenuCommandsState); - - useEffect(() => { - setCommands(computeCommandMenuCommands(actionMenuEntries)); - }, [actionMenuEntries, setCommands]); - - return null; -}; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordChip.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordChip.tsx index e87c4311c..ec1c0d836 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordChip.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordChip.tsx @@ -1,15 +1,8 @@ -import { useContextStoreSelectedRecords } from '@/context-store/hooks/useContextStoreSelectedRecords'; -import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; -import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon'; +import { CommandMenuContextRecordChipAvatars } from '@/command-menu/components/CommandMenuContextRecordChipAvatars'; +import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore'; import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier'; -import { useRecordChipData } from '@/object-record/hooks/useRecordChipData'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { Avatar } from 'twenty-ui'; import { capitalize } from '~/utils/string/capitalize'; const StyledChip = styled.div` @@ -28,70 +21,23 @@ const StyledChip = styled.div` color: ${({ theme }) => theme.font.color.primary}; `; -const StyledAvatarWrapper = styled.div` - background-color: ${({ theme }) => theme.background.primary}; - border-radius: ${({ theme }) => theme.border.radius.sm}; - padding: ${({ theme }) => theme.spacing(0.5)}; - border: 1px solid ${({ theme }) => theme.border.color.medium}; - &:not(:first-of-type) { - margin-left: -${({ theme }) => theme.spacing(1)}; - } - display: flex; - align-items: center; - justify-content: center; -`; - const StyledAvatarContainer = styled.div` display: flex; `; -const CommandMenuContextRecordChipAvatars = ({ - objectMetadataItem, - record, +export const CommandMenuContextRecordChip = ({ + objectMetadataItemId, }: { - objectMetadataItem: ObjectMetadataItem; - record: ObjectRecord; + objectMetadataItemId: string; }) => { - const { recordChipData } = useRecordChipData({ - objectNameSingular: objectMetadataItem.nameSingular, - record, - }); - - const { Icon, IconColor } = useGetStandardObjectIcon( - objectMetadataItem.nameSingular, - ); - - const theme = useTheme(); - - return ( - - {Icon ? ( - - ) : ( - - )} - - ); -}; - -export const CommandMenuContextRecordChip = () => { - const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2( - contextStoreCurrentObjectMetadataIdComponentState, - ); - const { objectMetadataItem } = useObjectMetadataItemById({ - objectId: contextStoreCurrentObjectMetadataId ?? '', + objectId: objectMetadataItemId, }); - const { records, loading, totalCount } = useContextStoreSelectedRecords({ - limit: 3, - }); + const { records, loading, totalCount } = + useFindManyRecordsSelectedInContextStore({ + limit: 3, + }); if (loading || !totalCount) { return null; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordChipAvatars.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordChipAvatars.tsx new file mode 100644 index 000000000..a83ab135a --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuContextRecordChipAvatars.tsx @@ -0,0 +1,55 @@ +import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useRecordChipData } from '@/object-record/hooks/useRecordChipData'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { Avatar } from 'twenty-ui'; + +const StyledAvatarWrapper = styled.div` + background-color: ${({ theme }) => theme.background.primary}; + border-radius: ${({ theme }) => theme.border.radius.sm}; + padding: ${({ theme }) => theme.spacing(0.5)}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + &:not(:first-of-type) { + margin-left: -${({ theme }) => theme.spacing(1)}; + } + display: flex; + align-items: center; + justify-content: center; +`; + +export const CommandMenuContextRecordChipAvatars = ({ + objectMetadataItem, + record, +}: { + objectMetadataItem: ObjectMetadataItem; + record: ObjectRecord; +}) => { + const { recordChipData } = useRecordChipData({ + objectNameSingular: objectMetadataItem.nameSingular, + record, + }); + + const { Icon, IconColor } = useGetStandardObjectIcon( + objectMetadataItem.nameSingular, + ); + + const theme = useTheme(); + + return ( + + {Icon ? ( + + ) : ( + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx index 2385683fd..4bb2bc904 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenuTopBar.tsx @@ -2,8 +2,10 @@ import { CommandMenuContextRecordChip } from '@/command-menu/components/CommandM 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 { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import styled from '@emotion/styled'; -import { IconX, LightIconButton, useIsMobile } from 'twenty-ui'; +import { IconX, LightIconButton, isDefined, useIsMobile } from 'twenty-ui'; const StyledInputContainer = styled.div` align-items: center; @@ -65,9 +67,17 @@ export const CommandMenuTopBar = ({ const { closeCommandMenu } = useCommandMenu(); + const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2( + contextStoreCurrentObjectMetadataIdComponentState, + ); + return ( - + {isDefined(contextStoreCurrentObjectMetadataId) && ( + + )} = { const setCurrentWorkspaceMember = useSetRecoilState( currentWorkspaceMemberState, ); - const objectMetadataItems = useRecoilValue(objectMetadataItemsState); - - const { addToCommandMenu, setObjectsInCommandMenu, openCommandMenu } = - useCommandMenu(); + const setIsCommandMenuOpened = useSetRecoilState( + isCommandMenuOpenedState, + ); setCurrentWorkspace(mockDefaultWorkspace); setCurrentWorkspaceMember(mockedWorkspaceMemberData); - - useEffect(() => { - const nonSystemActiveObjects = objectMetadataItems.filter( - (object) => !object.isSystem && object.isActive, - ); - - setObjectsInCommandMenu(nonSystemActiveObjects); - - addToCommandMenu([ - { - id: 'create-task', - to: '', - label: 'Create Task', - type: CommandType.Create, - Icon: IconCheckbox, - onCommandClick: action('create task click'), - }, - { - id: 'create-note', - to: '', - label: 'Create Note', - type: CommandType.Create, - Icon: IconNotes, - onCommandClick: action('create note click'), - }, - ]); - openCommandMenu(); - }, [ - addToCommandMenu, - setObjectsInCommandMenu, - openCommandMenu, - objectMetadataItems, - ]); + setIsCommandMenuOpened(true); return ; }, @@ -115,9 +77,6 @@ export const DefaultWithoutSearch: Story = { play: async () => { const canvas = within(document.body); - expect( - await canvas.findByText('Create Task', undefined, { timeout: 10000 }), - ).toBeInTheDocument(); expect(await canvas.findByText('Go to People')).toBeInTheDocument(); expect(await canvas.findByText('Go to Companies')).toBeInTheDocument(); expect(await canvas.findByText('Go to Opportunities')).toBeInTheDocument(); @@ -134,7 +93,6 @@ export const MatchingPersonCompanyActivityCreateNavigate: Story = { await userEvent.type(searchInput, 'n'); expect(await canvas.findByText('Linkedin')).toBeInTheDocument(); expect(await canvas.findByText(companiesMock[0].name)).toBeInTheDocument(); - expect(await canvas.findByText('Create Note')).toBeInTheDocument(); expect(await canvas.findByText('Go to Companies')).toBeInTheDocument(); }, }; @@ -145,7 +103,6 @@ export const OnlyMatchingCreateAndNavigate: Story = { const searchInput = await canvas.findByPlaceholderText('Type anything'); await sleep(openTimeout); await userEvent.type(searchInput, 'ta'); - expect(await canvas.findByText('Create Task')).toBeInTheDocument(); expect(await canvas.findByText('Go to Tasks')).toBeInTheDocument(); }, }; diff --git a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuCommands.ts b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuNavigateCommands.ts similarity index 94% rename from packages/twenty-front/src/modules/command-menu/constants/CommandMenuCommands.ts rename to packages/twenty-front/src/modules/command-menu/constants/CommandMenuNavigateCommands.ts index 711fbff88..c997ff304 100644 --- a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuCommands.ts +++ b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuNavigateCommands.ts @@ -8,7 +8,7 @@ import { import { Command, CommandType } from '../types/Command'; -export const COMMAND_MENU_COMMANDS: { [key: string]: Command } = { +export const COMMAND_MENU_NAVIGATE_COMMANDS: { [key: string]: Command } = { people: { id: 'go-to-people', to: '/objects/people', 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 db4bdf31a..aac286a6e 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 @@ -1,12 +1,12 @@ import { renderHook } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; import { MemoryRouter } from 'react-router-dom'; -import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil'; +import { RecoilRoot, useRecoilValue } from 'recoil'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; -import { commandMenuCommandsState } from '@/command-menu/states/commandMenuCommandsState'; +import { commandMenuCommandsComponentSelector } from '@/command-menu/states/commandMenuCommandsSelector'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; -import { CommandType } from '@/command-menu/types/Command'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; const Wrapper = ({ children }: { children: React.ReactNode }) => ( @@ -24,15 +24,15 @@ const renderHooks = () => { () => { const commandMenu = useCommandMenu(); const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState); - const [commandMenuCommands, setCommandMenuCommands] = useRecoilState( - commandMenuCommandsState, + const commandMenuCommands = useRecoilComponentValueV2( + commandMenuCommandsComponentSelector, + 'command-menu', ); return { commandMenu, isCommandMenuOpened, commandMenuCommands, - setCommandMenuCommands, }; }, { @@ -77,24 +77,6 @@ describe('useCommandMenu', () => { expect(result.current.isCommandMenuOpened).toBe(false); }); - it('should add commands to the menu', () => { - const { result } = renderHooks(); - - expect( - result.current.commandMenuCommands.find((cmd) => cmd.label === 'Test'), - ).toBeUndefined(); - - act(() => { - result.current.commandMenu.addToCommandMenu([ - { label: 'Test', id: 'test', to: '/test', type: CommandType.Navigate }, - ]); - }); - - expect( - result.current.commandMenuCommands.find((cmd) => cmd.label === 'Test'), - ).toBeDefined(); - }); - it('onItemClick', () => { const { result } = renderHooks(); const onClickMock = jest.fn(); @@ -106,43 +88,4 @@ describe('useCommandMenu', () => { expect(result.current.isCommandMenuOpened).toBe(true); expect(onClickMock).toHaveBeenCalledTimes(1); }); - - it('should setObjectsInCommandMenu command menu', () => { - const { result } = renderHooks(); - - act(() => { - result.current.commandMenu.setObjectsInCommandMenu([]); - }); - - expect(result.current.commandMenuCommands.length).toBe(1); - - act(() => { - result.current.commandMenu.setObjectsInCommandMenu([ - { - id: 'b88745ce-9021-4316-a018-8884e02d05ca', - nameSingular: 'task', - namePlural: 'tasks', - labelSingular: 'Task', - labelPlural: 'Tasks', - isLabelSyncedWithName: true, - shortcut: 'T', - description: 'A task', - icon: 'IconCheckbox', - isCustom: false, - isRemote: false, - isActive: true, - isSystem: false, - createdAt: '2024-09-12T20:23:46.041Z', - updatedAt: '2024-09-13T08:36:53.426Z', - labelIdentifierFieldMetadataId: - 'ab7901eb-43e1-4dc7-8f3b-cdee2857eb9a', - imageIdentifierFieldMetadataId: null, - fields: [], - indexMetadatas: [], - }, - ]); - }); - - expect(result.current.commandMenuCommands.length).toBe(2); - }); }); 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 45618ba2e..6967fdbba 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts @@ -9,24 +9,17 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { isDefined } from '~/utils/isDefined'; -import { COMMAND_MENU_COMMANDS } from '@/command-menu/constants/CommandMenuCommands'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId'; -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { ALL_ICONS } from '@ui/display/icon/providers/internal/AllIcons'; -import { sortByProperty } from '~/utils/array/sortByProperty'; -import { commandMenuCommandsState } from '../states/commandMenuCommandsState'; import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState'; -import { Command, CommandType } from '../types/Command'; export const useCommandMenu = () => { const navigate = useNavigate(); const setIsCommandMenuOpened = useSetRecoilState(isCommandMenuOpenedState); - const setCommands = useSetRecoilState(commandMenuCommandsState); const { resetSelectedItem } = useSelectableList('command-menu-list'); const { setHotkeyScopeAndMemorizePreviousScope, @@ -161,36 +154,6 @@ export const useCommandMenu = () => { [closeCommandMenu, openCommandMenu], ); - const addToCommandMenu = useCallback( - (addCommand: Command[]) => { - setCommands((prev) => [...prev, ...addCommand]); - }, - [setCommands], - ); - - const setObjectsInCommandMenu = (menuItems: ObjectMetadataItem[]) => { - const formattedItems = [ - ...[ - ...menuItems.map( - (item) => - ({ - id: item.id, - to: `/objects/${item.namePlural}`, - label: `Go to ${item.labelPlural}`, - type: CommandType.Navigate, - firstHotKey: item.shortcut ? 'G' : undefined, - secondHotKey: item.shortcut, - Icon: ALL_ICONS[ - (item?.icon as keyof typeof ALL_ICONS) ?? 'IconArrowUpRight' - ], - }) as Command, - ), - ].sort(sortByProperty('label', 'asc')), - COMMAND_MENU_COMMANDS.settings, - ]; - setCommands(formattedItems); - }; - const onItemClick = useCallback( (onClick?: () => void, to?: string) => { toggleCommandMenu(); @@ -211,8 +174,6 @@ export const useCommandMenu = () => { openCommandMenu, closeCommandMenu, toggleCommandMenu, - addToCommandMenu, onItemClick, - setObjectsInCommandMenu, }; }; diff --git a/packages/twenty-front/src/modules/command-menu/states/commandMenuCommandsSelector.ts b/packages/twenty-front/src/modules/command-menu/states/commandMenuCommandsSelector.ts new file mode 100644 index 000000000..bff43955c --- /dev/null +++ b/packages/twenty-front/src/modules/command-menu/states/commandMenuCommandsSelector.ts @@ -0,0 +1,23 @@ +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/states/commandMenuCommandsState.ts b/packages/twenty-front/src/modules/command-menu/states/commandMenuCommandsState.ts deleted file mode 100644 index 309754be2..000000000 --- a/packages/twenty-front/src/modules/command-menu/states/commandMenuCommandsState.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createState } from 'twenty-ui'; - -import { Command, CommandType } from '../types/Command'; - -export const commandMenuCommandsState = createState({ - key: 'command-menu/commandMenuCommandsState', - defaultValue: [ - { - id: '', - to: '', - label: '', - type: CommandType.Navigate, - }, - ], -}); diff --git a/packages/twenty-front/src/modules/command-menu/utils/computeCommandMenuCommands.ts b/packages/twenty-front/src/modules/command-menu/utils/computeCommandMenuCommands.ts index 933ad9c39..706bdd3c3 100644 --- a/packages/twenty-front/src/modules/command-menu/utils/computeCommandMenuCommands.ts +++ b/packages/twenty-front/src/modules/command-menu/utils/computeCommandMenuCommands.ts @@ -3,7 +3,7 @@ import { ActionMenuEntryScope, ActionMenuEntryType, } from '@/action-menu/types/ActionMenuEntry'; -import { COMMAND_MENU_COMMANDS } from '@/command-menu/constants/CommandMenuCommands'; +import { COMMAND_MENU_NAVIGATE_COMMANDS } from '@/command-menu/constants/CommandMenuNavigateCommands'; import { Command, CommandScope, @@ -13,7 +13,7 @@ import { export const computeCommandMenuCommands = ( actionMenuEntries: ActionMenuEntry[], ): Command[] => { - const commands = Object.values(COMMAND_MENU_COMMANDS); + const navigateCommands = Object.values(COMMAND_MENU_NAVIGATE_COMMANDS); const actionCommands: Command[] = actionMenuEntries ?.filter( @@ -49,5 +49,5 @@ export const computeCommandMenuCommands = ( : CommandScope.Global, })); - return [...commands, ...actionCommands, ...workflowRunCommands]; + return [...navigateCommands, ...actionCommands, ...workflowRunCommands]; }; diff --git a/packages/twenty-front/src/modules/context-store/components/MainContextStoreComponentInstanceIdSetterEffect.tsx b/packages/twenty-front/src/modules/context-store/components/MainContextStoreComponentInstanceIdSetterEffect.tsx index 633203892..f1cfc3e32 100644 --- a/packages/twenty-front/src/modules/context-store/components/MainContextStoreComponentInstanceIdSetterEffect.tsx +++ b/packages/twenty-front/src/modules/context-store/components/MainContextStoreComponentInstanceIdSetterEffect.tsx @@ -1,3 +1,4 @@ +import { CONTEXT_STORE_INSTANCE_ID_DEFAULT_VALUE } from '@/context-store/constants/ContextStoreInstanceIdDefaultValue'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId'; import { useContext, useEffect } from 'react'; @@ -11,10 +12,14 @@ export const MainContextStoreComponentInstanceIdSetterEffect = () => { const context = useContext(ContextStoreComponentInstanceContext); useEffect(() => { - setMainContextStoreComponentInstanceId(context?.instanceId ?? 'app'); + setMainContextStoreComponentInstanceId( + context?.instanceId ?? CONTEXT_STORE_INSTANCE_ID_DEFAULT_VALUE, + ); return () => { - setMainContextStoreComponentInstanceId('app'); + setMainContextStoreComponentInstanceId( + CONTEXT_STORE_INSTANCE_ID_DEFAULT_VALUE, + ); }; }, [context, setMainContextStoreComponentInstanceId]); diff --git a/packages/twenty-front/src/modules/context-store/constants/ContextStoreInstanceIdDefaultValue.tsx b/packages/twenty-front/src/modules/context-store/constants/ContextStoreInstanceIdDefaultValue.tsx new file mode 100644 index 000000000..e6ecdbdad --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/constants/ContextStoreInstanceIdDefaultValue.tsx @@ -0,0 +1 @@ +export const CONTEXT_STORE_INSTANCE_ID_DEFAULT_VALUE = 'app'; diff --git a/packages/twenty-front/src/modules/context-store/hooks/useContextStoreCurrentObjectMetadataIdOrThrow.ts b/packages/twenty-front/src/modules/context-store/hooks/useContextStoreCurrentObjectMetadataIdOrThrow.ts index cf147833a..2b265fe47 100644 --- a/packages/twenty-front/src/modules/context-store/hooks/useContextStoreCurrentObjectMetadataIdOrThrow.ts +++ b/packages/twenty-front/src/modules/context-store/hooks/useContextStoreCurrentObjectMetadataIdOrThrow.ts @@ -4,15 +4,14 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/ export const useContextStoreCurrentObjectMetadataIdOrThrow = ( instanceId?: string, ) => { - const contextStoreCurrentObjectMetadataIdComponent = - useRecoilComponentValueV2( - contextStoreCurrentObjectMetadataIdComponentState, - instanceId, - ); + const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2( + contextStoreCurrentObjectMetadataIdComponentState, + instanceId, + ); - if (!contextStoreCurrentObjectMetadataIdComponent) { + if (!contextStoreCurrentObjectMetadataId) { throw new Error('contextStoreCurrentObjectMetadataIdComponent is not set'); } - return contextStoreCurrentObjectMetadataIdComponent; + return contextStoreCurrentObjectMetadataId; }; diff --git a/packages/twenty-front/src/modules/context-store/hooks/useContextStoreSelectedRecords.ts b/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts similarity index 96% rename from packages/twenty-front/src/modules/context-store/hooks/useContextStoreSelectedRecords.ts rename to packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts index 362e44276..4d6867e6d 100644 --- a/packages/twenty-front/src/modules/context-store/hooks/useContextStoreSelectedRecords.ts +++ b/packages/twenty-front/src/modules/context-store/hooks/useFindManyRecordsSelectedInContextStore.ts @@ -6,7 +6,7 @@ import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMeta import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -export const useContextStoreSelectedRecords = ({ +export const useFindManyRecordsSelectedInContextStore = ({ instanceId, limit = 3, }: { diff --git a/packages/twenty-front/src/modules/context-store/states/mainContextStoreComponentInstanceId.ts b/packages/twenty-front/src/modules/context-store/states/mainContextStoreComponentInstanceId.ts index 242b85e64..461c03be8 100644 --- a/packages/twenty-front/src/modules/context-store/states/mainContextStoreComponentInstanceId.ts +++ b/packages/twenty-front/src/modules/context-store/states/mainContextStoreComponentInstanceId.ts @@ -1,6 +1,7 @@ +import { CONTEXT_STORE_INSTANCE_ID_DEFAULT_VALUE } from '@/context-store/constants/ContextStoreInstanceIdDefaultValue'; import { createState } from 'twenty-ui'; export const mainContextStoreComponentInstanceIdState = createState({ key: 'mainContextStoreComponentInstanceIdState', - defaultValue: 'app', + defaultValue: CONTEXT_STORE_INSTANCE_ID_DEFAULT_VALUE, }); 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 c0f4ff700..2e3c99c77 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,10 +1,9 @@ -import { GlobalActionMenuEntriesSetter } from '@/action-menu/actions/global-actions/components/GlobalActionMenuEntriesSetter'; 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 { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { AuthModal } from '@/auth/components/AuthModal'; import { CommandMenu } from '@/command-menu/components/CommandMenu'; -import { CommandMenuCommandsEffect } from '@/command-menu/components/CommandMenuCommandsEffect'; import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary'; import { KeyboardShortcutMenu } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenu'; @@ -93,9 +92,8 @@ export const DefaultLayout = () => { value={{ instanceId: 'command-menu' }} > - + - diff --git a/packages/twenty-front/src/testing/graphqlMocks.ts b/packages/twenty-front/src/testing/graphqlMocks.ts index da49b618d..151fc1d18 100644 --- a/packages/twenty-front/src/testing/graphqlMocks.ts +++ b/packages/twenty-front/src/testing/graphqlMocks.ts @@ -716,6 +716,11 @@ export const graphqlMocks = { }, deletedAt: null, workflowId: '200c1508-f102-4bb9-af32-eda55239ae61', + workflow: { + __typename: 'Workflow', + id: '200c1508-f102-4bb9-af32-eda55239ae61', + name: '1231 qqerrt', + }, }, }, ],