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