[COMMAND MENU ITEMS] Add engine component key (#18554)
## PR Description In the process of migrating all the existing commands to the backend, we stumbled across a couple of problems that made us reconsider the full migration. This PR introduces a way for command menu items to bypass front components and to directly reference a frontend component from twenty front. It: - Introduces a `engineFrontComponentKey` field on `CommandMenuItem` as an alternative to `frontComponentId` and `workflowVersionId`, allowing command menu items to reference frontend components by key directly rather than requiring a FrontComponent entity - Updates the DB constraint to allow exactly one of `workflowVersionId`, `frontComponentId`, or `engineFrontComponentKey` ### All standard command menu items from the frontend which use `standardFrontComponentKey` These are all commands that execute a GraphQL query or a mutation. Two mains concerned have been raised that made us go with this (temporary) architecture instead: - If those commands are part of the standard application, they can only alter objects from that application and not custom objects. - We would need to implement a way to trigger optimistic rendering from the front components, which might take some time to implement. List: - Create new record - Delete (single record) - Delete records (multiple) - Restore record - Restore records (multiple) - Permanently destroy record - Permanently destroy records (multiple) - Add to favorites - Remove from favorites - Merge records - Duplicate Dashboard - Save Dashboard - Save Page Layout - Activate Workflow - Deactivate Workflow - Discard Draft (workflow) - Test Workflow - Tidy up workflow - Duplicate Workflow - Stop (workflow run) - Use as draft (workflow version) --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
+121
-48
@@ -1,3 +1,4 @@
|
||||
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 { CommandMenuItemScope } from '@/command-menu-item/types/CommandMenuItemScope';
|
||||
@@ -22,6 +23,7 @@ import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants
|
||||
import {
|
||||
CommandMenuItemAvailabilityType,
|
||||
type CommandMenuItemFieldsFragment,
|
||||
type EngineComponentKey,
|
||||
useFindManyCommandMenuItemsQuery,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
@@ -30,6 +32,10 @@ type CommandMenuItemWithFrontComponent = CommandMenuItemFieldsFragment & {
|
||||
conditionalAvailabilityExpression?: string | null;
|
||||
};
|
||||
|
||||
type CommandMenuItemWithSource = CommandMenuItemFieldsFragment & {
|
||||
conditionalAvailabilityExpression?: string | null;
|
||||
};
|
||||
|
||||
type BuildCommandMenuItemFromFrontComponentParams = {
|
||||
item: CommandMenuItemWithFrontComponent;
|
||||
type?: CommandMenuItemType;
|
||||
@@ -53,8 +59,6 @@ type BuildCommandMenuItemFromFrontComponentParams = {
|
||||
commandMenuContextApi: CommandMenuContextApi;
|
||||
};
|
||||
|
||||
// TODO: we should remove this backward compatibility logic in the future
|
||||
// once we have migrated all command menu items
|
||||
const buildCommandMenuItemFromFrontComponent = ({
|
||||
item,
|
||||
type = CommandMenuItemType.FrontComponent,
|
||||
@@ -115,6 +119,47 @@ const buildCommandMenuItemFromFrontComponent = ({
|
||||
};
|
||||
};
|
||||
|
||||
type BuildCommandMenuItemFromStandardKeyParams = {
|
||||
item: CommandMenuItemWithSource;
|
||||
engineComponentKey: EngineComponentKey;
|
||||
type?: CommandMenuItemType;
|
||||
scope: CommandMenuItemScope;
|
||||
isPinned: boolean;
|
||||
getIcon: ReturnType<typeof useIcons>['getIcon'];
|
||||
commandMenuContextApi: CommandMenuContextApi;
|
||||
};
|
||||
|
||||
const buildCommandItemFromEngineKey = ({
|
||||
item,
|
||||
engineComponentKey,
|
||||
type = CommandMenuItemType.Standard,
|
||||
scope,
|
||||
isPinned,
|
||||
getIcon,
|
||||
commandMenuContextApi,
|
||||
}: BuildCommandMenuItemFromStandardKeyParams) => {
|
||||
const Icon = getIcon(item.icon, COMMAND_MENU_DEFAULT_ICON);
|
||||
|
||||
const component = ENGINE_COMPONENT_KEY_COMPONENT_MAP[engineComponentKey];
|
||||
|
||||
return {
|
||||
type,
|
||||
key: `command-menu-item-engine-${item.id}`,
|
||||
scope,
|
||||
label: item.label,
|
||||
shortLabel: item.shortLabel ?? undefined,
|
||||
position: item.position,
|
||||
isPinned,
|
||||
Icon,
|
||||
shouldBeRegistered: () =>
|
||||
evaluateConditionalAvailabilityExpression(
|
||||
item.conditionalAvailabilityExpression,
|
||||
commandMenuContextApi,
|
||||
),
|
||||
component,
|
||||
};
|
||||
};
|
||||
|
||||
export const useCommandMenuItemFrontComponentCommands = (
|
||||
commandMenuContextApi: CommandMenuContextApi,
|
||||
) => {
|
||||
@@ -159,74 +204,102 @@ export const useCommandMenuItemFrontComponentCommands = (
|
||||
|
||||
const { data } = useFindManyCommandMenuItemsQuery();
|
||||
|
||||
const frontComponentItems =
|
||||
data?.commandMenuItems?.filter(
|
||||
(item): item is CommandMenuItemWithFrontComponent =>
|
||||
isDefined(item.frontComponentId),
|
||||
) ?? [];
|
||||
const allItems = data?.commandMenuItems ?? [];
|
||||
|
||||
const objectMatches = (item: CommandMenuItemWithFrontComponent) =>
|
||||
const objectMatches = (item: CommandMenuItemFieldsFragment) =>
|
||||
!isDefined(item.availabilityObjectMetadataId) ||
|
||||
item.availabilityObjectMetadataId ===
|
||||
contextStoreCurrentObjectMetadataItemId;
|
||||
|
||||
const frontComponentItemsWithObjectMatches =
|
||||
frontComponentItems.filter(objectMatches);
|
||||
const itemsWithObjectMatches = allItems.filter(objectMatches);
|
||||
|
||||
const globalItems = frontComponentItemsWithObjectMatches.filter(
|
||||
const buildCommandMenuItem = ({
|
||||
item,
|
||||
scope,
|
||||
isPinned,
|
||||
typeOverride,
|
||||
}: {
|
||||
item: CommandMenuItemFieldsFragment;
|
||||
scope: CommandMenuItemScope;
|
||||
isPinned: boolean;
|
||||
typeOverride?: CommandMenuItemType;
|
||||
}) => {
|
||||
if (isDefined(item.engineComponentKey)) {
|
||||
return buildCommandItemFromEngineKey({
|
||||
item,
|
||||
engineComponentKey: item.engineComponentKey,
|
||||
type: typeOverride,
|
||||
scope,
|
||||
isPinned,
|
||||
getIcon,
|
||||
commandMenuContextApi,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(item.frontComponentId)) {
|
||||
return buildCommandMenuItemFromFrontComponent({
|
||||
item: item as CommandMenuItemWithFrontComponent,
|
||||
type: typeOverride,
|
||||
scope,
|
||||
isPinned,
|
||||
getIcon,
|
||||
openFrontComponentInSidePanel,
|
||||
mountHeadlessFrontComponent,
|
||||
commandMenuContextApi,
|
||||
mountContext,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const globalItems = itemsWithObjectMatches.filter(
|
||||
(item) => item.availabilityType === CommandMenuItemAvailabilityType.GLOBAL,
|
||||
);
|
||||
|
||||
const recordScopedItems = frontComponentItemsWithObjectMatches.filter(
|
||||
const recordScopedItems = itemsWithObjectMatches.filter(
|
||||
(item) =>
|
||||
item.availabilityType ===
|
||||
CommandMenuItemAvailabilityType.RECORD_SELECTION,
|
||||
);
|
||||
|
||||
const fallbackItems = frontComponentItemsWithObjectMatches.filter(
|
||||
const fallbackItems = itemsWithObjectMatches.filter(
|
||||
(item) =>
|
||||
item.availabilityType === CommandMenuItemAvailabilityType.FALLBACK,
|
||||
);
|
||||
|
||||
const globalCommandMenuItems = globalItems.map((item) =>
|
||||
buildCommandMenuItemFromFrontComponent({
|
||||
item,
|
||||
scope: CommandMenuItemScope.Global,
|
||||
isPinned: !contextStoreIsPageInEditMode && item.isPinned,
|
||||
getIcon,
|
||||
openFrontComponentInSidePanel,
|
||||
mountHeadlessFrontComponent,
|
||||
commandMenuContextApi,
|
||||
}),
|
||||
);
|
||||
const globalCommandMenuItems = globalItems
|
||||
.map((item) =>
|
||||
buildCommandMenuItem({
|
||||
item,
|
||||
scope: CommandMenuItemScope.Global,
|
||||
isPinned: !contextStoreIsPageInEditMode && item.isPinned,
|
||||
}),
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
const recordScopedCommandMenuItems = hasRecordSelection
|
||||
? recordScopedItems.map((item) =>
|
||||
buildCommandMenuItemFromFrontComponent({
|
||||
item,
|
||||
scope: CommandMenuItemScope.RecordSelection,
|
||||
isPinned: !contextStoreIsPageInEditMode && item.isPinned,
|
||||
getIcon,
|
||||
openFrontComponentInSidePanel,
|
||||
mountHeadlessFrontComponent,
|
||||
commandMenuContextApi,
|
||||
mountContext,
|
||||
}),
|
||||
)
|
||||
? recordScopedItems
|
||||
.map((item) =>
|
||||
buildCommandMenuItem({
|
||||
item,
|
||||
scope: CommandMenuItemScope.RecordSelection,
|
||||
isPinned: !contextStoreIsPageInEditMode && item.isPinned,
|
||||
}),
|
||||
)
|
||||
.filter(isDefined)
|
||||
: [];
|
||||
|
||||
const fallbackCommandMenuItems = fallbackItems.map((item) =>
|
||||
buildCommandMenuItemFromFrontComponent({
|
||||
item,
|
||||
type: CommandMenuItemType.Fallback,
|
||||
scope: CommandMenuItemScope.Global,
|
||||
isPinned: false,
|
||||
getIcon,
|
||||
openFrontComponentInSidePanel,
|
||||
mountHeadlessFrontComponent,
|
||||
commandMenuContextApi,
|
||||
}),
|
||||
);
|
||||
const fallbackCommandMenuItems = fallbackItems
|
||||
.map((item) =>
|
||||
buildCommandMenuItem({
|
||||
item,
|
||||
scope: CommandMenuItemScope.Global,
|
||||
isPinned: false,
|
||||
typeOverride: CommandMenuItemType.Fallback,
|
||||
}),
|
||||
)
|
||||
.filter(isDefined);
|
||||
|
||||
return [
|
||||
...globalCommandMenuItems,
|
||||
|
||||
Reference in New Issue
Block a user