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',
+ },
},
},
],