[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:
+1
-1
@@ -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,
|
||||
|
||||
+49
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user