abdab2fb7e
## 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.
130 lines
4.9 KiB
TypeScript
130 lines
4.9 KiB
TypeScript
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;
|
|
};
|