feat: CommandMenuItem entity and FrontComponent support in command menu (#17555)

https://github.com/user-attachments/assets/6f172ffc-d43d-42fd-a26b-94f591fb767e
This commit is contained in:
Abdul Rahman
2026-01-30 22:05:06 +05:30
committed by GitHub
parent 6462ff6c5f
commit 51a2a90b0a
45 changed files with 899 additions and 34 deletions
@@ -0,0 +1,18 @@
import { gql } from '@apollo/client';
export const COMMAND_MENU_ITEM_FRAGMENT = gql`
fragment CommandMenuItemFields on CommandMenuItem {
id
workflowVersionId
frontComponentId
frontComponent {
id
name
}
label
icon
isPinned
availabilityType
availabilityObjectMetadataId
}
`;
@@ -0,0 +1,11 @@
import { COMMAND_MENU_ITEM_FRAGMENT } from '@/command-menu-item/graphql/fragments/commandMenuItemFragment';
import { gql } from '@apollo/client';
export const FIND_MANY_COMMAND_MENU_ITEMS = gql`
${COMMAND_MENU_ITEM_FRAGMENT}
query FindManyCommandMenuItems {
commandMenuItems {
...CommandMenuItemFields
}
}
`;
@@ -0,0 +1,158 @@
import { Action } from '@/action-menu/actions/components/Action';
import { ActionScope } from '@/action-menu/actions/types/ActionScope';
import { ActionType } from '@/action-menu/actions/types/ActionType';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { useOpenFrontComponentInCommandMenu } from '@/command-menu/hooks/useOpenFrontComponentInCommandMenu';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { type IconComponent, useIcons } from 'twenty-ui/display';
import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants/CommandMenuDefaultIcon';
import {
type CommandMenuItemFieldsFragment,
CommandMenuItemAvailabilityType,
useFindManyCommandMenuItemsQuery,
} from '~/generated-metadata/graphql';
import { FeatureFlagKey } from '~/generated/graphql';
type CommandMenuItemWithFrontComponent = CommandMenuItemFieldsFragment & {
frontComponentId: string;
};
type BuildActionFromItemParams = {
item: CommandMenuItemWithFrontComponent;
scope: ActionScope;
index: number;
isPinned: boolean;
getIcon: ReturnType<typeof useIcons>['getIcon'];
openFrontComponentInCommandMenu: (params: {
frontComponentId: string;
pageTitle: string;
pageIcon: IconComponent;
}) => void;
};
const buildActionFromItem = ({
item,
scope,
index,
isPinned,
getIcon,
openFrontComponentInCommandMenu,
}: BuildActionFromItemParams) => {
const displayLabel = item.label;
const Icon = getIcon(item.icon, COMMAND_MENU_DEFAULT_ICON);
return {
type: ActionType.FrontComponent,
key: `command-menu-item-front-component-${item.id}`,
scope,
label: displayLabel,
shortLabel: displayLabel,
position: index,
isPinned,
Icon,
shouldBeRegistered: () => true,
component: (
<Action
onClick={() =>
openFrontComponentInCommandMenu({
frontComponentId: item.frontComponentId,
pageTitle: displayLabel,
pageIcon: Icon,
})
}
closeSidePanelOnCommandMenuListActionExecution={false}
/>
),
};
};
export const useCommandMenuItemFrontComponentActions = () => {
const { getIcon } = useIcons();
const { openFrontComponentInCommandMenu } =
useOpenFrontComponentInCommandMenu();
const isPageInEditMode = useRecoilComponentValue(
contextStoreIsPageInEditModeComponentState,
);
const { actionMenuType } = useContext(ActionMenuContext);
const currentObjectMetadataItemId = useRecoilComponentValue(
contextStoreCurrentObjectMetadataItemIdComponentState,
);
const targetedRecordsRule = useRecoilComponentValue(
contextStoreTargetedRecordsRuleComponentState,
);
const isCommandMenuItemEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
);
const { data } = useFindManyCommandMenuItemsQuery({
skip:
!isCommandMenuItemEnabled ||
(actionMenuType !== 'command-menu' &&
actionMenuType !== 'command-menu-show-page-action-menu-dropdown'),
});
const frontComponentItems =
data?.commandMenuItems?.filter(
(item): item is CommandMenuItemWithFrontComponent =>
isDefined(item.frontComponentId),
) ?? [];
const selectedRecordCount =
targetedRecordsRule.mode === 'selection'
? targetedRecordsRule.selectedRecordIds.length
: 0;
const objectMatches = (item: CommandMenuItemWithFrontComponent) =>
!isDefined(item.availabilityObjectMetadataId) ||
item.availabilityObjectMetadataId === currentObjectMetadataItemId;
const globalItems = frontComponentItems.filter(
(item) => item.availabilityType === CommandMenuItemAvailabilityType.GLOBAL,
);
const recordScopedItems = frontComponentItems.filter((item) => {
if (!objectMatches(item)) return false;
if (item.availabilityType === CommandMenuItemAvailabilityType.SINGLE_RECORD)
return selectedRecordCount === 1;
if (item.availabilityType === CommandMenuItemAvailabilityType.BULK_RECORDS)
return selectedRecordCount >= 1;
return false;
});
const globalActions = globalItems.map((item, index) =>
buildActionFromItem({
item,
scope: ActionScope.Global,
index,
isPinned: !isPageInEditMode && item.isPinned,
getIcon,
openFrontComponentInCommandMenu,
}),
);
const recordScopedActions = recordScopedItems.map((item, index) =>
buildActionFromItem({
item,
scope: ActionScope.RecordSelection,
index,
isPinned: !isPageInEditMode && item.isPinned,
getIcon,
openFrontComponentInCommandMenu,
}),
);
return [...globalActions, ...recordScopedActions];
};