[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:
+5
-4
@@ -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 (
|
||||
<CommandMenuContext.Provider
|
||||
@@ -30,7 +31,7 @@ export const CommandMenuContextProviderServerItemsContent = ({
|
||||
isInSidePanel,
|
||||
displayType,
|
||||
containerType,
|
||||
commandMenuItems: commandMenuItemFrontComponentActions,
|
||||
commandMenuItems,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
+23
-4
@@ -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<typeof useIcons>['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: <Command onClick={handleClick} />,
|
||||
};
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user