From 2cab730bf95f3ba0ff3f908c743df636cd848bf4 Mon Sep 17 00:00:00 2001 From: clawdbot Date: Thu, 29 Jan 2026 00:35:01 +0800 Subject: [PATCH] fix(editor): context menu selection parity --- packages/editor/src/editor/ContextMenu.tsx | 41 +++++++++++++++------- packages/editor/src/editor/EditorApp.tsx | 21 ++--------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/packages/editor/src/editor/ContextMenu.tsx b/packages/editor/src/editor/ContextMenu.tsx index 81cb712..590a5df 100644 --- a/packages/editor/src/editor/ContextMenu.tsx +++ b/packages/editor/src/editor/ContextMenu.tsx @@ -1,5 +1,6 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { Typography } from 'antd'; +import type { WidgetNode } from '@astralview/sdk'; import { selectionKeyOf } from './selection'; @@ -26,13 +27,8 @@ export type ContextMenuState = export function ContextMenu(props: { state: ContextMenuState | null; + nodes: WidgetNode[]; selectionIds: string[]; - selectionAllLocked: boolean; - selectionSomeLocked: boolean; - selectionHasUnlocked: boolean; - selectionAllHidden: boolean; - selectionSomeHidden: boolean; - hasAnyNodes: boolean; onClose: () => void; onAddTextAt: (x: number, y: number) => void; onSelectSingle?: (id?: string) => void; @@ -110,8 +106,29 @@ export function ContextMenu(props: { // This prevents transient "disabled" menu items right after right-click selecting. const selectionIds = selectionInSync ? props.selectionIds : ctx.selectionIds; + const nodesById = new Map(props.nodes.map((n) => [n.id, n] as const)); + const hasAnyNodes = props.nodes.length > 0; + + const selectedNodes = selectionIds + .map((id) => nodesById.get(id)) + .filter((n): n is WidgetNode => !!n); + const hasSelection = selectionIds.length > 0; - const canModifySelection = hasSelection && props.selectionHasUnlocked; + const hasAnyResolved = selectedNodes.length > 0; + + // If selection contains unknown ids (shouldn't happen, but can during transient updates), + // treat them as "locked" so we don't enable destructive actions accidentally. + const selectionAllLocked = + hasSelection && (hasAnyResolved ? selectedNodes.every((n) => !!n.locked) : true); + const selectionSomeLocked = hasAnyResolved ? selectedNodes.some((n) => !!n.locked) : hasSelection; + const selectionHasUnlocked = hasAnyResolved ? selectedNodes.some((n) => !n.locked) : false; + + const selectionAllHidden = + hasSelection && (hasAnyResolved ? selectedNodes.every((n) => !!n.hidden) : false); + const selectionSomeHidden = hasAnyResolved ? selectedNodes.some((n) => !!n.hidden) : false; + + const canModifySelection = hasSelection && selectionHasUnlocked; + const hasTarget = ctx.kind === 'node'; const targetId = hasTarget ? ctx.targetId : undefined; const targetInSelection = !!targetId && selectionIds.includes(targetId); @@ -165,7 +182,7 @@ export function ContextMenu(props: { /> ) : null} - {!hasTarget && props.hasAnyNodes && props.onSelectAll ? ( + {!hasTarget && hasAnyNodes && props.onSelectAll ? ( { @@ -188,9 +205,9 @@ export function ContextMenu(props: { { - const ids = new Set(selectionIdsForMenu); - return state.doc.screen.nodes.filter((n) => ids.has(n.id)); - }, [selectionIdsForMenu, state.doc.screen.nodes]); - - const menuSelectionAllLocked = selectionForMenu.length > 0 && selectionForMenu.every((n) => n.locked); - const menuSelectionSomeLocked = - selectionForMenu.length > 0 && selectionForMenu.some((n) => n.locked) && !menuSelectionAllLocked; - const menuSelectionHasUnlocked = selectionForMenu.length > 0 && selectionForMenu.some((n) => !n.locked); - - const menuSelectionAllHidden = selectionForMenu.length > 0 && selectionForMenu.every((n) => n.hidden); - const menuSelectionSomeHidden = - selectionForMenu.length > 0 && selectionForMenu.some((n) => n.hidden) && !menuSelectionAllHidden; + // (Context menu selection flags are computed inside using the captured selection.) const bounds = useMemo( () => ({ w: state.doc.screen.width, h: state.doc.screen.height }), @@ -648,13 +636,8 @@ export function EditorApp() { 0} onClose={closeContextMenu} onAddTextAt={(x, y) => dispatch({ type: 'addTextAt', x, y })} onSelectSingle={(id) => dispatch({ type: 'selectSingle', id })}