[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
@@ -1,8 +1,7 @@
import { PinnedCommandMenuItemButtons } from '@/command-menu-item/display/components/PinnedCommandMenuItemButtons';
import { RecordIndexCommandMenuDropdown } from '@/command-menu-item/components/RecordIndexCommandMenuDropdown';
import { CommandMenuContextProvider } from '@/command-menu-item/contexts/CommandMenuContextProvider';
import { PinnedCommandMenuItemButtons } from '@/command-menu-item/display/components/PinnedCommandMenuItemButtons';
import { CommandMenuItemEditButton } from '@/command-menu-item/edit/components/CommandMenuItemEditButton';
import { PinnedCommandMenuItemButtonsEditMode } from '@/command-menu-item/edit/components/PinnedCommandMenuItemButtonsEditMode';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
@@ -21,23 +20,18 @@ export const RecordIndexCommandMenu = () => {
isLayoutCustomizationModeEnabledState,
);
const showEditModePinnedButtons = isLayoutCustomizationModeEnabled;
return (
<>
{contextStoreCurrentObjectMetadataItemId && (
<>
{!isMobile && showEditModePinnedButtons ? (
<PinnedCommandMenuItemButtonsEditMode />
) : (
<CommandMenuContextProvider
isInSidePanel={false}
displayType="button"
containerType="index-page-header"
>
{!isMobile && <PinnedCommandMenuItemButtons />}
</CommandMenuContextProvider>
)}
<CommandMenuContextProvider
isInSidePanel={false}
displayType="button"
containerType="index-page-header"
isInPreviewMode={isLayoutCustomizationModeEnabled}
>
{!isMobile && <PinnedCommandMenuItemButtons />}
</CommandMenuContextProvider>
<CommandMenuContextProvider
isInSidePanel={false}
displayType="dropdownItem"
@@ -4,7 +4,9 @@ import { CommandMenuItemEditButton } from '@/command-menu-item/edit/components/C
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useIsMobile } from 'twenty-ui/utilities';
export const RecordShowCommandMenu = () => {
@@ -23,6 +25,9 @@ export const RecordShowCommandMenu = () => {
contextStoreTargetedRecordsRule.selectedRecordIds.length === 1;
const isMobile = useIsMobile();
const isLayoutCustomizationModeEnabled = useAtomStateValue(
isLayoutCustomizationModeEnabledState,
);
return (
<>
@@ -32,6 +37,7 @@ export const RecordShowCommandMenu = () => {
isInSidePanel={false}
displayType="button"
containerType="show-page-header"
isInPreviewMode={isLayoutCustomizationModeEnabled}
>
{!isMobile && <PinnedCommandMenuItemButtons />}
</CommandMenuContextProvider>
@@ -125,6 +125,7 @@ export const StandalonePageCommandMenu = () => {
containerType: 'standalone-page-header',
commandMenuItems: filteredCommandMenuItems,
commandMenuContextApi,
isInPreviewMode: false,
}}
>
{!isMobile && <PinnedCommandMenuItemButtons />}
@@ -45,6 +45,7 @@ const meta: Meta<typeof RecordIndexCommandMenuDropdown> = {
containerType: 'index-page-dropdown',
commandMenuItems: createMockCommandMenuItems(),
commandMenuContextApi: EMPTY_COMMAND_MENU_CONTEXT_API,
isInPreviewMode: false,
}}
>
<Story />
@@ -3,13 +3,13 @@ import { Provider as JotaiProvider } from 'jotai';
import { userEvent, within } from 'storybook/test';
import { RecordPageSidePanelCommandMenuDropdown } from '@/command-menu-item/components/RecordPageSidePanelCommandMenuDropdown';
import { EMPTY_COMMAND_MENU_CONTEXT_API } from '@/command-menu-item/constants/EmptyCommandMenuContextApi';
import { CommandMenuContext } from '@/command-menu-item/contexts/CommandMenuContext';
import { createMockCommandMenuItems } from '@/command-menu-item/mock/command-menu-items.mock';
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { EMPTY_COMMAND_MENU_CONTEXT_API } from '@/command-menu-item/constants/EmptyCommandMenuContextApi';
import { ComponentDecorator, RouterDecorator } from 'twenty-ui/testing';
import { ContextStoreDecorator } from '~/testing/decorators/ContextStoreDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
@@ -44,6 +44,7 @@ const meta: Meta<typeof RecordPageSidePanelCommandMenuDropdown> = {
...EMPTY_COMMAND_MENU_CONTEXT_API,
isInSidePanel: true,
},
isInPreviewMode: false,
}}
>
<Story />