[Command Menu] Fix record-selection command filtering in edit mode (#20034)

https://github.com/user-attachments/assets/fe1461c7-0d5c-4c6f-8c2e-2cf569e7de90

## What

Fix `RECORD_SELECTION` items leaking into the command menu when nothing
is selected, and unify how the menu renders in normal vs edit mode.

## The bug

`RECORD_SELECTION`-availability items were showing up even when
`numberOfSelectedRecords === 0`. New util
`doesCommandMenuItemMatchSelectionState` gates them, applied
consistently in the runtime provider and the editor.

## The refactor

`PinnedCommandMenuItemButtonsEditMode` was a 140-line near-duplicate of
`PinnedCommandMenuItemButtons` with its own (drifting) filter logic.
Killed it. Edit mode now flows through the same
`CommandMenuContextProvider` with a new `isInPreviewMode` flag — one
filter chain, one rendering path.

## Behavior in edit mode

**Header (pinned buttons in page header):**
- Runs the full filter chain — object metadata, page type, selection
state, page layout, *and the conditional availability expression*
- Buttons render at full styling but are inert via `pointer-events:
none` + `cursor: not-allowed`
- Preview now reflects exactly what users will see on the live page (not
a grayed-out approximation)

**Side panel editor:**
- New `useEditableCommandMenuItems` hook
- Same filters as runtime *minus* the conditional availability
expression and `FALLBACK` items — so it surfaces everything that's
actually configurable for this page context
- Still gates on selection state — if no records selected,
`RECORD_SELECTION` items are hidden from the editor too. Open to
feedback if we'd rather always show them so users can pin them ahead of
time.

## Misc

- `usePinnedCommandMenuItemsInlineLayout` — visible count now waits
until every item is measured before committing. Fixes a flash of wrong
counts on mount/resize
- Renamed `useCommandMenuContextApi` → `useCurrentCommandMenuContextApi`
— name now conveys it reads from the *current* scoped context store
- Copy: "Records selected" → "Record(s) selected"
This commit is contained in:
nitin
2026-04-29 19:04:28 +05:30
committed by GitHub
parent 51384bc085
commit 4649736d49
20 changed files with 200 additions and 218 deletions
@@ -11,6 +11,7 @@ import { isSelectedItemIdComponentFamilyState } from '@/ui/layout/selectable-lis
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants/CommandMenuDefaultIcon';
import { styled } from '@linaria/react';
import { useContext } from 'react';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
@@ -18,6 +19,14 @@ import { Loader } from 'twenty-ui/feedback';
import { MenuItem } from 'twenty-ui/navigation';
import { type CommandMenuItemFieldsFragment } from '~/generated-metadata/graphql';
const StyledPreviewWrapper = styled.div`
cursor: not-allowed;
& * {
pointer-events: none;
}
`;
type CommandMenuItemRendererProps = {
item: CommandMenuItemFieldsFragment;
};
@@ -27,7 +36,8 @@ type CommandMenuItemButtonRendererProps = CommandMenuItemRendererProps;
const CommandMenuItemButtonRenderer = ({
item,
}: CommandMenuItemButtonRendererProps) => {
const { commandMenuContextApi } = useContext(CommandMenuContext);
const { commandMenuContextApi, isInPreviewMode } =
useContext(CommandMenuContext);
const { getIcon } = useIcons();
const { iconKey, label, shortLabel } = interpolateCommandMenuItemFields(
@@ -43,14 +53,19 @@ const CommandMenuItemButtonRenderer = ({
label,
});
const command = { key: item.id, label, shortLabel, Icon };
if (isInPreviewMode) {
return (
<StyledPreviewWrapper>
<CommandMenuButton command={command} />
</StyledPreviewWrapper>
);
}
return (
<CommandMenuButton
command={{
key: item.id,
label,
shortLabel,
Icon,
}}
command={command}
onClick={disabled ? undefined : handleClick}
disabled={disabled}
/>
@@ -2,6 +2,7 @@ import { PINNED_COMMAND_MENU_ITEMS_GAP } from '@/command-menu-item/display/const
import { commandMenuPinnedInlineLayoutState } from '@/command-menu-item/display/states/commandMenuPinnedInlineLayoutState';
import { getVisibleCommandMenuItemCountForContainerWidth } from '@/command-menu-item/display/utils/getVisibleCommandMenuItemCountForContainerWidth';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { isNumber } from '@sniptt/guards';
import { useCallback, useMemo } from 'react';
import { type CommandMenuItemFieldsFragment } from '~/generated-metadata/graphql';
@@ -25,18 +26,37 @@ export const usePinnedCommandMenuItemsInlineLayout = ({
[pinnedCommandMenuItems],
);
const hasKnownPinnedInlineLayout = useMemo(
() =>
commandMenuPinnedInlineLayout.containerWidth > 0 &&
pinnedCommandMenuItemKeysInDisplayOrder.every((commandMenuItemKey) =>
isNumber(
commandMenuPinnedInlineLayout.commandMenuItemWidthsByKey[
commandMenuItemKey
],
),
),
[commandMenuPinnedInlineLayout, pinnedCommandMenuItemKeysInDisplayOrder],
);
const visiblePinnedCommandMenuItemCount = useMemo(
() =>
getVisibleCommandMenuItemCountForContainerWidth({
commandMenuItemKeysInDisplayOrder:
pinnedCommandMenuItemKeysInDisplayOrder,
commandMenuItemWidthsByKey:
commandMenuPinnedInlineLayout.commandMenuItemWidthsByKey,
commandMenuItemsContainerWidth:
commandMenuPinnedInlineLayout.containerWidth,
commandMenuItemsGapWidth: PINNED_COMMAND_MENU_ITEMS_GAP,
}),
[commandMenuPinnedInlineLayout, pinnedCommandMenuItemKeysInDisplayOrder],
hasKnownPinnedInlineLayout
? getVisibleCommandMenuItemCountForContainerWidth({
commandMenuItemKeysInDisplayOrder:
pinnedCommandMenuItemKeysInDisplayOrder,
commandMenuItemWidthsByKey:
commandMenuPinnedInlineLayout.commandMenuItemWidthsByKey,
commandMenuItemsContainerWidth:
commandMenuPinnedInlineLayout.containerWidth,
commandMenuItemsGapWidth: PINNED_COMMAND_MENU_ITEMS_GAP,
})
: 0,
[
commandMenuPinnedInlineLayout,
hasKnownPinnedInlineLayout,
pinnedCommandMenuItemKeysInDisplayOrder,
],
);
const pinnedInlineCommandMenuItems = useMemo(