diff --git a/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx
index 082c3a1a8b..34fded19dc 100644
--- a/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx
+++ b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx
@@ -1,12 +1,10 @@
import { AgentChatProvider } from '@/ai/components/AgentChatProvider';
-import { CommandMenuConfirmationModalManager } from '@/command-menu-item/confirmation-modal/components/CommandMenuConfirmationModalManager';
import { ApolloProvider } from '@/apollo/components/ApolloProvider';
+import { CommandMenuConfirmationModalManager } from '@/command-menu-item/confirmation-modal/components/CommandMenuConfirmationModalManager';
import { MinimalMetadataGater } from '@/metadata-store/components/MinimalMetadataGater';
import { IsMinimalMetadataReadyEffect } from '@/metadata-store/effect-components/IsMinimalMetadataReadyEffect';
import { GotoHotkeysEffectsProvider } from '@/app/effect-components/GotoHotkeysEffectsProvider';
-import { MinimalMetadataLoadEffect } from '@/metadata-store/effect-components/MinimalMetadataLoadEffect';
-import { UserMetadataProviderInitialEffect } from '@/metadata-store/effect-components/UserMetadataProviderInitialEffect';
import { PageChangeEffect } from '@/app/effect-components/PageChangeEffect';
import { AuthProvider } from '@/auth/components/AuthProvider';
import { CaptchaProvider } from '@/captcha/components/CaptchaProvider';
@@ -17,10 +15,13 @@ import { ClientConfigProviderEffect } from '@/client-config/components/ClientCon
import { MainContextStoreProvider } from '@/context-store/components/MainContextStoreProvider';
import { ErrorMessageEffect } from '@/error-handler/components/ErrorMessageEffect';
import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect';
-import { HeadlessFrontComponentMountRoot } from '@/front-components/components/HeadlessFrontComponentMountRoot';
+import { MinimalMetadataLoadEffect } from '@/metadata-store/effect-components/MinimalMetadataLoadEffect';
+import { UserMetadataProviderInitialEffect } from '@/metadata-store/effect-components/UserMetadataProviderInitialEffect';
import { ApolloCoreProvider } from '@/object-metadata/components/ApolloCoreProvider';
import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider';
+import { HeadlessEngineCommandMountRoot } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandMountRoot';
+import { HeadlessFrontComponentMountRoot } from '@/front-components/components/HeadlessFrontComponentMountRoot';
import { SSEProvider } from '@/sse-db-event/components/SSEProvider';
import { SupportChatEffect } from '@/support/components/SupportChatEffect';
import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager';
@@ -74,6 +75,7 @@ export const AppRouterProviders = () => {
+
diff --git a/packages/twenty-front/src/modules/command-menu-item/confirmation-modal/components/CommandMenuConfirmationModalManager.tsx b/packages/twenty-front/src/modules/command-menu-item/confirmation-modal/components/CommandMenuConfirmationModalManager.tsx
index f562e182ef..a926a22205 100644
--- a/packages/twenty-front/src/modules/command-menu-item/confirmation-modal/components/CommandMenuConfirmationModalManager.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/confirmation-modal/components/CommandMenuConfirmationModalManager.tsx
@@ -1,5 +1,5 @@
-import { COMMAND_MENU_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME } from '@/command-menu-item/confirmation-modal/constants/CommandMenuItemConfirmationModalResultBrowserEventName';
import { COMMAND_MENU_CONFIRMATION_MODAL_INSTANCE_ID } from '@/command-menu-item/confirmation-modal/constants/CommandMenuItemConfirmationModalId';
+import { COMMAND_MENU_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME } from '@/command-menu-item/confirmation-modal/constants/CommandMenuItemConfirmationModalResultBrowserEventName';
import { commandMenuItemConfirmationModalConfigState } from '@/command-menu-item/confirmation-modal/states/commandMenuItemConfirmationModalState';
import {
type CommandMenuConfirmationModalResult,
@@ -10,6 +10,7 @@ import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpe
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
+import { isDefined } from 'twenty-shared/utils';
export const CommandMenuConfirmationModalManager = () => {
const commandMenuItemConfirmationModalConfig = useAtomStateValue(
@@ -23,12 +24,12 @@ export const CommandMenuConfirmationModalManager = () => {
commandMenuItemConfirmationModalConfigState,
);
- const callerId = commandMenuItemConfirmationModalConfig?.frontComponentId;
+ const caller = commandMenuItemConfirmationModalConfig?.caller;
const emitConfirmationResult = (
confirmationResult: CommandMenuConfirmationModalResult,
) => {
- if (!callerId) {
+ if (!isDefined(caller)) {
return;
}
@@ -37,7 +38,7 @@ export const CommandMenuConfirmationModalManager = () => {
COMMAND_MENU_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME,
{
detail: {
- frontComponentId: callerId,
+ caller,
confirmationResult,
},
},
diff --git a/packages/twenty-front/src/modules/command-menu-item/confirmation-modal/hooks/useCommandMenuConfirmationModal.ts b/packages/twenty-front/src/modules/command-menu-item/confirmation-modal/hooks/useCommandMenuConfirmationModal.ts
index 11a56bb921..bc4ecbcd14 100644
--- a/packages/twenty-front/src/modules/command-menu-item/confirmation-modal/hooks/useCommandMenuConfirmationModal.ts
+++ b/packages/twenty-front/src/modules/command-menu-item/confirmation-modal/hooks/useCommandMenuConfirmationModal.ts
@@ -33,7 +33,7 @@ export const useCommandMenuConfirmationModal = () => {
isCommandMenuItemConfirmationModalOpened
) {
throw new Error(
- 'Command menu item confirmation modal is already active for another front component',
+ 'Command menu item confirmation modal is already active for another caller',
);
}
diff --git a/packages/twenty-front/src/modules/command-menu-item/confirmation-modal/states/commandMenuItemConfirmationModalState.ts b/packages/twenty-front/src/modules/command-menu-item/confirmation-modal/states/commandMenuItemConfirmationModalState.ts
index b6f3448810..3a77b03023 100644
--- a/packages/twenty-front/src/modules/command-menu-item/confirmation-modal/states/commandMenuItemConfirmationModalState.ts
+++ b/packages/twenty-front/src/modules/command-menu-item/confirmation-modal/states/commandMenuItemConfirmationModalState.ts
@@ -1,10 +1,11 @@
import { type ReactNode } from 'react';
+import { type ConfirmationModalCaller } from 'twenty-shared/types';
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { type ButtonAccent } from 'twenty-ui/input';
export type CommandMenuItemConfirmationModalConfig = {
- frontComponentId: string;
+ caller: ConfirmationModalCaller;
title: string;
subtitle: ReactNode;
confirmButtonText?: string;
diff --git a/packages/twenty-front/src/modules/command-menu-item/confirmation-modal/types/CommandMenuConfirmationModalResultBrowserEventDetail.ts b/packages/twenty-front/src/modules/command-menu-item/confirmation-modal/types/CommandMenuConfirmationModalResultBrowserEventDetail.ts
index e5030c7ae1..41bfe70b47 100644
--- a/packages/twenty-front/src/modules/command-menu-item/confirmation-modal/types/CommandMenuConfirmationModalResultBrowserEventDetail.ts
+++ b/packages/twenty-front/src/modules/command-menu-item/confirmation-modal/types/CommandMenuConfirmationModalResultBrowserEventDetail.ts
@@ -1,6 +1,8 @@
+import { type ConfirmationModalCaller } from 'twenty-shared/types';
+
export type CommandMenuConfirmationModalResult = 'confirm' | 'cancel';
export type CommandMenuConfirmationModalResultBrowserEventDetail = {
- frontComponentId: string;
+ caller: ConfirmationModalCaller;
confirmationResult: CommandMenuConfirmationModalResult;
};
diff --git a/packages/twenty-front/src/modules/command-menu-item/display/components/CommandMenuItemComponent.tsx b/packages/twenty-front/src/modules/command-menu-item/display/components/CommandMenuItemComponent.tsx
index 0b9bc213ad..ecc70d584b 100644
--- a/packages/twenty-front/src/modules/command-menu-item/display/components/CommandMenuItemComponent.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/display/components/CommandMenuItemComponent.tsx
@@ -1,5 +1,5 @@
-import { type CommandMenuItemConfig } from '@/command-menu-item/types/CommandMenuItemConfig';
import { CommandConfigContext } from '@/command-menu-item/contexts/CommandConfigContext';
+import { type CommandMenuItemConfig } from '@/command-menu-item/types/CommandMenuItemConfig';
export const CommandMenuItemComponent = ({
action,
diff --git a/packages/twenty-front/src/modules/command-menu-item/display/components/CommandMenuItemErrorBoundary.tsx b/packages/twenty-front/src/modules/command-menu-item/display/components/CommandMenuItemErrorBoundary.tsx
new file mode 100644
index 0000000000..4b3e7c2809
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/display/components/CommandMenuItemErrorBoundary.tsx
@@ -0,0 +1,49 @@
+import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
+import { type ReactNode } from 'react';
+import { ErrorBoundary } from 'react-error-boundary';
+
+type CommandMenuItemErrorBoundaryProps = {
+ children: ReactNode;
+ engineCommandId: string;
+ shouldReportToSentry?: boolean;
+ onError?: () => void;
+};
+
+export const CommandMenuItemErrorBoundary = ({
+ children,
+ engineCommandId,
+ shouldReportToSentry = false,
+ onError,
+}: CommandMenuItemErrorBoundaryProps) => {
+ const { enqueueErrorSnackBar } = useSnackBar();
+
+ const handleError = async (error: Error) => {
+ enqueueErrorSnackBar({ message: error.message });
+
+ if (shouldReportToSentry) {
+ try {
+ const { captureException } = await import('@sentry/react');
+ captureException(error, (scope) => {
+ scope.setTag('component', 'CommandMenuItem');
+
+ return scope;
+ });
+ } catch {
+ // oxlint-disable-next-line no-console
+ console.error('Failed to capture exception with Sentry:', error);
+ }
+ }
+
+ onError?.();
+ };
+
+ return (
+ null}
+ onError={handleError}
+ resetKeys={[engineCommandId]}
+ >
+ {children}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/components/HeadlessConfirmationModalEngineCommandEffect.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/components/HeadlessConfirmationModalEngineCommandEffect.tsx
new file mode 100644
index 0000000000..a069c3b1e5
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/components/HeadlessConfirmationModalEngineCommandEffect.tsx
@@ -0,0 +1,95 @@
+import { useIsHeadlessEngineCommandEffectInitialized } from '@/command-menu-item/engine-command/hooks/useIsHeadlessEngineCommandEffectInitialized';
+import { type ReactNode, useEffect } from 'react';
+
+import { COMMAND_MENU_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME } from '@/command-menu-item/confirmation-modal/constants/CommandMenuItemConfirmationModalResultBrowserEventName';
+import { useCommandMenuConfirmationModal } from '@/command-menu-item/confirmation-modal/hooks/useCommandMenuConfirmationModal';
+import { type CommandMenuConfirmationModalResultBrowserEventDetail } from '@/command-menu-item/confirmation-modal/types/CommandMenuConfirmationModalResultBrowserEventDetail';
+import { useUnmountEngineCommand } from '@/command-menu-item/engine-command/hooks/useUnmountEngineCommand';
+import { EngineCommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/EngineCommandComponentInstanceContext';
+import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
+import { type ButtonAccent } from 'twenty-ui/input';
+
+export type HeadlessConfirmationModalEngineCommandEffectProps = {
+ title: string;
+ subtitle: ReactNode;
+ confirmButtonText: string;
+ confirmButtonAccent?: ButtonAccent;
+ execute: () => void | Promise;
+};
+
+export const HeadlessConfirmationModalEngineCommandEffect = ({
+ title,
+ subtitle,
+ confirmButtonText,
+ confirmButtonAccent = 'danger',
+ execute,
+}: HeadlessConfirmationModalEngineCommandEffectProps) => {
+ const { isInitializedRef, setIsInitialized } =
+ useIsHeadlessEngineCommandEffectInitialized();
+ const engineCommandId = useAvailableComponentInstanceIdOrThrow(
+ EngineCommandComponentInstanceContext,
+ );
+ const unmountEngineCommand = useUnmountEngineCommand();
+ const { openConfirmationModal } = useCommandMenuConfirmationModal();
+
+ useEffect(() => {
+ if (isInitializedRef.current) {
+ return;
+ }
+
+ setIsInitialized(true);
+
+ openConfirmationModal({
+ caller: { type: 'engineCommand', engineCommandId },
+ title,
+ subtitle,
+ confirmButtonText,
+ confirmButtonAccent,
+ });
+ }, [
+ isInitializedRef,
+ setIsInitialized,
+ engineCommandId,
+ openConfirmationModal,
+ title,
+ subtitle,
+ confirmButtonText,
+ confirmButtonAccent,
+ ]);
+
+ useEffect(() => {
+ const handleConfirmationResult = async (event: Event) => {
+ const customEvent =
+ event as CustomEvent;
+
+ const caller = customEvent.detail.caller;
+
+ if (
+ caller.type !== 'engineCommand' ||
+ caller.engineCommandId !== engineCommandId
+ ) {
+ return;
+ }
+
+ if (customEvent.detail.confirmationResult === 'confirm') {
+ await execute();
+ }
+
+ unmountEngineCommand(engineCommandId);
+ };
+
+ window.addEventListener(
+ COMMAND_MENU_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME,
+ handleConfirmationResult,
+ );
+
+ return () => {
+ window.removeEventListener(
+ COMMAND_MENU_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME,
+ handleConfirmationResult,
+ );
+ };
+ }, [engineCommandId, execute, unmountEngineCommand]);
+
+ return null;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/components/HeadlessEngineCommandMountRoot.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/components/HeadlessEngineCommandMountRoot.tsx
new file mode 100644
index 0000000000..d5983f842f
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/components/HeadlessEngineCommandMountRoot.tsx
@@ -0,0 +1,41 @@
+import { CommandMenuItemErrorBoundary } from '@/command-menu-item/display/components/CommandMenuItemErrorBoundary';
+import { ENGINE_COMPONENT_KEY_HEADLESS_COMPONENT_MAP } from '@/command-menu-item/engine-command/constants/EngineComponentKeyHeadlessComponentMap';
+import { useUnmountEngineCommand } from '@/command-menu-item/engine-command/hooks/useUnmountEngineCommand';
+import { EngineCommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/EngineCommandComponentInstanceContext';
+import { mountedEngineCommandsState } from '@/command-menu-item/engine-command/states/mountedEngineCommandsState';
+import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+
+export const HeadlessEngineCommandMountRoot = () => {
+ const mountedEngineCommands = useAtomStateValue(mountedEngineCommandsState);
+ const unmountEngineCommand = useUnmountEngineCommand();
+
+ return (
+ <>
+ {[...mountedEngineCommands.entries()].map(
+ ([engineCommandId, mountContext]) => (
+ unmountEngineCommand(engineCommandId)}
+ >
+
+
+ {
+ ENGINE_COMPONENT_KEY_HEADLESS_COMPONENT_MAP[
+ mountContext.engineComponentKey
+ ]
+ }
+
+
+
+ ),
+ )}
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect.tsx
new file mode 100644
index 0000000000..4ebe7f75bc
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect.tsx
@@ -0,0 +1,50 @@
+import { useIsHeadlessEngineCommandEffectInitialized } from '@/command-menu-item/engine-command/hooks/useIsHeadlessEngineCommandEffectInitialized';
+import { useUnmountEngineCommand } from '@/command-menu-item/engine-command/hooks/useUnmountEngineCommand';
+import { EngineCommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/EngineCommandComponentInstanceContext';
+import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
+import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
+import { useEffect } from 'react';
+
+export type HeadlessEngineCommandWrapperEffectProps = {
+ execute: () => void | Promise;
+};
+
+export const HeadlessEngineCommandWrapperEffect = ({
+ execute,
+}: HeadlessEngineCommandWrapperEffectProps) => {
+ const { isInitializedRef, setIsInitialized } =
+ useIsHeadlessEngineCommandEffectInitialized();
+
+ const engineCommandId = useAvailableComponentInstanceIdOrThrow(
+ EngineCommandComponentInstanceContext,
+ );
+
+ const unmountEngineCommand = useUnmountEngineCommand();
+
+ const { enqueueErrorSnackBar } = useSnackBar();
+
+ useEffect(() => {
+ if (isInitializedRef.current) {
+ return;
+ }
+
+ setIsInitialized(true);
+
+ const run = async () => {
+ await execute();
+
+ unmountEngineCommand(engineCommandId);
+ };
+
+ run();
+ }, [
+ execute,
+ isInitializedRef,
+ setIsInitialized,
+ engineCommandId,
+ unmountEngineCommand,
+ enqueueErrorSnackBar,
+ ]);
+
+ return null;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand.tsx
new file mode 100644
index 0000000000..8ffb407e33
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand.tsx
@@ -0,0 +1,24 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { type PathParam, useNavigate } from 'react-router-dom';
+import { type AppPath } from 'twenty-shared/types';
+import { getAppPath } from 'twenty-shared/utils';
+
+export const HeadlessNavigateEngineCommand = ({
+ to,
+ params,
+ queryParams,
+}: {
+ to: T;
+ params?: { [key in PathParam]: string | null };
+ queryParams?: Record;
+}) => {
+ const navigate = useNavigate();
+ const path = getAppPath(to, params, queryParams);
+
+ // eslint-disable-next-line twenty/no-navigate-prefer-link
+ const onExecute = () => {
+ navigate(path);
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/components/HeadlessOpenSidePanelPageEngineCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/components/HeadlessOpenSidePanelPageEngineCommand.tsx
new file mode 100644
index 0000000000..cb7e6485b0
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/components/HeadlessOpenSidePanelPageEngineCommand.tsx
@@ -0,0 +1,37 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
+import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
+import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
+import { type MessageDescriptor } from '@lingui/core';
+import { t } from '@lingui/core/macro';
+import { type SidePanelPages } from 'twenty-shared/types';
+import { type IconComponent } from 'twenty-ui/display';
+
+export const HeadlessOpenSidePanelPageEngineCommand = ({
+ page,
+ pageTitle,
+ pageIcon,
+ shouldResetSearchState = false,
+}: {
+ page: SidePanelPages;
+ pageTitle: MessageDescriptor;
+ pageIcon: IconComponent;
+ shouldResetSearchState?: boolean;
+}) => {
+ const { navigateSidePanel } = useNavigateSidePanel();
+ const setSidePanelSearch = useSetAtomState(sidePanelSearchState);
+
+ const onExecute = () => {
+ navigateSidePanel({
+ page,
+ pageTitle: t(pageTitle),
+ pageIcon,
+ });
+
+ if (shouldResetSearchState) {
+ setSidePanelSearch('');
+ }
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/constants/EngineComponentKeyHeadlessComponentMap.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/constants/EngineComponentKeyHeadlessComponentMap.tsx
new file mode 100644
index 0000000000..e50cd6b420
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/constants/EngineComponentKeyHeadlessComponentMap.tsx
@@ -0,0 +1,254 @@
+import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
+import { HeadlessOpenSidePanelPageEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessOpenSidePanelPageEngineCommand';
+import { DeleteMultipleRecordsCommand } from '@/command-menu-item/engine-command/record/multiple-records/components/DeleteMultipleRecordsCommand';
+import { DestroyMultipleRecordsCommand } from '@/command-menu-item/engine-command/record/multiple-records/components/DestroyMultipleRecordsCommand';
+import { ExportMultipleRecordsCommand } from '@/command-menu-item/engine-command/record/multiple-records/components/ExportMultipleRecordsCommand';
+import { MergeMultipleRecordsCommand } from '@/command-menu-item/engine-command/record/multiple-records/components/MergeMultipleRecordsCommand';
+import { RestoreMultipleRecordsCommand } from '@/command-menu-item/engine-command/record/multiple-records/components/RestoreMultipleRecordsCommand';
+import { UpdateMultipleRecordsCommand } from '@/command-menu-item/engine-command/record/multiple-records/components/UpdateMultipleRecordsCommand';
+import { CreateNewIndexRecordNoSelectionRecordCommand } from '@/command-menu-item/engine-command/record/no-selection/components/CreateNewIndexRecordNoSelectionRecordCommand';
+import { CreateNewViewNoSelectionRecordCommand } from '@/command-menu-item/engine-command/record/no-selection/components/CreateNewViewNoSelectionRecordCommand';
+import { HideDeletedRecordsNoSelectionRecordCommand } from '@/command-menu-item/engine-command/record/no-selection/components/HideDeletedRecordsNoSelectionRecordCommand';
+import { ImportRecordsNoSelectionRecordCommand } from '@/command-menu-item/engine-command/record/no-selection/components/ImportRecordsNoSelectionRecordCommand';
+import { SeeDeletedRecordsNoSelectionRecordCommand } from '@/command-menu-item/engine-command/record/no-selection/components/SeeDeletedRecordsNoSelectionRecordCommand';
+import { AddToFavoritesSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/AddToFavoritesSingleRecordCommand';
+import { DeleteSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/DeleteSingleRecordCommand';
+import { DestroySingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/DestroySingleRecordCommand';
+import { ExportNoteSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/ExportNoteSingleRecordCommand';
+import { ExportSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/ExportSingleRecordCommand';
+import { NavigateToNextRecordSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/NavigateToNextRecordSingleRecordCommand';
+import { NavigateToPreviousRecordSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/NavigateToPreviousRecordSingleRecordCommand';
+import { RemoveFromFavoritesSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/RemoveFromFavoritesSingleRecordCommand';
+import { RestoreSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/components/RestoreSingleRecordCommand';
+import { CancelDashboardSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/dashboard/components/CancelDashboardSingleRecordCommand';
+import { DuplicateDashboardSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/dashboard/components/DuplicateDashboardSingleRecordCommand';
+import { EditDashboardSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/dashboard/components/EditDashboardSingleRecordCommand';
+import { SaveDashboardSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/dashboard/components/SaveDashboardSingleRecordCommand';
+import { EditRecordPageLayoutSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand';
+import { SeeVersionWorkflowRunSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-runs/components/SeeVersionWorkflowRunSingleRecordCommand';
+import { SeeWorkflowWorkflowRunSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-runs/components/SeeWorkflowWorkflowRunSingleRecordCommand';
+import { StopWorkflowRunSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-runs/components/StopWorkflowRunSingleRecordCommand';
+import { SeeRunsWorkflowVersionSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-versions/components/SeeRunsWorkflowVersionSingleRecordCommand';
+import { SeeVersionsWorkflowVersionSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-versions/components/SeeVersionsWorkflowVersionSingleRecordCommand';
+import { SeeWorkflowWorkflowVersionSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-versions/components/SeeWorkflowWorkflowVersionSingleRecordCommand';
+import { UseAsDraftWorkflowVersionSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow-versions/components/UseAsDraftWorkflowVersionSingleRecordCommand';
+import { ActivateWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/ActivateWorkflowSingleRecordCommand';
+import { AddNodeWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/AddNodeWorkflowSingleRecordCommand';
+import { DeactivateWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/DeactivateWorkflowSingleRecordCommand';
+import { DiscardDraftWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/DiscardDraftWorkflowSingleRecordCommand';
+import { DuplicateWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/DuplicateWorkflowSingleRecordCommand';
+import { SeeActiveVersionWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/SeeActiveVersionWorkflowSingleRecordCommand';
+import { SeeRunsWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/SeeRunsWorkflowSingleRecordCommand';
+import { SeeVersionsWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/SeeVersionsWorkflowSingleRecordCommand';
+import { TestWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/TestWorkflowSingleRecordCommand';
+import { TidyUpWorkflowSingleRecordCommand } from '@/command-menu-item/engine-command/record/single-record/workflow/components/TidyUpWorkflowSingleRecordCommand';
+import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
+import { msg } from '@lingui/core/macro';
+import { AppPath, SettingsPath, SidePanelPages } from 'twenty-shared/types';
+import { IconHistory, IconSearch, IconSparkles } from 'twenty-ui/display';
+import { EngineComponentKey } from '~/generated-metadata/graphql';
+
+export const ENGINE_COMPONENT_KEY_HEADLESS_COMPONENT_MAP: Record<
+ EngineComponentKey,
+ React.ReactNode
+> = {
+ [EngineComponentKey.CREATE_NEW_RECORD]: (
+
+ ),
+ [EngineComponentKey.DELETE_SINGLE_RECORD]: ,
+ [EngineComponentKey.DELETE_MULTIPLE_RECORDS]: (
+
+ ),
+ [EngineComponentKey.RESTORE_SINGLE_RECORD]: ,
+ [EngineComponentKey.RESTORE_MULTIPLE_RECORDS]: (
+
+ ),
+ [EngineComponentKey.DESTROY_SINGLE_RECORD]: ,
+ [EngineComponentKey.DESTROY_MULTIPLE_RECORDS]: (
+
+ ),
+ [EngineComponentKey.ADD_TO_FAVORITES]: ,
+ [EngineComponentKey.REMOVE_FROM_FAVORITES]: (
+
+ ),
+ [EngineComponentKey.MERGE_MULTIPLE_RECORDS]: ,
+ [EngineComponentKey.DUPLICATE_DASHBOARD]: (
+
+ ),
+ [EngineComponentKey.DUPLICATE_WORKFLOW]: (
+
+ ),
+ [EngineComponentKey.ACTIVATE_WORKFLOW]: (
+
+ ),
+ [EngineComponentKey.DEACTIVATE_WORKFLOW]: (
+
+ ),
+ [EngineComponentKey.DISCARD_DRAFT_WORKFLOW]: (
+
+ ),
+ [EngineComponentKey.TEST_WORKFLOW]: ,
+ [EngineComponentKey.STOP_WORKFLOW_RUN]: (
+
+ ),
+ [EngineComponentKey.USE_AS_DRAFT_WORKFLOW_VERSION]: (
+
+ ),
+ [EngineComponentKey.SAVE_DASHBOARD_LAYOUT]: (
+
+ ),
+ [EngineComponentKey.TIDY_UP_WORKFLOW]: ,
+ [EngineComponentKey.NAVIGATE_TO_NEXT_RECORD]: (
+
+ ),
+ [EngineComponentKey.NAVIGATE_TO_PREVIOUS_RECORD]: (
+
+ ),
+ [EngineComponentKey.EXPORT_NOTE_TO_PDF]: ,
+ [EngineComponentKey.EXPORT_FROM_RECORD_INDEX]: (
+
+ ),
+ [EngineComponentKey.EXPORT_FROM_RECORD_SHOW]: ,
+ [EngineComponentKey.EXPORT_MULTIPLE_RECORDS]: (
+
+ ),
+ [EngineComponentKey.UPDATE_MULTIPLE_RECORDS]: (
+
+ ),
+ [EngineComponentKey.IMPORT_RECORDS]: (
+
+ ),
+ [EngineComponentKey.EXPORT_VIEW]: ,
+ [EngineComponentKey.SEE_DELETED_RECORDS]: (
+
+ ),
+ [EngineComponentKey.CREATE_NEW_VIEW]: (
+
+ ),
+ [EngineComponentKey.HIDE_DELETED_RECORDS]: (
+
+ ),
+ [EngineComponentKey.EDIT_RECORD_PAGE_LAYOUT]: (
+
+ ),
+ [EngineComponentKey.EDIT_DASHBOARD_LAYOUT]: (
+
+ ),
+ [EngineComponentKey.CANCEL_DASHBOARD_LAYOUT]: (
+
+ ),
+ [EngineComponentKey.GO_TO_PEOPLE]: (
+
+ ),
+ [EngineComponentKey.GO_TO_COMPANIES]: (
+
+ ),
+ [EngineComponentKey.GO_TO_DASHBOARDS]: (
+
+ ),
+ [EngineComponentKey.GO_TO_OPPORTUNITIES]: (
+
+ ),
+ [EngineComponentKey.GO_TO_SETTINGS]: (
+
+ ),
+ [EngineComponentKey.GO_TO_TASKS]: (
+
+ ),
+ [EngineComponentKey.GO_TO_NOTES]: (
+
+ ),
+ [EngineComponentKey.GO_TO_WORKFLOWS]: (
+
+ ),
+ [EngineComponentKey.GO_TO_RUNS]: (
+
+ ),
+ [EngineComponentKey.SEARCH_RECORDS]: (
+
+ ),
+ [EngineComponentKey.SEARCH_RECORDS_FALLBACK]: (
+
+ ),
+ [EngineComponentKey.ASK_AI]: (
+
+ ),
+ [EngineComponentKey.VIEW_PREVIOUS_AI_CHATS]: (
+
+ ),
+ [EngineComponentKey.SEE_ACTIVE_VERSION_WORKFLOW]: (
+
+ ),
+ [EngineComponentKey.SEE_RUNS_WORKFLOW]: (
+
+ ),
+ [EngineComponentKey.SEE_VERSIONS_WORKFLOW]: (
+
+ ),
+ [EngineComponentKey.ADD_NODE_WORKFLOW]: (
+
+ ),
+ [EngineComponentKey.SEE_VERSION_WORKFLOW_RUN]: (
+
+ ),
+ [EngineComponentKey.SEE_WORKFLOW_WORKFLOW_RUN]: (
+
+ ),
+ [EngineComponentKey.SEE_RUNS_WORKFLOW_VERSION]: (
+
+ ),
+ [EngineComponentKey.SEE_WORKFLOW_WORKFLOW_VERSION]: (
+
+ ),
+ [EngineComponentKey.SEE_VERSIONS_WORKFLOW_VERSION]: (
+
+ ),
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/hooks/useIsHeadlessEngineCommandEffectInitialized.ts b/packages/twenty-front/src/modules/command-menu-item/engine-command/hooks/useIsHeadlessEngineCommandEffectInitialized.ts
new file mode 100644
index 0000000000..27fdde1d2a
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/hooks/useIsHeadlessEngineCommandEffectInitialized.ts
@@ -0,0 +1,12 @@
+import { useRef } from 'react';
+
+export const useIsHeadlessEngineCommandEffectInitialized = () => {
+ // eslint-disable-next-line twenty/no-state-useref
+ const isInitializedRef = useRef(false);
+
+ const setIsInitialized = (value: boolean) => {
+ isInitializedRef.current = value;
+ };
+
+ return { isInitializedRef, setIsInitialized };
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/hooks/useMountEngineCommand.ts b/packages/twenty-front/src/modules/command-menu-item/engine-command/hooks/useMountEngineCommand.ts
new file mode 100644
index 0000000000..00f7370414
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/hooks/useMountEngineCommand.ts
@@ -0,0 +1,129 @@
+import { useCallback } from 'react';
+
+import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
+import { mountedEngineCommandsState } from '@/command-menu-item/engine-command/states/mountedEngineCommandsState';
+import { contextStoreAnyFieldFilterValueComponentState } from '@/context-store/states/contextStoreAnyFieldFilterValueComponentState';
+import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
+import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
+import { contextStoreFilterGroupsComponentState } from '@/context-store/states/contextStoreFilterGroupsComponentState';
+import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
+import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
+import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
+import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
+import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
+import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
+import { useStore } from 'jotai';
+import { isDefined } from 'twenty-shared/utils';
+import { type EngineComponentKey } from '~/generated-metadata/graphql';
+
+export const useMountEngineCommand = () => {
+ const store = useStore();
+
+ const mountEngineCommand = useCallback(
+ (
+ engineCommandId: string,
+ contextStoreInstanceId: string,
+ engineComponentKey: EngineComponentKey,
+ ) => {
+ const objectMetadataItemId = store.get(
+ contextStoreCurrentObjectMetadataItemIdComponentState.atomFamily({
+ instanceId: contextStoreInstanceId,
+ }),
+ );
+
+ const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
+ const objectMetadataItem = objectMetadataItems.find(
+ (item) => item.id === objectMetadataItemId,
+ );
+
+ const currentViewId = store.get(
+ contextStoreCurrentViewIdComponentState.atomFamily({
+ instanceId: contextStoreInstanceId,
+ }),
+ );
+
+ const targetedRecordsRule = store.get(
+ contextStoreTargetedRecordsRuleComponentState.atomFamily({
+ instanceId: contextStoreInstanceId,
+ }),
+ );
+
+ const selectedRecords = (
+ targetedRecordsRule.mode === 'selection'
+ ? targetedRecordsRule.selectedRecordIds
+ : []
+ )
+ .map((id) => store.get(recordStoreFamilyState.atomFamily(id)))
+ .filter(isDefined);
+
+ const filters = store.get(
+ contextStoreFiltersComponentState.atomFamily({
+ instanceId: contextStoreInstanceId,
+ }),
+ );
+
+ const filterGroups = store.get(
+ contextStoreFilterGroupsComponentState.atomFamily({
+ instanceId: contextStoreInstanceId,
+ }),
+ );
+
+ const anyFieldFilterValue = store.get(
+ contextStoreAnyFieldFilterValueComponentState.atomFamily({
+ instanceId: contextStoreInstanceId,
+ }),
+ );
+
+ const currentWorkspaceMember = store.get(
+ currentWorkspaceMemberState.atom,
+ );
+ const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+ const userTimezone =
+ currentWorkspaceMember?.timeZone !== 'system'
+ ? (currentWorkspaceMember?.timeZone ?? systemTimeZone)
+ : systemTimeZone;
+
+ const graphqlFilter = isDefined(objectMetadataItem)
+ ? computeContextStoreFilters({
+ contextStoreTargetedRecordsRule: targetedRecordsRule,
+ contextStoreFilters: filters,
+ contextStoreFilterGroups: filterGroups,
+ objectMetadataItem,
+ filterValueDependencies: {
+ currentWorkspaceMemberId: currentWorkspaceMember?.id,
+ timeZone: userTimezone,
+ },
+ contextStoreAnyFieldFilterValue: anyFieldFilterValue,
+ })
+ : null;
+
+ const recordIndexId =
+ objectMetadataItem && currentViewId
+ ? getRecordIndexIdFromObjectNamePluralAndViewId(
+ objectMetadataItem.namePlural,
+ currentViewId,
+ )
+ : null;
+
+ store.set(mountedEngineCommandsState.atom, (previousMap) => {
+ const newMap = new Map(previousMap);
+
+ newMap.set(engineCommandId, {
+ engineComponentKey,
+ contextStoreInstanceId,
+ objectMetadataItem: objectMetadataItem ?? null,
+ currentViewId,
+ recordIndexId,
+ targetedRecordsRule,
+ selectedRecords,
+ graphqlFilter,
+ });
+
+ return newMap;
+ });
+ },
+ [store],
+ );
+
+ return mountEngineCommand;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/hooks/useMountedEngineCommandContext.ts b/packages/twenty-front/src/modules/command-menu-item/engine-command/hooks/useMountedEngineCommandContext.ts
new file mode 100644
index 0000000000..39f144ae91
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/hooks/useMountedEngineCommandContext.ts
@@ -0,0 +1,22 @@
+import { EngineCommandComponentInstanceContext } from '@/command-menu-item/engine-command/states/contexts/EngineCommandComponentInstanceContext';
+import { mountedEngineCommandsState } from '@/command-menu-item/engine-command/states/mountedEngineCommandsState';
+import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { isDefined } from 'twenty-shared/utils';
+
+export const useMountedEngineCommandContext = () => {
+ const engineCommandId = useAvailableComponentInstanceIdOrThrow(
+ EngineCommandComponentInstanceContext,
+ );
+
+ const mountedEngineCommands = useAtomStateValue(mountedEngineCommandsState);
+ const context = mountedEngineCommands.get(engineCommandId);
+
+ if (!isDefined(context)) {
+ throw new Error(
+ 'Engine command mount context not found. Make sure the command was mounted via the engine command mount flow.',
+ );
+ }
+
+ return context;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/hooks/useUnmountEngineCommand.ts b/packages/twenty-front/src/modules/command-menu-item/engine-command/hooks/useUnmountEngineCommand.ts
new file mode 100644
index 0000000000..caa8858a20
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/hooks/useUnmountEngineCommand.ts
@@ -0,0 +1,23 @@
+import { useCallback } from 'react';
+
+import { mountedEngineCommandsState } from '@/command-menu-item/engine-command/states/mountedEngineCommandsState';
+import { useStore } from 'jotai';
+
+export const useUnmountEngineCommand = () => {
+ const store = useStore();
+
+ const unmountEngineCommand = useCallback(
+ (engineCommandId: string) => {
+ store.set(mountedEngineCommandsState.atom, (previousMap) => {
+ const newMap = new Map(previousMap);
+
+ newMap.delete(engineCommandId);
+
+ return newMap;
+ });
+ },
+ [store],
+ );
+
+ return unmountEngineCommand;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/DeleteMultipleRecordsCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/DeleteMultipleRecordsCommand.tsx
new file mode 100644
index 0000000000..5a24591a0a
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/DeleteMultipleRecordsCommand.tsx
@@ -0,0 +1,42 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
+import { useIncrementalDeleteManyRecords } from '@/object-record/hooks/useIncrementalDeleteManyRecords';
+import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
+import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
+import { isDefined } from 'twenty-shared/utils';
+
+export const DeleteMultipleRecordsCommand = () => {
+ const { recordIndexId, objectMetadataItem, graphqlFilter } =
+ useMountedEngineCommandContext();
+
+ if (
+ !isDefined(recordIndexId) ||
+ !isDefined(objectMetadataItem) ||
+ !isDefined(graphqlFilter)
+ ) {
+ throw new Error(
+ 'Record index ID, object metadata item, and graphql filter are required to delete multiple records',
+ );
+ }
+
+ const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
+
+ const { removeSelectedRecordsFromRecordBoard } =
+ useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
+
+ const { incrementalDeleteManyRecords } = useIncrementalDeleteManyRecords({
+ objectNameSingular: objectMetadataItem.nameSingular,
+ filter: graphqlFilter,
+ pageSize: DEFAULT_QUERY_PAGE_SIZE,
+ delayInMsBetweenMutations: 50,
+ });
+
+ const handleExecute = async () => {
+ removeSelectedRecordsFromRecordBoard();
+ resetTableRowSelection();
+ await incrementalDeleteManyRecords();
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/DestroyMultipleRecordsCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/DestroyMultipleRecordsCommand.tsx
new file mode 100644
index 0000000000..42f6db5b3b
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/DestroyMultipleRecordsCommand.tsx
@@ -0,0 +1,61 @@
+import { HeadlessConfirmationModalEngineCommandEffect } from '@/command-menu-item/engine-command/components/HeadlessConfirmationModalEngineCommandEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
+import { useIncrementalDestroyManyRecords } from '@/object-record/hooks/useIncrementalDestroyManyRecords';
+import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
+import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
+import { t } from '@lingui/core/macro';
+import { type RecordGqlOperationFilter } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
+
+export const DestroyMultipleRecordsCommand = () => {
+ const { recordIndexId, objectMetadataItem, graphqlFilter } =
+ useMountedEngineCommandContext();
+
+ if (
+ !isDefined(recordIndexId) ||
+ !isDefined(objectMetadataItem) ||
+ !isDefined(graphqlFilter)
+ ) {
+ throw new Error(
+ 'Record index ID, object metadata item, and graphql filter are required to destroy multiple records',
+ );
+ }
+
+ const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
+ const { removeSelectedRecordsFromRecordBoard } =
+ useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
+
+ const deletedAtFilter: RecordGqlOperationFilter = {
+ deletedAt: { is: 'NOT_NULL' },
+ };
+
+ const combinedFilter = {
+ ...graphqlFilter,
+ ...deletedAtFilter,
+ };
+
+ const { incrementalDestroyManyRecords } = useIncrementalDestroyManyRecords({
+ objectNameSingular: objectMetadataItem.nameSingular,
+ filter: combinedFilter,
+ pageSize: DEFAULT_QUERY_PAGE_SIZE,
+ delayInMsBetweenMutations: 50,
+ });
+
+ const handleExecute = async () => {
+ removeSelectedRecordsFromRecordBoard();
+
+ resetTableRowSelection();
+
+ await incrementalDestroyManyRecords();
+ };
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/ExportMultipleRecordsCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/ExportMultipleRecordsCommand.tsx
new file mode 100644
index 0000000000..1bea3b5a6e
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/ExportMultipleRecordsCommand.tsx
@@ -0,0 +1,24 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useRecordIndexExportRecords } from '@/object-record/record-index/export/hooks/useRecordIndexExportRecords';
+import { isDefined } from 'twenty-shared/utils';
+
+export const ExportMultipleRecordsCommand = () => {
+ const { objectMetadataItem, recordIndexId } =
+ useMountedEngineCommandContext();
+
+ if (!isDefined(objectMetadataItem) || !isDefined(recordIndexId)) {
+ throw new Error(
+ 'Object metadata item and record index ID are required to export multiple records',
+ );
+ }
+
+ const { download } = useRecordIndexExportRecords({
+ delayMs: 100,
+ objectMetadataItem,
+ recordIndexId,
+ filename: `${objectMetadataItem.nameSingular}.csv`,
+ });
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/MergeMultipleRecordsCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/MergeMultipleRecordsCommand.tsx
new file mode 100644
index 0000000000..8d2b992283
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/MergeMultipleRecordsCommand.tsx
@@ -0,0 +1,29 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useOpenMergeRecordsPageInSidePanel } from '@/side-panel/hooks/useOpenMergeRecordsPageInSidePanel';
+import { isDefined } from 'twenty-shared/utils';
+
+export const MergeMultipleRecordsCommand = () => {
+ const { objectMetadataItem, selectedRecords } =
+ useMountedEngineCommandContext();
+
+ const selectedRecordIds = selectedRecords.map((record) => record.id);
+
+ if (!isDefined(objectMetadataItem) || selectedRecordIds.length === 0) {
+ throw new Error(
+ 'Object metadata item and selected records are required to merge multiple records',
+ );
+ }
+
+ const { openMergeRecordsPageInSidePanel } =
+ useOpenMergeRecordsPageInSidePanel({
+ objectNameSingular: objectMetadataItem.nameSingular,
+ objectRecordIds: selectedRecordIds,
+ });
+
+ const handleExecute = () => {
+ openMergeRecordsPageInSidePanel();
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/RestoreMultipleRecordsCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/RestoreMultipleRecordsCommand.tsx
new file mode 100644
index 0000000000..4ec0ac2d48
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/RestoreMultipleRecordsCommand.tsx
@@ -0,0 +1,71 @@
+import { HeadlessConfirmationModalEngineCommandEffect } from '@/command-menu-item/engine-command/components/HeadlessConfirmationModalEngineCommandEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
+import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
+import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
+import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
+import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
+import { t } from '@lingui/core/macro';
+import { type RecordGqlOperationFilter } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
+
+export const RestoreMultipleRecordsCommand = () => {
+ const { recordIndexId, objectMetadataItem, graphqlFilter } =
+ useMountedEngineCommandContext();
+
+ if (
+ !isDefined(recordIndexId) ||
+ !isDefined(objectMetadataItem) ||
+ !isDefined(graphqlFilter)
+ ) {
+ throw new Error(
+ 'Record index ID, object metadata item, and graphql filter are required to restore multiple records',
+ );
+ }
+
+ const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
+ const { removeSelectedRecordsFromRecordBoard } =
+ useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
+
+ const { restoreManyRecords } = useRestoreManyRecords({
+ objectNameSingular: objectMetadataItem.nameSingular,
+ });
+
+ const deletedAtFilter: RecordGqlOperationFilter = {
+ deletedAt: { is: 'NOT_NULL' },
+ };
+
+ const combinedFilter = {
+ ...graphqlFilter,
+ ...deletedAtFilter,
+ };
+
+ const { fetchAllRecords: fetchAllRecordIds } = useLazyFetchAllRecords({
+ objectNameSingular: objectMetadataItem.nameSingular,
+ filter: combinedFilter,
+ limit: DEFAULT_QUERY_PAGE_SIZE,
+ recordGqlFields: { id: true },
+ });
+
+ const handleExecute = async () => {
+ removeSelectedRecordsFromRecordBoard();
+ const recordsToRestore = await fetchAllRecordIds();
+ const recordIdsToRestore = recordsToRestore.map((record) => record.id);
+
+ resetTableRowSelection();
+
+ await restoreManyRecords({
+ idsToRestore: recordIdsToRestore,
+ });
+ };
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/UpdateMultipleRecordsCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/UpdateMultipleRecordsCommand.tsx
new file mode 100644
index 0000000000..6ad8a2cc76
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/multiple-records/components/UpdateMultipleRecordsCommand.tsx
@@ -0,0 +1,18 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useOpenUpdateMultipleRecordsPageInSidePanel } from '@/side-panel/hooks/useOpenUpdateMultipleRecordsPageInSidePanel';
+
+export const UpdateMultipleRecordsCommand = () => {
+ const { contextStoreInstanceId } = useMountedEngineCommandContext();
+
+ const { openUpdateMultipleRecordsPageInSidePanel } =
+ useOpenUpdateMultipleRecordsPageInSidePanel({
+ contextStoreInstanceId,
+ });
+
+ const handleExecute = () => {
+ openUpdateMultipleRecordsPageInSidePanel();
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/no-selection/components/CreateNewIndexRecordNoSelectionRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/no-selection/components/CreateNewIndexRecordNoSelectionRecordCommand.tsx
new file mode 100644
index 0000000000..9cf9b38964
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/no-selection/components/CreateNewIndexRecordNoSelectionRecordCommand.tsx
@@ -0,0 +1,26 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useCreateNewIndexRecord } from '@/object-record/record-table/hooks/useCreateNewIndexRecord';
+import { isDefined } from 'twenty-shared/utils';
+
+export const CreateNewIndexRecordNoSelectionRecordCommand = () => {
+ const { objectMetadataItem, recordIndexId } =
+ useMountedEngineCommandContext();
+
+ if (!isDefined(objectMetadataItem) || !isDefined(recordIndexId)) {
+ throw new Error(
+ 'Object metadata item and record index ID are required to create new index record',
+ );
+ }
+
+ const { createNewIndexRecord } = useCreateNewIndexRecord({
+ objectMetadataItem,
+ instanceId: recordIndexId,
+ });
+
+ return (
+ createNewIndexRecord({ position: 'first' })}
+ />
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/no-selection/components/CreateNewViewNoSelectionRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/no-selection/components/CreateNewViewNoSelectionRecordCommand.tsx
new file mode 100644
index 0000000000..b0666f0ccc
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/no-selection/components/CreateNewViewNoSelectionRecordCommand.tsx
@@ -0,0 +1,39 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
+import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
+import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId';
+import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode';
+import { viewPickerReferenceViewIdComponentState } from '@/views/view-picker/states/viewPickerReferenceViewIdComponentState';
+import { isDefined } from 'twenty-shared/utils';
+
+export const CreateNewViewNoSelectionRecordCommand = () => {
+ const { currentViewId, recordIndexId } = useMountedEngineCommandContext();
+
+ const { openDropdown } = useOpenDropdown();
+
+ if (!isDefined(currentViewId) || !isDefined(recordIndexId)) {
+ throw new Error(
+ 'Current view ID and record index ID are required to create new view',
+ );
+ }
+
+ const setViewPickerReferenceViewId = useSetAtomComponentState(
+ viewPickerReferenceViewIdComponentState,
+ recordIndexId,
+ );
+
+ const { setViewPickerMode } = useViewPickerMode(recordIndexId);
+
+ const handleExecute = () => {
+ if (currentViewId) {
+ setViewPickerReferenceViewId(currentViewId);
+ }
+ setViewPickerMode('create-empty');
+ openDropdown({
+ dropdownComponentInstanceIdFromProps: VIEW_PICKER_DROPDOWN_ID,
+ });
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/no-selection/components/HideDeletedRecordsNoSelectionRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/no-selection/components/HideDeletedRecordsNoSelectionRecordCommand.tsx
new file mode 100644
index 0000000000..a6ace7cad9
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/no-selection/components/HideDeletedRecordsNoSelectionRecordCommand.tsx
@@ -0,0 +1,49 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useCheckIsSoftDeleteFilter } from '@/object-record/record-filter/hooks/useCheckIsSoftDeleteFilter';
+import { useRemoveRecordFilter } from '@/object-record/record-filter/hooks/useRemoveRecordFilter';
+import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
+import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
+import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
+import { isDefined } from 'twenty-shared/utils';
+
+export const HideDeletedRecordsNoSelectionRecordCommand = () => {
+ const { objectMetadataItem, recordIndexId } =
+ useMountedEngineCommandContext();
+
+ if (!isDefined(objectMetadataItem) || !isDefined(recordIndexId)) {
+ throw new Error(
+ 'Object metadata item and record index ID are required to hide deleted records',
+ );
+ }
+
+ const { toggleSoftDeleteFilterState } = useHandleToggleTrashColumnFilter({
+ objectNameSingular: objectMetadataItem.nameSingular,
+ viewBarId: recordIndexId,
+ recordFiltersInstanceId: recordIndexId,
+ });
+
+ const { isRecordFilterAboutSoftDelete } = useCheckIsSoftDeleteFilter();
+
+ const currentRecordFilters = useAtomComponentStateValue(
+ currentRecordFiltersComponentState,
+ recordIndexId,
+ );
+
+ const deletedFilter = currentRecordFilters.find(
+ isRecordFilterAboutSoftDelete,
+ );
+
+ const { removeRecordFilter } = useRemoveRecordFilter(recordIndexId);
+
+ const handleExecute = () => {
+ if (!isDefined(deletedFilter)) {
+ return;
+ }
+
+ removeRecordFilter({ recordFilterId: deletedFilter.id });
+ toggleSoftDeleteFilterState(false);
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/no-selection/components/ImportRecordsNoSelectionRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/no-selection/components/ImportRecordsNoSelectionRecordCommand.tsx
new file mode 100644
index 0000000000..0433b0fffc
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/no-selection/components/ImportRecordsNoSelectionRecordCommand.tsx
@@ -0,0 +1,23 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useOpenObjectRecordsSpreadsheetImportDialog } from '@/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreadsheetImportDialog';
+import { isDefined } from 'twenty-shared/utils';
+
+export const ImportRecordsNoSelectionRecordCommand = () => {
+ const { objectMetadataItem } = useMountedEngineCommandContext();
+
+ if (!isDefined(objectMetadataItem)) {
+ throw new Error('Object metadata item is required to import records');
+ }
+
+ const { openObjectRecordsSpreadsheetImportDialog } =
+ useOpenObjectRecordsSpreadsheetImportDialog(
+ objectMetadataItem.nameSingular,
+ );
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/no-selection/components/SeeDeletedRecordsNoSelectionRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/no-selection/components/SeeDeletedRecordsNoSelectionRecordCommand.tsx
new file mode 100644
index 0000000000..2f2a5aacc6
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/no-selection/components/SeeDeletedRecordsNoSelectionRecordCommand.tsx
@@ -0,0 +1,31 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
+import { isDefined } from 'twenty-shared/utils';
+
+export const SeeDeletedRecordsNoSelectionRecordCommand = () => {
+ const { objectMetadataItem, recordIndexId } =
+ useMountedEngineCommandContext();
+
+ if (!isDefined(objectMetadataItem) || !isDefined(recordIndexId)) {
+ throw new Error(
+ 'Object metadata item and record index ID are required to see deleted records',
+ );
+ }
+
+ const { handleToggleTrashColumnFilter, toggleSoftDeleteFilterState } =
+ useHandleToggleTrashColumnFilter({
+ objectNameSingular: objectMetadataItem.nameSingular,
+ viewBarId: recordIndexId,
+ recordFiltersInstanceId: recordIndexId,
+ });
+
+ return (
+ {
+ handleToggleTrashColumnFilter();
+ toggleSoftDeleteFilterState(true);
+ }}
+ />
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/AddToFavoritesSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/AddToFavoritesSingleRecordCommand.tsx
new file mode 100644
index 0000000000..eb4c38c27a
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/AddToFavoritesSingleRecordCommand.tsx
@@ -0,0 +1,27 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useCreateNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useCreateNavigationMenuItem';
+import { isDefined } from 'twenty-shared/utils';
+
+export const AddToFavoritesSingleRecordCommand = () => {
+ const { objectMetadataItem, selectedRecords } =
+ useMountedEngineCommandContext();
+
+ const selectedRecord = selectedRecords[0];
+
+ if (!isDefined(objectMetadataItem)) {
+ throw new Error('Object metadata item is required to add to favorites');
+ }
+
+ const { createNavigationMenuItem } = useCreateNavigationMenuItem();
+
+ const handleExecute = () => {
+ if (!isDefined(selectedRecord)) {
+ return;
+ }
+
+ createNavigationMenuItem(selectedRecord, objectMetadataItem.nameSingular);
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/DeleteSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/DeleteSingleRecordCommand.tsx
new file mode 100644
index 0000000000..75d1969fa4
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/DeleteSingleRecordCommand.tsx
@@ -0,0 +1,59 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
+import { useRemoveNavigationMenuItemByTargetRecordId } from '@/navigation-menu-item/common/hooks/useRemoveNavigationMenuItemByTargetRecordId';
+import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
+import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
+import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
+import { isDefined } from 'twenty-shared/utils';
+
+export const DeleteSingleRecordCommand = () => {
+ const { recordIndexId, objectMetadataItem, selectedRecords } =
+ useMountedEngineCommandContext();
+
+ const recordId = selectedRecords[0]?.id;
+
+ if (
+ !isDefined(recordId) ||
+ !isDefined(recordIndexId) ||
+ !isDefined(objectMetadataItem)
+ ) {
+ throw new Error(
+ 'Record ID, record index ID, and object metadata are required to delete single record',
+ );
+ }
+
+ const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
+
+ const { removeSelectedRecordsFromRecordBoard } =
+ useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
+
+ const { deleteOneRecord } = useDeleteOneRecord({
+ objectNameSingular: objectMetadataItem.nameSingular,
+ });
+
+ const { navigationMenuItems, workspaceNavigationMenuItems } =
+ useNavigationMenuItemsData();
+
+ const { removeNavigationMenuItemsByTargetRecordIds } =
+ useRemoveNavigationMenuItemByTargetRecordId();
+
+ const handleExecute = async () => {
+ removeSelectedRecordsFromRecordBoard();
+
+ resetTableRowSelection();
+
+ const foundNavigationMenuItem = [
+ ...navigationMenuItems,
+ ...workspaceNavigationMenuItems,
+ ].find((item) => item.targetRecordId === recordId);
+
+ if (isDefined(foundNavigationMenuItem)) {
+ removeNavigationMenuItemsByTargetRecordIds([recordId]);
+ }
+
+ await deleteOneRecord(recordId);
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/DestroySingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/DestroySingleRecordCommand.tsx
new file mode 100644
index 0000000000..890c817bdd
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/DestroySingleRecordCommand.tsx
@@ -0,0 +1,56 @@
+import { HeadlessConfirmationModalEngineCommandEffect } from '@/command-menu-item/engine-command/components/HeadlessConfirmationModalEngineCommandEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
+import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
+import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
+import { t } from '@lingui/core/macro';
+import { AppPath } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
+import { useNavigateApp } from '~/hooks/useNavigateApp';
+
+export const DestroySingleRecordCommand = () => {
+ const { recordIndexId, objectMetadataItem, selectedRecords } =
+ useMountedEngineCommandContext();
+
+ const recordId = selectedRecords[0]?.id;
+
+ if (
+ !isDefined(recordId) ||
+ !isDefined(recordIndexId) ||
+ !isDefined(objectMetadataItem)
+ ) {
+ throw new Error(
+ 'Record ID, record index ID, and object metadata are required to destroy single record',
+ );
+ }
+
+ const navigateApp = useNavigateApp();
+
+ const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
+
+ const { removeSelectedRecordsFromRecordBoard } =
+ useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
+
+ const { destroyOneRecord } = useDestroyOneRecord({
+ objectNameSingular: objectMetadataItem.nameSingular,
+ });
+
+ const handleExecute = async () => {
+ removeSelectedRecordsFromRecordBoard();
+ resetTableRowSelection();
+
+ await destroyOneRecord(recordId);
+ navigateApp(AppPath.RecordIndexPage, {
+ objectNamePlural: objectMetadataItem.namePlural,
+ });
+ };
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/ExportNoteSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/ExportNoteSingleRecordCommand.tsx
new file mode 100644
index 0000000000..4da3425668
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/ExportNoteSingleRecordCommand.tsx
@@ -0,0 +1,44 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { isDefined } from 'twenty-shared/utils';
+
+export const ExportNoteSingleRecordCommand = () => {
+ const { selectedRecords } = useMountedEngineCommandContext();
+ const selectedRecord = selectedRecords[0];
+
+ const recordId = selectedRecord?.id;
+
+ if (!isDefined(recordId) || !isDefined(selectedRecord)) {
+ throw new Error(
+ 'Record ID and selected record are required to export note to PDF',
+ );
+ }
+
+ const filename = `${(selectedRecord.title || 'Untitled Note').replace(/[<>:"/\\|?*]/g, '-')}`;
+
+ const handleExecute = async () => {
+ const initialBody = selectedRecord.bodyV2?.blocknote;
+
+ let parsedBody = [];
+
+ // TODO: Remove this once we have removed the old rich text
+ try {
+ parsedBody = JSON.parse(initialBody);
+ } catch {
+ // oxlint-disable-next-line no-console
+ console.warn(
+ `Failed to parse body for record ${recordId}, for rich text version 'v2'`,
+ );
+ // oxlint-disable-next-line no-console
+ console.warn(initialBody);
+ }
+
+ const { exportBlockNoteEditorToPdf } = await import(
+ '@/command-menu-item/record/single-record/utils/exportBlockNoteEditorToPdf'
+ );
+
+ await exportBlockNoteEditorToPdf(parsedBody, filename);
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/ExportSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/ExportSingleRecordCommand.tsx
new file mode 100644
index 0000000000..c1a5968674
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/ExportSingleRecordCommand.tsx
@@ -0,0 +1,30 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useExportSingleRecord } from '@/object-record/record-show/hooks/useExportSingleRecord';
+import { isDefined } from 'twenty-shared/utils';
+
+export const ExportSingleRecordCommand = () => {
+ const { objectMetadataItem, currentViewId, selectedRecords } =
+ useMountedEngineCommandContext();
+
+ const recordId = selectedRecords[0]?.id;
+
+ if (
+ !isDefined(currentViewId) ||
+ !isDefined(recordId) ||
+ !isDefined(objectMetadataItem)
+ ) {
+ throw new Error(
+ 'Current view ID, record ID, and object metadata are required to export single record',
+ );
+ }
+
+ const filename = `${objectMetadataItem.nameSingular}.csv`;
+ const { download } = useExportSingleRecord({
+ filename,
+ objectMetadataItem,
+ recordId,
+ });
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/NavigateToNextRecordSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/NavigateToNextRecordSingleRecordCommand.tsx
new file mode 100644
index 0000000000..cee4b730c4
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/NavigateToNextRecordSingleRecordCommand.tsx
@@ -0,0 +1,24 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
+import { isDefined } from 'twenty-shared/utils';
+
+export const NavigateToNextRecordSingleRecordCommand = () => {
+ const { objectMetadataItem, selectedRecords } =
+ useMountedEngineCommandContext();
+
+ const recordId = selectedRecords[0]?.id;
+
+ if (!isDefined(recordId) || !isDefined(objectMetadataItem)) {
+ throw new Error(
+ 'Record ID and object metadata are required to navigate to next record',
+ );
+ }
+
+ const { navigateToNextRecord } = useRecordShowPagePagination(
+ objectMetadataItem.nameSingular,
+ recordId,
+ );
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/NavigateToPreviousRecordSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/NavigateToPreviousRecordSingleRecordCommand.tsx
new file mode 100644
index 0000000000..27e6684576
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/NavigateToPreviousRecordSingleRecordCommand.tsx
@@ -0,0 +1,26 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
+import { isDefined } from 'twenty-shared/utils';
+
+export const NavigateToPreviousRecordSingleRecordCommand = () => {
+ const { objectMetadataItem, selectedRecords } =
+ useMountedEngineCommandContext();
+
+ const recordId = selectedRecords[0]?.id;
+
+ if (!isDefined(recordId) || !isDefined(objectMetadataItem)) {
+ throw new Error(
+ 'Record ID and object metadata are required to navigate to previous record',
+ );
+ }
+
+ const { navigateToPreviousRecord } = useRecordShowPagePagination(
+ objectMetadataItem.nameSingular,
+ recordId,
+ );
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/RemoveFromFavoritesSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/RemoveFromFavoritesSingleRecordCommand.tsx
new file mode 100644
index 0000000000..15d6258939
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/RemoveFromFavoritesSingleRecordCommand.tsx
@@ -0,0 +1,42 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/common/hooks/useDeleteNavigationMenuItem';
+import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
+import { isDefined } from 'twenty-shared/utils';
+
+export const RemoveFromFavoritesSingleRecordCommand = () => {
+ const { selectedRecords, objectMetadataItem } =
+ useMountedEngineCommandContext();
+
+ const recordId = selectedRecords[0]?.id;
+
+ if (!isDefined(recordId) || !isDefined(objectMetadataItem)) {
+ throw new Error(
+ 'Record ID and object metadata are required to remove from favorites',
+ );
+ }
+
+ const { navigationMenuItems, workspaceNavigationMenuItems } =
+ useNavigationMenuItemsData();
+
+ const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
+
+ const foundNavigationMenuItem = [
+ ...navigationMenuItems,
+ ...workspaceNavigationMenuItems,
+ ].find(
+ (item) =>
+ item.targetRecordId === recordId &&
+ item.targetObjectMetadataId === objectMetadataItem.id,
+ );
+
+ const handleExecute = () => {
+ if (!isDefined(foundNavigationMenuItem)) {
+ return;
+ }
+
+ deleteNavigationMenuItem(foundNavigationMenuItem.id);
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/RestoreSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/RestoreSingleRecordCommand.tsx
new file mode 100644
index 0000000000..b22e03c1bd
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/components/RestoreSingleRecordCommand.tsx
@@ -0,0 +1,51 @@
+import { HeadlessConfirmationModalEngineCommandEffect } from '@/command-menu-item/engine-command/components/HeadlessConfirmationModalEngineCommandEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
+import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
+import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
+import { t } from '@lingui/core/macro';
+import { isDefined } from 'twenty-shared/utils';
+
+export const RestoreSingleRecordCommand = () => {
+ const { recordIndexId, objectMetadataItem, selectedRecords } =
+ useMountedEngineCommandContext();
+
+ const recordId = selectedRecords[0]?.id;
+
+ if (
+ !isDefined(recordId) ||
+ !isDefined(recordIndexId) ||
+ !isDefined(objectMetadataItem)
+ ) {
+ throw new Error(
+ 'Record ID, record index ID, and object metadata are required to restore single record',
+ );
+ }
+
+ const { resetTableRowSelection } = useResetTableRowSelection(recordIndexId);
+ const { removeSelectedRecordsFromRecordBoard } =
+ useRemoveSelectedRecordsFromRecordBoard(recordIndexId);
+
+ const { restoreManyRecords } = useRestoreManyRecords({
+ objectNameSingular: objectMetadataItem.nameSingular,
+ });
+
+ const handleExecute = async () => {
+ removeSelectedRecordsFromRecordBoard();
+ resetTableRowSelection();
+
+ await restoreManyRecords({
+ idsToRestore: [recordId],
+ });
+ };
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/dashboard/components/CancelDashboardSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/dashboard/components/CancelDashboardSingleRecordCommand.tsx
new file mode 100644
index 0000000000..e341b1572e
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/dashboard/components/CancelDashboardSingleRecordCommand.tsx
@@ -0,0 +1,34 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useResetDraftPageLayoutToPersistedPageLayout } from '@/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout';
+import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
+import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
+import { isDefined } from 'twenty-shared/utils';
+
+export const CancelDashboardSingleRecordCommand = () => {
+ const { selectedRecords } = useMountedEngineCommandContext();
+ const selectedRecord = selectedRecords[0];
+
+ if (!isDefined(selectedRecord)) {
+ throw new Error('Selected record is required to cancel dashboard');
+ }
+
+ const pageLayoutId = selectedRecord.pageLayoutId;
+
+ const { closeSidePanelMenu } = useSidePanelMenu();
+
+ const { setIsPageLayoutInEditMode } =
+ useSetIsPageLayoutInEditMode(pageLayoutId);
+
+ const { resetDraftPageLayoutToPersistedPageLayout } =
+ useResetDraftPageLayoutToPersistedPageLayout(pageLayoutId);
+
+ const handleExecute = () => {
+ closeSidePanelMenu();
+
+ resetDraftPageLayoutToPersistedPageLayout();
+ setIsPageLayoutInEditMode(false);
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/dashboard/components/DuplicateDashboardSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/dashboard/components/DuplicateDashboardSingleRecordCommand.tsx
new file mode 100644
index 0000000000..9279a8e28c
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/dashboard/components/DuplicateDashboardSingleRecordCommand.tsx
@@ -0,0 +1,44 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useDuplicateDashboard } from '@/dashboards/hooks/useDuplicateDashboard';
+import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
+import { useLingui } from '@lingui/react/macro';
+import { isNonEmptyString } from '@sniptt/guards';
+import { AppPath, CoreObjectNameSingular } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
+import { useNavigateApp } from '~/hooks/useNavigateApp';
+
+export const DuplicateDashboardSingleRecordCommand = () => {
+ const { selectedRecords } = useMountedEngineCommandContext();
+
+ const recordId = selectedRecords[0]?.id;
+ const { duplicateDashboard } = useDuplicateDashboard();
+ const navigate = useNavigateApp();
+ const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
+ const { t } = useLingui();
+
+ if (!isDefined(recordId)) {
+ throw new Error('Record ID is required to duplicate dashboard');
+ }
+
+ const handleExecute = async () => {
+ const result = await duplicateDashboard(recordId);
+
+ if (isDefined(result) && isNonEmptyString(result.id)) {
+ enqueueSuccessSnackBar({
+ message: t`Dashboard duplicated successfully`,
+ });
+
+ navigate(AppPath.RecordShowPage, {
+ objectNameSingular: CoreObjectNameSingular.Dashboard,
+ objectRecordId: result.id,
+ });
+ } else {
+ enqueueErrorSnackBar({
+ message: t`Failed to duplicate dashboard`,
+ });
+ }
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/dashboard/components/EditDashboardSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/dashboard/components/EditDashboardSingleRecordCommand.tsx
new file mode 100644
index 0000000000..5e374aca65
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/dashboard/components/EditDashboardSingleRecordCommand.tsx
@@ -0,0 +1,28 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
+import { isDefined } from 'twenty-shared/utils';
+import { useResetLocationHash } from 'twenty-ui/utilities';
+
+export const EditDashboardSingleRecordCommand = () => {
+ const { selectedRecords } = useMountedEngineCommandContext();
+ const selectedRecord = selectedRecords[0];
+
+ if (!isDefined(selectedRecord)) {
+ throw new Error('Selected record is required to edit dashboard');
+ }
+
+ const pageLayoutId = selectedRecord.pageLayoutId;
+
+ const { setIsPageLayoutInEditMode } =
+ useSetIsPageLayoutInEditMode(pageLayoutId);
+
+ const { resetLocationHash } = useResetLocationHash();
+
+ const handleExecute = () => {
+ setIsPageLayoutInEditMode(true);
+ resetLocationHash();
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/dashboard/components/SaveDashboardSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/dashboard/components/SaveDashboardSingleRecordCommand.tsx
new file mode 100644
index 0000000000..fb26507faa
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/dashboard/components/SaveDashboardSingleRecordCommand.tsx
@@ -0,0 +1,35 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout';
+import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
+import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
+import { isDefined } from 'twenty-shared/utils';
+
+export const SaveDashboardSingleRecordCommand = () => {
+ const { selectedRecords } = useMountedEngineCommandContext();
+ const selectedRecord = selectedRecords[0];
+
+ if (!isDefined(selectedRecord)) {
+ throw new Error('Selected record is required to save dashboard');
+ }
+
+ const pageLayoutId = selectedRecord.pageLayoutId;
+
+ const { savePageLayout } = useSavePageLayout(pageLayoutId);
+
+ const { setIsPageLayoutInEditMode } =
+ useSetIsPageLayoutInEditMode(pageLayoutId);
+
+ const { closeSidePanelMenu } = useSidePanelMenu();
+
+ const handleExecute = async () => {
+ const result = await savePageLayout();
+
+ if (result.status === 'successful') {
+ closeSidePanelMenu();
+ setIsPageLayoutInEditMode(false);
+ }
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/record-page-layout/components/CancelRecordPageLayoutSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/record-page-layout/components/CancelRecordPageLayoutSingleRecordCommand.tsx
new file mode 100644
index 0000000000..60655c69ee
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/record-page-layout/components/CancelRecordPageLayoutSingleRecordCommand.tsx
@@ -0,0 +1,38 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
+import { useResetDraftPageLayoutToPersistedPageLayout } from '@/page-layout/hooks/useResetDraftPageLayoutToPersistedPageLayout';
+import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
+import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
+import { isDefined } from 'twenty-shared/utils';
+
+export const CancelRecordPageLayoutSingleRecordCommand = () => {
+ const { objectMetadataItem } = useMountedEngineCommandContext();
+
+ if (!isDefined(objectMetadataItem)) {
+ throw new Error(
+ 'Object metadata item is required to cancel record page layout',
+ );
+ }
+
+ const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
+ targetObjectNameSingular: objectMetadataItem.nameSingular,
+ });
+
+ const { closeSidePanelMenu } = useSidePanelMenu();
+
+ const { setIsPageLayoutInEditMode } =
+ useSetIsPageLayoutInEditMode(pageLayoutId);
+
+ const { resetDraftPageLayoutToPersistedPageLayout } =
+ useResetDraftPageLayoutToPersistedPageLayout(pageLayoutId);
+
+ const handleExecute = () => {
+ closeSidePanelMenu();
+
+ resetDraftPageLayoutToPersistedPageLayout();
+ setIsPageLayoutInEditMode(false);
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand.tsx
new file mode 100644
index 0000000000..05032e916b
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/record-page-layout/components/EditRecordPageLayoutSingleRecordCommand.tsx
@@ -0,0 +1,32 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
+import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
+import { isDefined } from 'twenty-shared/utils';
+import { useResetLocationHash } from 'twenty-ui/utilities';
+
+export const EditRecordPageLayoutSingleRecordCommand = () => {
+ const { objectMetadataItem } = useMountedEngineCommandContext();
+
+ if (!isDefined(objectMetadataItem)) {
+ throw new Error(
+ 'Object metadata item is required to edit record page layout',
+ );
+ }
+
+ const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
+ targetObjectNameSingular: objectMetadataItem.nameSingular,
+ });
+
+ const { setIsPageLayoutInEditMode } =
+ useSetIsPageLayoutInEditMode(pageLayoutId);
+
+ const { resetLocationHash } = useResetLocationHash();
+
+ const handleExecute = () => {
+ setIsPageLayoutInEditMode(true);
+ resetLocationHash();
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/record-page-layout/components/SaveRecordPageLayoutSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/record-page-layout/components/SaveRecordPageLayoutSingleRecordCommand.tsx
new file mode 100644
index 0000000000..e121fff299
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/record-page-layout/components/SaveRecordPageLayoutSingleRecordCommand.tsx
@@ -0,0 +1,44 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useRecordPageLayoutIdFromRecordStoreOrThrow } from '@/page-layout/hooks/useRecordPageLayoutIdFromRecordStoreOrThrow';
+import { useSaveFieldsWidgetGroups } from '@/page-layout/hooks/useSaveFieldsWidgetGroups';
+import { useSavePageLayout } from '@/page-layout/hooks/useSavePageLayout';
+import { useSetIsPageLayoutInEditMode } from '@/page-layout/hooks/useSetIsPageLayoutInEditMode';
+import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
+import { isDefined } from 'twenty-shared/utils';
+
+export const SaveRecordPageLayoutSingleRecordCommand = () => {
+ const { objectMetadataItem } = useMountedEngineCommandContext();
+
+ if (!isDefined(objectMetadataItem)) {
+ throw new Error(
+ 'Object metadata item is required to save record page layout',
+ );
+ }
+
+ const { pageLayoutId } = useRecordPageLayoutIdFromRecordStoreOrThrow({
+ targetObjectNameSingular: objectMetadataItem.nameSingular,
+ });
+
+ const { savePageLayout } = useSavePageLayout(pageLayoutId);
+
+ const { saveFieldsWidgetGroups } = useSaveFieldsWidgetGroups();
+
+ const { setIsPageLayoutInEditMode } =
+ useSetIsPageLayoutInEditMode(pageLayoutId);
+
+ const { closeSidePanelMenu } = useSidePanelMenu();
+
+ const handleExecute = async () => {
+ const result = await savePageLayout();
+
+ if (result.status === 'successful') {
+ await saveFieldsWidgetGroups(pageLayoutId);
+
+ closeSidePanelMenu();
+ setIsPageLayoutInEditMode(false);
+ }
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-runs/components/SeeVersionWorkflowRunSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-runs/components/SeeVersionWorkflowRunSingleRecordCommand.tsx
new file mode 100644
index 0000000000..75ce2dcd88
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-runs/components/SeeVersionWorkflowRunSingleRecordCommand.tsx
@@ -0,0 +1,26 @@
+import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { AppPath, CoreObjectNameSingular } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
+
+export const SeeVersionWorkflowRunSingleRecordCommand = () => {
+ const { selectedRecords } = useMountedEngineCommandContext();
+ const selectedRecord = selectedRecords[0];
+
+ if (
+ !isDefined(selectedRecord) ||
+ !isDefined(selectedRecord?.workflowVersion?.id)
+ ) {
+ throw new Error('Selected record is required to see version workflow run');
+ }
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-runs/components/SeeWorkflowWorkflowRunSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-runs/components/SeeWorkflowWorkflowRunSingleRecordCommand.tsx
new file mode 100644
index 0000000000..38c8536e04
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-runs/components/SeeWorkflowWorkflowRunSingleRecordCommand.tsx
@@ -0,0 +1,23 @@
+import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { CoreObjectNameSingular, AppPath } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
+
+export const SeeWorkflowWorkflowRunSingleRecordCommand = () => {
+ const { selectedRecords } = useMountedEngineCommandContext();
+ const selectedRecord = selectedRecords[0];
+
+ if (!isDefined(selectedRecord) || !isDefined(selectedRecord?.workflow?.id)) {
+ return null;
+ }
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-runs/components/StopWorkflowRunSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-runs/components/StopWorkflowRunSingleRecordCommand.tsx
new file mode 100644
index 0000000000..e6c07295a8
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-runs/components/StopWorkflowRunSingleRecordCommand.tsx
@@ -0,0 +1,37 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
+import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
+import { useStopWorkflowRun } from '@/workflow/hooks/useStopWorkflowRun';
+import { CoreObjectNameSingular } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
+
+export const StopWorkflowRunSingleRecordCommand = () => {
+ const { targetedRecordsRule, graphqlFilter } =
+ useMountedEngineCommandContext();
+
+ const { fetchAllRecords: fetchAllRecordIds } = useLazyFetchAllRecords({
+ objectNameSingular: CoreObjectNameSingular.WorkflowRun,
+ filter: isDefined(graphqlFilter) ? graphqlFilter : undefined,
+ limit: DEFAULT_QUERY_PAGE_SIZE,
+ recordGqlFields: { id: true },
+ });
+
+ const { stopWorkflowRun } = useStopWorkflowRun();
+
+ const handleExecute = async () => {
+ if (targetedRecordsRule.mode === 'selection') {
+ for (const selectedRecordId of targetedRecordsRule.selectedRecordIds) {
+ await stopWorkflowRun(selectedRecordId);
+ }
+ } else {
+ const records = await fetchAllRecordIds();
+
+ for (const record of records) {
+ await stopWorkflowRun(record.id);
+ }
+ }
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-versions/components/SeeRunsWorkflowVersionSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-versions/components/SeeRunsWorkflowVersionSingleRecordCommand.tsx
new file mode 100644
index 0000000000..7174f5b55e
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-versions/components/SeeRunsWorkflowVersionSingleRecordCommand.tsx
@@ -0,0 +1,59 @@
+import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
+import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
+import { AppPath, ViewFilterOperand } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
+
+const SeeRunsWorkflowVersionSingleRecordCommandContent = ({
+ workflowId,
+ recordId,
+}: {
+ workflowId: string;
+ recordId: string;
+}) => {
+ const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
+
+ return (
+
+ );
+};
+
+export const SeeRunsWorkflowVersionSingleRecordCommand = () => {
+ const { selectedRecords } = useMountedEngineCommandContext();
+
+ const recordId = selectedRecords[0]?.id;
+ const selectedRecord = selectedRecords[0];
+
+ const workflowId = selectedRecord?.workflow?.id;
+
+ if (!isDefined(recordId) || !isDefined(workflowId)) {
+ throw new Error(
+ 'Record ID and workflow ID are required to see runs workflow version',
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-versions/components/SeeVersionsWorkflowVersionSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-versions/components/SeeVersionsWorkflowVersionSingleRecordCommand.tsx
new file mode 100644
index 0000000000..83d66ac0ae
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-versions/components/SeeVersionsWorkflowVersionSingleRecordCommand.tsx
@@ -0,0 +1,47 @@
+import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
+import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
+import { AppPath, ViewFilterOperand } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
+
+const SeeVersionsWorkflowVersionSingleRecordCommandContent = ({
+ workflowId,
+}: {
+ workflowId: string;
+}) => {
+ const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
+
+ return (
+
+ );
+};
+
+export const SeeVersionsWorkflowVersionSingleRecordCommand = () => {
+ const { selectedRecords } = useMountedEngineCommandContext();
+ const selectedRecord = selectedRecords[0];
+
+ if (!isDefined(selectedRecord) || !isDefined(selectedRecord.workflowId)) {
+ throw new Error(
+ 'Selected record and workflow ID are required to see versions workflow version',
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-versions/components/SeeWorkflowWorkflowVersionSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-versions/components/SeeWorkflowWorkflowVersionSingleRecordCommand.tsx
new file mode 100644
index 0000000000..bc730b144e
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-versions/components/SeeWorkflowWorkflowVersionSingleRecordCommand.tsx
@@ -0,0 +1,25 @@
+import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { AppPath, CoreObjectNameSingular } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
+
+export const SeeWorkflowWorkflowVersionSingleRecordCommand = () => {
+ const { selectedRecords } = useMountedEngineCommandContext();
+ const selectedRecord = selectedRecords[0];
+
+ if (!isDefined(selectedRecord) || !isDefined(selectedRecord?.workflow?.id)) {
+ throw new Error(
+ 'Selected record and workflow ID are required to see workflow workflow version',
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-versions/components/UseAsDraftWorkflowVersionSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-versions/components/UseAsDraftWorkflowVersionSingleRecordCommand.tsx
new file mode 100644
index 0000000000..c0b93c58ea
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow-versions/components/UseAsDraftWorkflowVersionSingleRecordCommand.tsx
@@ -0,0 +1,86 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useModal } from '@/ui/layout/modal/hooks/useModal';
+import { OverrideWorkflowDraftConfirmationModal } from '@/workflow/components/OverrideWorkflowDraftConfirmationModal';
+import { OVERRIDE_WORKFLOW_DRAFT_CONFIRMATION_MODAL_ID } from '@/workflow/constants/OverrideWorkflowDraftConfirmationModalId';
+import { useCreateDraftFromWorkflowVersion } from '@/workflow/hooks/useCreateDraftFromWorkflowVersion';
+import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
+import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
+import { useState } from 'react';
+import { AppPath, CoreObjectNameSingular } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
+import { useNavigateApp } from '~/hooks/useNavigateApp';
+
+const UseAsDraftWorkflowVersionSingleRecordCommandContent = ({
+ workflowId,
+ workflowVersionId,
+}: {
+ workflowId: string;
+ workflowVersionId: string;
+}) => {
+ const { openModal } = useModal();
+ const workflow = useWorkflowWithCurrentVersion(workflowId);
+ const { createDraftFromWorkflowVersion } =
+ useCreateDraftFromWorkflowVersion();
+ const navigate = useNavigateApp();
+ const [hasNavigated, setHasNavigated] = useState(false);
+
+ const hasAlreadyDraftVersion =
+ workflow?.versions.some((version) => version.status === 'DRAFT') || false;
+
+ const handleExecute = () => {
+ if (!isDefined(workflow) || hasNavigated) {
+ return;
+ }
+
+ if (hasAlreadyDraftVersion) {
+ openModal(OVERRIDE_WORKFLOW_DRAFT_CONFIRMATION_MODAL_ID);
+ } else {
+ const executeCommandWithoutWaiting = async () => {
+ await createDraftFromWorkflowVersion({
+ workflowId,
+ workflowVersionIdToCopy: workflowVersionId,
+ });
+
+ navigate(AppPath.RecordShowPage, {
+ objectNameSingular: CoreObjectNameSingular.Workflow,
+ objectRecordId: workflowId,
+ });
+
+ setHasNavigated(true);
+ };
+
+ executeCommandWithoutWaiting();
+ }
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export const UseAsDraftWorkflowVersionSingleRecordCommand = () => {
+ const { selectedRecords } = useMountedEngineCommandContext();
+
+ const recordId = selectedRecords[0]?.id;
+ const workflowVersion = useWorkflowVersion(recordId ?? '');
+
+ if (!recordId || !isDefined(workflowVersion?.workflow?.id)) {
+ throw new Error(
+ 'Record ID and workflow ID are required to use as draft workflow version',
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/ActivateWorkflowSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/ActivateWorkflowSingleRecordCommand.tsx
new file mode 100644
index 0000000000..9d4a65121c
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/ActivateWorkflowSingleRecordCommand.tsx
@@ -0,0 +1,32 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion';
+import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
+import { isDefined } from 'twenty-shared/utils';
+
+export const ActivateWorkflowSingleRecordCommand = () => {
+ const { selectedRecords } = useMountedEngineCommandContext();
+ const recordId = selectedRecords[0]?.id;
+
+ const { activateWorkflowVersion } = useActivateWorkflowVersion();
+ const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
+ recordId ?? '',
+ );
+
+ if (!isDefined(recordId)) {
+ throw new Error('Record ID is required to activate workflow');
+ }
+
+ const handleExecute = () => {
+ if (!isDefined(workflowWithCurrentVersion)) {
+ return;
+ }
+
+ activateWorkflowVersion({
+ workflowVersionId: workflowWithCurrentVersion.currentVersion.id,
+ workflowId: workflowWithCurrentVersion.id,
+ });
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/AddNodeWorkflowSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/AddNodeWorkflowSingleRecordCommand.tsx
new file mode 100644
index 0000000000..9788bde43c
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/AddNodeWorkflowSingleRecordCommand.tsx
@@ -0,0 +1,30 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useSidePanelWorkflowNavigation } from '@/side-panel/pages/workflow/hooks/useSidePanelWorkflowNavigation';
+import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
+import { isDefined } from 'twenty-shared/utils';
+
+export const AddNodeWorkflowSingleRecordCommand = () => {
+ const { selectedRecords } = useMountedEngineCommandContext();
+
+ const recordId = selectedRecords[0]?.id;
+ const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
+ recordId ?? '',
+ );
+ const { openWorkflowCreateStepInSidePanel } =
+ useSidePanelWorkflowNavigation();
+
+ if (!isDefined(recordId)) {
+ throw new Error('Record ID is required to add node workflow');
+ }
+
+ const handleExecute = () => {
+ if (!isDefined(workflowWithCurrentVersion)) {
+ return;
+ }
+
+ openWorkflowCreateStepInSidePanel(workflowWithCurrentVersion.id);
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/DeactivateWorkflowSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/DeactivateWorkflowSingleRecordCommand.tsx
new file mode 100644
index 0000000000..b3c8072a0f
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/DeactivateWorkflowSingleRecordCommand.tsx
@@ -0,0 +1,31 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useDeactivateWorkflowVersion } from '@/workflow/hooks/useDeactivateWorkflowVersion';
+import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
+import { isDefined } from 'twenty-shared/utils';
+
+export const DeactivateWorkflowSingleRecordCommand = () => {
+ const { selectedRecords } = useMountedEngineCommandContext();
+
+ const recordId = selectedRecords[0]?.id;
+ const { deactivateWorkflowVersion } = useDeactivateWorkflowVersion();
+ const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
+ recordId ?? '',
+ );
+
+ if (!isDefined(recordId)) {
+ throw new Error('Record ID is required to deactivate workflow');
+ }
+
+ const handleExecute = () => {
+ if (!isDefined(workflowWithCurrentVersion)) {
+ return;
+ }
+
+ deactivateWorkflowVersion({
+ workflowVersionId: workflowWithCurrentVersion.currentVersion.id,
+ });
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/DiscardDraftWorkflowSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/DiscardDraftWorkflowSingleRecordCommand.tsx
new file mode 100644
index 0000000000..d94ebde5ac
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/DiscardDraftWorkflowSingleRecordCommand.tsx
@@ -0,0 +1,31 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useDeleteOneWorkflowVersion } from '@/workflow/hooks/useDeleteOneWorkflowVersion';
+import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
+import { isDefined } from 'twenty-shared/utils';
+
+export const DiscardDraftWorkflowSingleRecordCommand = () => {
+ const { selectedRecords } = useMountedEngineCommandContext();
+
+ const recordId = selectedRecords[0]?.id;
+ const { deleteOneWorkflowVersion } = useDeleteOneWorkflowVersion();
+ const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
+ recordId ?? '',
+ );
+
+ if (!isDefined(recordId)) {
+ throw new Error('Record ID is required to discard draft workflow');
+ }
+
+ const handleExecute = () => {
+ if (!isDefined(workflowWithCurrentVersion)) {
+ return;
+ }
+
+ deleteOneWorkflowVersion({
+ workflowVersionId: workflowWithCurrentVersion.currentVersion.id,
+ });
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/DuplicateWorkflowSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/DuplicateWorkflowSingleRecordCommand.tsx
new file mode 100644
index 0000000000..437c2d7000
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/DuplicateWorkflowSingleRecordCommand.tsx
@@ -0,0 +1,55 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
+import { useDuplicateWorkflow } from '@/workflow/hooks/useDuplicateWorkflow';
+import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
+import { useLingui } from '@lingui/react/macro';
+import { isNonEmptyString } from '@sniptt/guards';
+import { AppPath, CoreObjectNameSingular } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
+import { useNavigateApp } from '~/hooks/useNavigateApp';
+
+export const DuplicateWorkflowSingleRecordCommand = () => {
+ const { selectedRecords } = useMountedEngineCommandContext();
+
+ const recordId = selectedRecords[0]?.id;
+ const workflow = useWorkflowWithCurrentVersion(recordId ?? '');
+ const { duplicateWorkflow } = useDuplicateWorkflow();
+ const navigate = useNavigateApp();
+ const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
+ const { t } = useLingui();
+
+ if (!isDefined(recordId)) {
+ throw new Error('Record ID is required to duplicate workflow');
+ }
+
+ const handleExecute = async () => {
+ if (!isDefined(workflow) || !isDefined(workflow.currentVersion)) {
+ return;
+ }
+
+ const result = await duplicateWorkflow({
+ workflowIdToDuplicate: workflow.id,
+ workflowVersionIdToCopy: workflow.currentVersion.id,
+ });
+
+ if (isDefined(result) && isNonEmptyString(result.workflowId)) {
+ enqueueSuccessSnackBar({
+ message: t`Workflow duplicated successfully`,
+ });
+
+ navigate(AppPath.RecordShowPage, {
+ objectNameSingular: CoreObjectNameSingular.Workflow,
+ objectRecordId: result.workflowId,
+ });
+ } else {
+ enqueueErrorSnackBar({
+ message: t`Failed to duplicate workflow`,
+ });
+ }
+ };
+
+ return isDefined(workflow) ? (
+
+ ) : null;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/SeeActiveVersionWorkflowSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/SeeActiveVersionWorkflowSingleRecordCommand.tsx
new file mode 100644
index 0000000000..16cec564c0
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/SeeActiveVersionWorkflowSingleRecordCommand.tsx
@@ -0,0 +1,33 @@
+import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useActiveWorkflowVersion } from '@/workflow/hooks/useActiveWorkflowVersion';
+import { AppPath, CoreObjectNameSingular } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
+
+export const SeeActiveVersionWorkflowSingleRecordCommand = () => {
+ const { selectedRecords } = useMountedEngineCommandContext();
+
+ const recordId = selectedRecords[0]?.id;
+
+ const { workflowVersion, loading } = useActiveWorkflowVersion({
+ workflowId: recordId ?? '',
+ });
+
+ if (!isDefined(recordId)) {
+ throw new Error('Record ID is required to see active version workflow');
+ }
+
+ if (loading) {
+ return null;
+ }
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/SeeRunsWorkflowSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/SeeRunsWorkflowSingleRecordCommand.tsx
new file mode 100644
index 0000000000..c02f76c22b
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/SeeRunsWorkflowSingleRecordCommand.tsx
@@ -0,0 +1,35 @@
+import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
+import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
+import { AppPath, ViewFilterOperand } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
+
+export const SeeRunsWorkflowSingleRecordCommand = () => {
+ const { selectedRecords } = useMountedEngineCommandContext();
+
+ const recordId = selectedRecords[0]?.id;
+ const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
+ recordId ?? '',
+ );
+
+ if (!isDefined(recordId)) {
+ throw new Error('Record ID is required to see runs workflow');
+ }
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/SeeVersionsWorkflowSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/SeeVersionsWorkflowSingleRecordCommand.tsx
new file mode 100644
index 0000000000..b77b3723df
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/SeeVersionsWorkflowSingleRecordCommand.tsx
@@ -0,0 +1,35 @@
+import { HeadlessNavigateEngineCommand } from '@/command-menu-item/engine-command/components/HeadlessNavigateEngineCommand';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
+import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
+import { AppPath, ViewFilterOperand } from 'twenty-shared/types';
+import { isDefined } from 'twenty-shared/utils';
+
+export const SeeVersionsWorkflowSingleRecordCommand = () => {
+ const { selectedRecords } = useMountedEngineCommandContext();
+
+ const recordId = selectedRecords[0]?.id;
+ const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
+ recordId ?? '',
+ );
+
+ if (!isDefined(recordId)) {
+ throw new Error('Record ID is required to see versions workflow');
+ }
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/TestWorkflowSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/TestWorkflowSingleRecordCommand.tsx
new file mode 100644
index 0000000000..3b2de6bb74
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/TestWorkflowSingleRecordCommand.tsx
@@ -0,0 +1,32 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
+import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
+import { isDefined } from 'twenty-shared/utils';
+
+export const TestWorkflowSingleRecordCommand = () => {
+ const { selectedRecords } = useMountedEngineCommandContext();
+
+ const recordId = selectedRecords[0]?.id;
+ const { runWorkflowVersion } = useRunWorkflowVersion();
+ const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
+ recordId ?? '',
+ );
+
+ if (!isDefined(recordId)) {
+ throw new Error('Record ID is required to test workflow');
+ }
+
+ const handleExecute = () => {
+ if (!isDefined(workflowWithCurrentVersion)) {
+ return;
+ }
+
+ runWorkflowVersion({
+ workflowVersionId: workflowWithCurrentVersion.currentVersion.id,
+ workflowId: workflowWithCurrentVersion.id,
+ });
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/TidyUpWorkflowSingleRecordCommand.tsx b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/TidyUpWorkflowSingleRecordCommand.tsx
new file mode 100644
index 0000000000..df4843e0af
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/record/single-record/workflow/components/TidyUpWorkflowSingleRecordCommand.tsx
@@ -0,0 +1,42 @@
+import { HeadlessEngineCommandWrapperEffect } from '@/command-menu-item/engine-command/components/HeadlessEngineCommandWrapperEffect';
+import { useMountedEngineCommandContext } from '@/command-menu-item/engine-command/hooks/useMountedEngineCommandContext';
+import { useGetUpdatableWorkflowVersionOrThrow } from '@/workflow/hooks/useGetUpdatableWorkflowVersionOrThrow';
+import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWorkflowVisualizerComponentInstanceId';
+import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
+import { useTidyUpWorkflowVersion } from '@/workflow/workflow-version/hooks/useTidyUpWorkflowVersion';
+import { useStore } from 'jotai';
+import { isDefined } from 'twenty-shared/utils';
+
+export const TidyUpWorkflowSingleRecordCommand = () => {
+ const store = useStore();
+ const { selectedRecords } = useMountedEngineCommandContext();
+
+ const recordId = selectedRecords[0]?.id;
+ const { tidyUpWorkflowVersion } = useTidyUpWorkflowVersion();
+ const instanceId = getWorkflowVisualizerComponentInstanceId({
+ recordId: recordId ?? '',
+ });
+ const { getUpdatableWorkflowVersion } =
+ useGetUpdatableWorkflowVersionOrThrow(instanceId);
+
+ if (!isDefined(recordId)) {
+ throw new Error('Record ID is required to tidy up workflow');
+ }
+
+ const handleExecute = async () => {
+ const workflowDiagramAtom = workflowDiagramComponentState.atomFamily({
+ instanceId,
+ });
+ const workflowDiagram = store.get(workflowDiagramAtom);
+
+ if (!isDefined(workflowDiagram)) {
+ return;
+ }
+
+ const workflowVersionId = await getUpdatableWorkflowVersion();
+
+ await tidyUpWorkflowVersion(workflowVersionId, workflowDiagram);
+ };
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/states/contexts/EngineCommandComponentInstanceContext.ts b/packages/twenty-front/src/modules/command-menu-item/engine-command/states/contexts/EngineCommandComponentInstanceContext.ts
new file mode 100644
index 0000000000..a2a1c93c95
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/states/contexts/EngineCommandComponentInstanceContext.ts
@@ -0,0 +1,4 @@
+import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
+
+export const EngineCommandComponentInstanceContext =
+ createComponentInstanceContext();
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/states/mountedEngineCommandsState.ts b/packages/twenty-front/src/modules/command-menu-item/engine-command/states/mountedEngineCommandsState.ts
new file mode 100644
index 0000000000..8036b58641
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/states/mountedEngineCommandsState.ts
@@ -0,0 +1,9 @@
+import { type MountedEngineCommandState } from '@/command-menu-item/engine-command/types/MountedEngineCommandContext';
+import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
+
+export const mountedEngineCommandsState = createAtomState<
+ Map
+>({
+ key: 'mountedEngineCommandsState',
+ defaultValue: new Map(),
+});
diff --git a/packages/twenty-front/src/modules/command-menu-item/engine-command/types/MountedEngineCommandContext.ts b/packages/twenty-front/src/modules/command-menu-item/engine-command/types/MountedEngineCommandContext.ts
new file mode 100644
index 0000000000..e12203254c
--- /dev/null
+++ b/packages/twenty-front/src/modules/command-menu-item/engine-command/types/MountedEngineCommandContext.ts
@@ -0,0 +1,19 @@
+import { type ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
+import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
+import {
+ type Nullable,
+ type RecordGqlOperationFilter,
+} from 'twenty-shared/types';
+import { type EngineComponentKey } from '~/generated-metadata/graphql';
+
+export type MountedEngineCommandState = {
+ engineComponentKey: EngineComponentKey;
+ contextStoreInstanceId: string;
+ objectMetadataItem: Nullable;
+ currentViewId: Nullable;
+ recordIndexId: Nullable;
+ targetedRecordsRule: ContextStoreTargetedRecordsRule;
+ selectedRecords: ObjectRecord[];
+ graphqlFilter: Nullable;
+};
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/contexts/CommandMenuContextProviderServerItemsContent.tsx b/packages/twenty-front/src/modules/command-menu-item/server-items/contexts/CommandMenuContextProviderServerItemsContent.tsx
index b912281e70..f1e334b7c3 100644
--- a/packages/twenty-front/src/modules/command-menu-item/server-items/contexts/CommandMenuContextProviderServerItemsContent.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/contexts/CommandMenuContextProviderServerItemsContent.tsx
@@ -2,7 +2,7 @@ import {
CommandMenuContext,
type CommandMenuContextType,
} from '@/command-menu-item/contexts/CommandMenuContext';
-import { useCommandMenuItemFrontComponentCommands } from '@/command-menu-item/server-items/hooks/useCommandMenuItemFrontComponentCommands';
+import { useCommandMenuItemsFromBackend } from '@/command-menu-item/server-items/hooks/useCommandMenuItemsFromBackend';
import { type CommandMenuContextApi } from 'twenty-shared/types';
type CommandMenuContextProviderServerItemsContentProps = {
@@ -21,8 +21,9 @@ export const CommandMenuContextProviderServerItemsContent = ({
}: CommandMenuContextProviderServerItemsContentProps & {
commandMenuContextApi: CommandMenuContextApi;
}) => {
- const commandMenuItemFrontComponentActions =
- useCommandMenuItemFrontComponentCommands(commandMenuContextApi);
+ const commandMenuItems = useCommandMenuItemsFromBackend(
+ commandMenuContextApi,
+ );
return (
{children}
diff --git a/packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useCommandMenuItemFrontComponentCommands.tsx b/packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useCommandMenuItemsFromBackend.tsx
similarity index 90%
rename from packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useCommandMenuItemFrontComponentCommands.tsx
rename to packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useCommandMenuItemsFromBackend.tsx
index 8a6fbab622..0129c273e0 100644
--- a/packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useCommandMenuItemFrontComponentCommands.tsx
+++ b/packages/twenty-front/src/modules/command-menu-item/server-items/hooks/useCommandMenuItemsFromBackend.tsx
@@ -1,14 +1,16 @@
-import { ENGINE_COMPONENT_KEY_COMPONENT_MAP } from '@/command-menu-item/constants/EngineComponentKeyComponentMap';
import { Command } from '@/command-menu-item/display/components/Command';
import { HeadlessFrontComponentCommandMenuItem } from '@/command-menu-item/display/components/HeadlessFrontComponentCommandMenuItem';
+import { useMountEngineCommand } from '@/command-menu-item/engine-command/hooks/useMountEngineCommand';
import { CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope';
import { CommandMenuItemType } from '@/command-menu-item/types/CommandMenuItemType';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
+import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { useMountHeadlessFrontComponent } from '@/front-components/hooks/useMountHeadlessFrontComponent';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useOpenFrontComponentInSidePanel } from '@/side-panel/hooks/useOpenFrontComponentInSidePanel';
+import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { type CommandMenuContextApi } from 'twenty-shared/types';
@@ -129,6 +131,12 @@ type BuildCommandMenuItemFromStandardKeyParams = {
isPinned: boolean;
getIcon: ReturnType['getIcon'];
commandMenuContextApi: CommandMenuContextApi;
+ mountEngineCommand: (
+ engineCommandId: string,
+ contextStoreInstanceId: string,
+ engineComponentKey: EngineComponentKey,
+ ) => void;
+ contextStoreInstanceId: string;
};
const buildCommandItemFromEngineKey = ({
@@ -139,10 +147,14 @@ const buildCommandItemFromEngineKey = ({
isPinned,
getIcon,
commandMenuContextApi,
+ mountEngineCommand,
+ contextStoreInstanceId,
}: BuildCommandMenuItemFromStandardKeyParams) => {
const Icon = getIcon(item.icon, COMMAND_MENU_DEFAULT_ICON);
- const component = ENGINE_COMPONENT_KEY_COMPONENT_MAP[engineComponentKey];
+ const handleClick = () => {
+ mountEngineCommand(item.id, contextStoreInstanceId, engineComponentKey);
+ };
return {
type,
@@ -159,16 +171,21 @@ const buildCommandItemFromEngineKey = ({
item.conditionalAvailabilityExpression,
commandMenuContextApi,
),
- component,
+ component: ,
};
};
-export const useCommandMenuItemFrontComponentCommands = (
+export const useCommandMenuItemsFromBackend = (
commandMenuContextApi: CommandMenuContextApi,
) => {
const { getIcon } = useIcons();
const { openFrontComponentInSidePanel } = useOpenFrontComponentInSidePanel();
const mountHeadlessFrontComponent = useMountHeadlessFrontComponent();
+ const mountEngineCommand = useMountEngineCommand();
+
+ const contextStoreInstanceId = useAvailableComponentInstanceIdOrThrow(
+ ContextStoreComponentInstanceContext,
+ );
const contextStoreIsPageInEditMode = useAtomComponentStateValue(
contextStoreIsPageInEditModeComponentState,
@@ -236,6 +253,8 @@ export const useCommandMenuItemFrontComponentCommands = (
isPinned,
getIcon,
commandMenuContextApi,
+ mountEngineCommand,
+ contextStoreInstanceId,
});
}
diff --git a/packages/twenty-front/src/modules/context-store/hooks/useContextStoreObjectMetadataItemOrThrow.ts b/packages/twenty-front/src/modules/context-store/hooks/useContextStoreObjectMetadataItemOrThrow.ts
index 2ef1d9b225..fac0e4d280 100644
--- a/packages/twenty-front/src/modules/context-store/hooks/useContextStoreObjectMetadataItemOrThrow.ts
+++ b/packages/twenty-front/src/modules/context-store/hooks/useContextStoreObjectMetadataItemOrThrow.ts
@@ -1,7 +1,7 @@
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
-import { isDefined } from 'twenty-shared/utils';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
+import { isDefined } from 'twenty-shared/utils';
export const useContextStoreObjectMetadataItemOrThrow = (
contextStoreInstanceId?: string,
diff --git a/packages/twenty-front/src/modules/front-components/components/HeadlessFrontComponentMountRoot.tsx b/packages/twenty-front/src/modules/front-components/components/HeadlessFrontComponentMountRoot.tsx
index 810617f2f2..df6463c160 100644
--- a/packages/twenty-front/src/modules/front-components/components/HeadlessFrontComponentMountRoot.tsx
+++ b/packages/twenty-front/src/modules/front-components/components/HeadlessFrontComponentMountRoot.tsx
@@ -1,5 +1,7 @@
import { Suspense, lazy } from 'react';
+import { CommandMenuItemErrorBoundary } from '@/command-menu-item/display/components/CommandMenuItemErrorBoundary';
+import { useUnmountHeadlessFrontComponent } from '@/front-components/hooks/useUnmountHeadlessFrontComponent';
import { mountedHeadlessFrontComponentMapsState } from '@/front-components/states/mountedHeadlessFrontComponentMapsState';
import { LayoutRenderingProvider } from '@/ui/layout/contexts/LayoutRenderingContext';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -16,34 +18,37 @@ export const HeadlessFrontComponentMountRoot = () => {
const mountedHeadlessFrontComponentMaps = useAtomStateValue(
mountedHeadlessFrontComponentMapsState,
);
-
- if (mountedHeadlessFrontComponentMaps.size === 0) {
- return null;
- }
+ const unmountHeadlessFrontComponent = useUnmountHeadlessFrontComponent();
return (
<>
{[...mountedHeadlessFrontComponentMaps.entries()].map(
([frontComponentId, mountContext]) => (
-
-
-
-
-
+ unmountHeadlessFrontComponent(frontComponentId)}
+ >
+
+
+
+
+
+
),
)}
>
diff --git a/packages/twenty-front/src/modules/front-components/hooks/useFrontComponentExecutionContext.ts b/packages/twenty-front/src/modules/front-components/hooks/useFrontComponentExecutionContext.ts
index 346cb6b26c..f943e4e70d 100644
--- a/packages/twenty-front/src/modules/front-components/hooks/useFrontComponentExecutionContext.ts
+++ b/packages/twenty-front/src/modules/front-components/hooks/useFrontComponentExecutionContext.ts
@@ -5,13 +5,13 @@ import {
} from 'twenty-sdk/front-component-renderer';
import { type AppPath, type EnqueueSnackbarParams } from 'twenty-shared/types';
-import { useCommandMenuConfirmationModal } from '@/command-menu-item/confirmation-modal/hooks/useCommandMenuConfirmationModal';
import { currentUserState } from '@/auth/states/currentUserState';
-import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
-import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
-import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
+import { useCommandMenuConfirmationModal } from '@/command-menu-item/confirmation-modal/hooks/useCommandMenuConfirmationModal';
import { useRequestApplicationTokenRefresh } from '@/front-components/hooks/useRequestApplicationTokenRefresh';
import { useUnmountHeadlessFrontComponent } from '@/front-components/hooks/useUnmountHeadlessFrontComponent';
+import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
+import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
+import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useLayoutRenderingContext } from '@/ui/layout/contexts/LayoutRenderingContext';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -75,7 +75,7 @@ export const useFrontComponentExecutionContext = ({
const openCommandConfirmationModal: FrontComponentHostCommunicationApi['openCommandConfirmationModal'] =
async ({ title, subtitle, confirmButtonText, confirmButtonAccent }) => {
openConfirmationModal({
- frontComponentId,
+ caller: { type: 'frontComponent', frontComponentId },
title,
subtitle,
confirmButtonText,
diff --git a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useRemoveRecordFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useRemoveRecordFilter.ts
index b9e6af535f..c3810b5ae9 100644
--- a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useRemoveRecordFilter.ts
+++ b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useRemoveRecordFilter.ts
@@ -6,9 +6,10 @@ import { useStore } from 'jotai';
import { useCallback, useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
-export const useRemoveRecordFilter = () => {
+export const useRemoveRecordFilter = (instanceId?: string) => {
const currentRecordFilters = useAtomComponentStateCallbackState(
currentRecordFiltersComponentState,
+ instanceId,
);
const store = useStore();
diff --git a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useUpsertRecordFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useUpsertRecordFilter.ts
index ad0c29fa16..00328508a7 100644
--- a/packages/twenty-front/src/modules/object-record/record-filter/hooks/useUpsertRecordFilter.ts
+++ b/packages/twenty-front/src/modules/object-record/record-filter/hooks/useUpsertRecordFilter.ts
@@ -5,9 +5,10 @@ import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/h
import { useStore } from 'jotai';
import { useCallback, useContext } from 'react';
-export const useUpsertRecordFilter = () => {
+export const useUpsertRecordFilter = (instanceId?: string) => {
const currentRecordFilters = useAtomComponentStateCallbackState(
currentRecordFiltersComponentState,
+ instanceId,
);
const store = useStore();
diff --git a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords.ts b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords.ts
index 5d9a1e55db..ccf68222c7 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords.ts
+++ b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useRecordIndexLazyFetchRecords.ts
@@ -89,6 +89,7 @@ export const useRecordIndexLazyFetchRecords = ({
const findManyRecordsParams = useFindManyRecordIndexTableParams(
objectMetadataItem.nameSingular,
+ recordIndexId,
);
const queryFilter = computeContextStoreFilters({
@@ -102,6 +103,7 @@ export const useRecordIndexLazyFetchRecords = ({
const visibleRecordFields = useAtomComponentSelectorValue(
visibleRecordFieldsComponentSelector,
+ recordIndexId,
);
const finalColumns: Pick<
diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts
index c8c2b30476..c22e70db26 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts
+++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useFindManyRecordIndexTableParams.ts
@@ -17,6 +17,7 @@ import {
export const useFindManyRecordIndexTableParams = (
objectNameSingular: string,
+ instanceId?: string,
) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
@@ -31,14 +32,17 @@ export const useFindManyRecordIndexTableParams = (
const currentRecordFilterGroups = useAtomComponentStateValue(
currentRecordFilterGroupsComponentState,
+ instanceId,
);
const currentRecordSorts = useAtomComponentStateValue(
currentRecordSortsComponentState,
+ instanceId,
);
const currentRecordFilters = useAtomComponentStateValue(
currentRecordFiltersComponentState,
+ instanceId,
);
const { filterValueDependencies } = useFilterValueDependencies();
@@ -52,6 +56,7 @@ export const useFindManyRecordIndexTableParams = (
const anyFieldFilterValue = useAtomComponentStateValue(
anyFieldFilterValueComponentState,
+ instanceId,
);
const { recordGqlOperationFilter: anyFieldFilter } =
diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleTrashColumnFilter.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleTrashColumnFilter.ts
index b5c68197d0..c6487d0a10 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleTrashColumnFilter.ts
+++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleTrashColumnFilter.ts
@@ -16,11 +16,13 @@ import { ViewFilterOperand } from 'twenty-shared/types';
type UseHandleToggleTrashColumnFilterProps = {
objectNameSingular: string;
viewBarId: string;
+ recordFiltersInstanceId?: string;
};
export const useHandleToggleTrashColumnFilter = ({
viewBarId,
objectNameSingular,
+ recordFiltersInstanceId,
}: UseHandleToggleTrashColumnFilterProps) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
@@ -35,7 +37,7 @@ export const useHandleToggleTrashColumnFilter = ({
viewBarId,
);
- const { upsertRecordFilter } = useUpsertRecordFilter();
+ const { upsertRecordFilter } = useUpsertRecordFilter(recordFiltersInstanceId);
const handleToggleTrashColumnFilter = useCallback(() => {
const trashFieldMetadata = objectMetadataItem.fields.find(
diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useBuildRecordInputFromFilters.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useBuildRecordInputFromFilters.ts
index 5f5c5c6584..c652e0bee6 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useBuildRecordInputFromFilters.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useBuildRecordInputFromFilters.ts
@@ -9,11 +9,14 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
export const useBuildRecordInputFromFilters = ({
objectMetadataItem,
+ instanceId,
}: {
objectMetadataItem: ObjectMetadataItem;
+ instanceId?: string;
}) => {
const currentRecordFilters = useAtomComponentStateValue(
currentRecordFiltersComponentState,
+ instanceId,
);
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewIndexRecord.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewIndexRecord.ts
index 44e5d90885..5374185ceb 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewIndexRecord.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewIndexRecord.ts
@@ -25,23 +25,28 @@ import { useNavigateApp } from '~/hooks/useNavigateApp';
type UseCreateNewIndexRecordProps = {
objectMetadataItem: ObjectMetadataItem;
+ instanceId?: string;
};
export const useCreateNewIndexRecord = ({
objectMetadataItem,
+ instanceId,
}: UseCreateNewIndexRecordProps) => {
const recordGroupDefinitions = useAtomComponentSelectorValue(
recordGroupDefinitionsComponentSelector,
+ instanceId,
);
const store = useStore();
const recordIndexRecordIdsByGroupCallbackState =
useAtomComponentFamilyStateCallbackState(
recordIndexRecordIdsByGroupComponentFamilyState,
+ instanceId,
);
const recordIndexGroupFieldMetadataItem = useAtomComponentStateValue(
recordIndexGroupFieldMetadataItemComponentState,
+ instanceId,
);
const { openRecordInSidePanel } = useOpenRecordInSidePanel();
@@ -59,6 +64,7 @@ export const useCreateNewIndexRecord = ({
const { buildRecordInputFromFilters } = useBuildRecordInputFromFilters({
objectMetadataItem,
+ instanceId,
});
const { buildRecordInputFromRLSPredicates } =
diff --git a/packages/twenty-sdk/src/front-component-renderer/remote/components/FrontComponentWorkerEffect.tsx b/packages/twenty-sdk/src/front-component-renderer/remote/components/FrontComponentWorkerEffect.tsx
index 341baa0e95..7a01f635ba 100644
--- a/packages/twenty-sdk/src/front-component-renderer/remote/components/FrontComponentWorkerEffect.tsx
+++ b/packages/twenty-sdk/src/front-component-renderer/remote/components/FrontComponentWorkerEffect.tsx
@@ -1,6 +1,7 @@
import { ThreadWebWorker, release, retain } from '@quilted/threads';
import { RemoteReceiver } from '@remote-dom/core/receivers';
import { useEffect, useRef } from 'react';
+import { type ConfirmationModalCaller } from 'twenty-shared/types';
import { type CommandConfirmationModalResult } from '../../../sdk/front-component-api/globals/frontComponentHostCommunicationApi';
import { type FrontComponentHostCommunicationApi } from '../../types/FrontComponentHostCommunicationApi';
import { type WorkerExports } from '../../types/WorkerExports';
@@ -11,7 +12,7 @@ const COMMAND_MENU_ITEM_CONFIRMATION_MODAL_RESULT_BROWSER_EVENT_NAME =
'command-menu-item-confirmation-modal-result';
type CommandMenuItemConfirmationModalResultBrowserEventDetail = {
- frontComponentId: string;
+ caller: ConfirmationModalCaller;
confirmationResult: CommandConfirmationModalResult;
};
@@ -73,9 +74,12 @@ export const FrontComponentWorkerEffect = ({
const commandMenuItemConfirmationModalResultBrowserEventDetail =
event.detail;
+ const caller =
+ commandMenuItemConfirmationModalResultBrowserEventDetail.caller;
+
if (
- commandMenuItemConfirmationModalResultBrowserEventDetail.frontComponentId !==
- frontComponentId
+ caller.type !== 'frontComponent' ||
+ caller.frontComponentId !== frontComponentId
) {
return;
}
diff --git a/packages/twenty-sdk/src/sdk/command/Command.tsx b/packages/twenty-sdk/src/sdk/command/Command.tsx
index beff14490e..a506f1aa30 100644
--- a/packages/twenty-sdk/src/sdk/command/Command.tsx
+++ b/packages/twenty-sdk/src/sdk/command/Command.tsx
@@ -1,8 +1,6 @@
import { useEffect, useState } from 'react';
import {
- enqueueSnackbar,
- getFrontComponentCommandErrorDedupeKey,
unmountFrontComponent,
useFrontComponentId,
} from '../front-component-api';
@@ -24,20 +22,9 @@ export const Command = ({ execute }: CommandProps) => {
setHasExecuted(true);
const run = async () => {
- try {
- await execute();
- } catch (error) {
- if (error instanceof Error) {
- await enqueueSnackbar({
- message: 'Command failed',
- detailedMessage: error.message,
- variant: 'error',
- dedupeKey: getFrontComponentCommandErrorDedupeKey(frontComponentId),
- });
- }
- } finally {
- await unmountFrontComponent();
- }
+ await execute();
+
+ await unmountFrontComponent();
};
run();
diff --git a/packages/twenty-sdk/src/sdk/command/CommandLink.tsx b/packages/twenty-sdk/src/sdk/command/CommandLink.tsx
index 8c25e45314..dc461e5ef4 100644
--- a/packages/twenty-sdk/src/sdk/command/CommandLink.tsx
+++ b/packages/twenty-sdk/src/sdk/command/CommandLink.tsx
@@ -4,8 +4,6 @@ import { type NavigateOptions } from 'react-router-dom';
import { type AppPath } from 'twenty-shared/types';
import { type getAppPath } from 'twenty-shared/utils';
import {
- enqueueSnackbar,
- getFrontComponentCommandErrorDedupeKey,
navigate,
unmountFrontComponent,
useFrontComponentId,
@@ -36,20 +34,9 @@ export const CommandLink = ({
setHasExecuted(true);
const run = async () => {
- try {
- await navigate(to, params, queryParams, options);
- } catch (error) {
- if (error instanceof Error) {
- await enqueueSnackbar({
- message: 'Command failed',
- detailedMessage: error.message,
- variant: 'error',
- dedupeKey: getFrontComponentCommandErrorDedupeKey(frontComponentId),
- });
- }
- } finally {
- await unmountFrontComponent();
- }
+ await navigate(to, params, queryParams, options);
+
+ await unmountFrontComponent();
};
run();
diff --git a/packages/twenty-sdk/src/sdk/command/CommandModal.tsx b/packages/twenty-sdk/src/sdk/command/CommandModal.tsx
index 94f4423301..5e6175ed55 100644
--- a/packages/twenty-sdk/src/sdk/command/CommandModal.tsx
+++ b/packages/twenty-sdk/src/sdk/command/CommandModal.tsx
@@ -1,8 +1,6 @@
import { useEffect, useState } from 'react';
import {
- enqueueSnackbar,
- getFrontComponentCommandErrorDedupeKey,
openCommandConfirmationModal,
unmountFrontComponent,
useFrontComponentId,
@@ -36,30 +34,20 @@ export const CommandModal = ({
setHasExecuted(true);
const run = async () => {
- try {
- const commandConfirmationModalResult =
- await openCommandConfirmationModal({
- title,
- subtitle,
- confirmButtonText,
- confirmButtonAccent,
- });
+ const commandConfirmationModalResult = await openCommandConfirmationModal(
+ {
+ title,
+ subtitle,
+ confirmButtonText,
+ confirmButtonAccent,
+ },
+ );
- if (commandConfirmationModalResult === 'confirm') {
- await execute();
- }
- } catch (error) {
- const message = error instanceof Error ? error.message : String(error);
-
- await enqueueSnackbar({
- message: 'Command failed',
- detailedMessage: message,
- variant: 'error',
- dedupeKey: getFrontComponentCommandErrorDedupeKey(frontComponentId),
- });
- } finally {
- await unmountFrontComponent();
+ if (commandConfirmationModalResult === 'confirm') {
+ await execute();
}
+
+ await unmountFrontComponent();
};
run();
diff --git a/packages/twenty-sdk/src/sdk/command/CommandOpenSidePanelPage.tsx b/packages/twenty-sdk/src/sdk/command/CommandOpenSidePanelPage.tsx
index ae96c4b77b..9d18b2284a 100644
--- a/packages/twenty-sdk/src/sdk/command/CommandOpenSidePanelPage.tsx
+++ b/packages/twenty-sdk/src/sdk/command/CommandOpenSidePanelPage.tsx
@@ -1,6 +1,4 @@
import {
- enqueueSnackbar,
- getFrontComponentCommandErrorDedupeKey,
openSidePanelPage,
unmountFrontComponent,
useFrontComponentId,
@@ -38,25 +36,14 @@ export const CommandOpenSidePanelPage = ({
const run = async () => {
onClick?.();
- try {
- await openSidePanelPage({
- page,
- pageTitle,
- pageIcon,
- shouldResetSearchState,
- });
- } catch (error) {
- if (error instanceof Error) {
- await enqueueSnackbar({
- message: 'Command failed',
- detailedMessage: error.message,
- variant: 'error',
- dedupeKey: getFrontComponentCommandErrorDedupeKey(frontComponentId),
- });
- }
- } finally {
- await unmountFrontComponent();
- }
+ await openSidePanelPage({
+ page,
+ pageTitle,
+ pageIcon,
+ shouldResetSearchState,
+ });
+
+ await unmountFrontComponent();
};
run();
diff --git a/packages/twenty-shared/src/types/ConfirmationModalCaller.ts b/packages/twenty-shared/src/types/ConfirmationModalCaller.ts
new file mode 100644
index 0000000000..d075ba1100
--- /dev/null
+++ b/packages/twenty-shared/src/types/ConfirmationModalCaller.ts
@@ -0,0 +1,3 @@
+export type ConfirmationModalCaller =
+ | { type: 'frontComponent'; frontComponentId: string }
+ | { type: 'engineCommand'; engineCommandId: string };
diff --git a/packages/twenty-shared/src/types/index.ts b/packages/twenty-shared/src/types/index.ts
index 23f1fe3aa6..147993ff8a 100644
--- a/packages/twenty-shared/src/types/index.ts
+++ b/packages/twenty-shared/src/types/index.ts
@@ -53,6 +53,7 @@ export {
} from './composite-types/rich-text.composite-type';
export type { CompositeFieldSubFieldName } from './CompositeFieldSubFieldNameType';
export type { ConfigVariableValue } from './ConfigVariableValue';
+export type { ConfirmationModalCaller } from './ConfirmationModalCaller';
export { ConnectedAccountProvider } from './ConnectedAccountProvider';
export { CoreObjectNameSingular } from './CoreObjectNameSingular';
export { CrudOperationType } from './CrudOperationType';