fix(editor): context menu selection parity
This commit is contained in:
parent
a2c44b9f1b
commit
2cab730bf9
@ -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 ? (
|
||||
<MenuItem
|
||||
label="Select All"
|
||||
onClick={() => {
|
||||
@ -188,9 +205,9 @@ export function ContextMenu(props: {
|
||||
|
||||
<MenuItem
|
||||
label={
|
||||
props.selectionAllLocked
|
||||
selectionAllLocked
|
||||
? 'Unlock'
|
||||
: props.selectionSomeLocked
|
||||
: selectionSomeLocked
|
||||
? 'Toggle Lock'
|
||||
: 'Lock'
|
||||
}
|
||||
@ -202,9 +219,9 @@ export function ContextMenu(props: {
|
||||
/>
|
||||
<MenuItem
|
||||
label={
|
||||
props.selectionAllHidden
|
||||
selectionAllHidden
|
||||
? 'Show'
|
||||
: props.selectionSomeHidden
|
||||
: selectionSomeHidden
|
||||
? 'Toggle Visibility'
|
||||
: 'Hide'
|
||||
}
|
||||
|
||||
@ -58,19 +58,7 @@ export function EditorApp() {
|
||||
// Use the captured selectionIds from the context menu state to avoid a one-frame mismatch.
|
||||
const selectionIdsForMenu = ctxMenu?.selectionIds ?? state.selection.ids;
|
||||
|
||||
const selectionForMenu = useMemo(() => {
|
||||
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 <ContextMenu /> using the captured selection.)
|
||||
|
||||
const bounds = useMemo(
|
||||
() => ({ w: state.doc.screen.width, h: state.doc.screen.height }),
|
||||
@ -648,13 +636,8 @@ export function EditorApp() {
|
||||
|
||||
<ContextMenu
|
||||
state={ctxMenu}
|
||||
nodes={state.doc.screen.nodes}
|
||||
selectionIds={selectionIdsForMenu}
|
||||
selectionAllLocked={menuSelectionAllLocked}
|
||||
selectionSomeLocked={menuSelectionSomeLocked}
|
||||
selectionHasUnlocked={menuSelectionHasUnlocked}
|
||||
selectionAllHidden={menuSelectionAllHidden}
|
||||
selectionSomeHidden={menuSelectionSomeHidden}
|
||||
hasAnyNodes={state.doc.screen.nodes.length > 0}
|
||||
onClose={closeContextMenu}
|
||||
onAddTextAt={(x, y) => dispatch({ type: 'addTextAt', x, y })}
|
||||
onSelectSingle={(id) => dispatch({ type: 'selectSingle', id })}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user