[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.
This commit is contained in:
Raphaël Bosi
2026-03-17 17:25:18 +01:00
committed by GitHub
parent bee474afdf
commit abdab2fb7e
83 changed files with 2594 additions and 132 deletions
@@ -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,
@@ -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 (
<ErrorBoundary
fallbackRender={() => null}
onError={handleError}
resetKeys={[engineCommandId]}
>
{children}
</ErrorBoundary>
);
};