[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:
+22
-7
@@ -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}
|
||||
/>
|
||||
|
||||
+30
-10
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user