Files
twenty/packages/twenty-sdk/src/sdk/command/CommandOpenSidePanelPage.tsx
T
Raphaël Bosi abdab2fb7e [Command menu items] Create engine commands (#18681)
## Description

- Introduces a new engine command execution model that replaces the
previous approach of mapping `EngineComponentKey` to React components.
Instead, engine commands are now mounted headlessly via
`HeadlessEngineCommandMountRoot`, with their execution context populated
synchronously before mounting.
- Creates new headless command components
- Moves error handling from the SDK layer to the host app by wrapping
all mounted commands with a new `CommandMenuItemErrorBoundary`

The new flow works as follows:
- When a command menu item with an `engineComponentKey` is clicked,
`useCommandMenuItemFrontComponentCommands` calls
`useMountEngineCommand`, which synchronously reads the current context
store (object metadata, selected records, filters, view ID, etc.) and
writes a `MountedEngineCommandContext` into
`mountedEngineCommandsState`.
- The command is then mounted into `mountedEngineCommandsState`, which
triggers `HeadlessEngineCommandMountRoot` to render the corresponding
headless component from `ENGINE_COMPONENT_KEY_HEADLESS_COMPONENT_MAP`,
wrapped in `CommandMenuItemErrorBoundary`,
`ContextStoreComponentInstanceContext.Provider`, and
`EngineCommandComponentInstanceContext.Provider`.
- Each command component reads its execution context and delegates to
one of the 4 execution patterns: `HeadlessEngineCommandWrapperEffect`
(simple actions), `HeadlessConfirmationModalEngineCommandEffect`
(destructive actions needing confirmation),
`HeadlessNavigateEngineCommand` (GO_TO_* commands), or
`HeadlessOpenSidePanelPageEngineCommand` (SEARCH_RECORDS, ASK_AI,
VIEW_PREVIOUS_AI_CHATS).
- After execution, the command self-unmounts via
`useUnmountEngineCommand`, which removes the entry from
`mountedEngineCommandsState` and stops rendering the component.
2026-03-17 17:25:18 +01:00

62 lines
1.1 KiB
TypeScript

import {
openSidePanelPage,
unmountFrontComponent,
useFrontComponentId,
} from '@/sdk/front-component-api';
import { useEffect, useState } from 'react';
import { type SidePanelPages } from 'twenty-shared/types';
export type CommandOpenSidePanelPageProps = {
page: SidePanelPages;
pageTitle: string;
pageIcon: string;
onClick?: () => void;
shouldResetSearchState?: boolean;
};
export const CommandOpenSidePanelPage = ({
page,
pageTitle,
pageIcon,
onClick,
shouldResetSearchState = false,
}: CommandOpenSidePanelPageProps) => {
const [hasExecuted, setHasExecuted] = useState(false);
const frontComponentId = useFrontComponentId();
useEffect(() => {
if (hasExecuted) {
return;
}
setHasExecuted(true);
const run = async () => {
onClick?.();
await openSidePanelPage({
page,
pageTitle,
pageIcon,
shouldResetSearchState,
});
await unmountFrontComponent();
};
run();
}, [
page,
pageTitle,
pageIcon,
shouldResetSearchState,
onClick,
hasExecuted,
frontComponentId,
]);
return null;
};