refactor: improve goView import and editor selection

This commit is contained in:
clawdbot 2026-01-28 10:08:18 +08:00
parent 01b7a3a722
commit 903b0da44a
5 changed files with 45 additions and 20 deletions

View File

@ -156,7 +156,11 @@ export function Canvas(props: CanvasProps) {
// (Left-click on background already clears unless additive via box-select.) // (Left-click on background already clears unless additive via box-select.)
} }
props.onOpenContextMenu({ clientX: e.clientX, clientY: e.clientY, worldX: p.x, worldY: p.y, targetId }); props.onOpenContextMenu(
targetId
? { kind: 'node', clientX: e.clientX, clientY: e.clientY, worldX: p.x, worldY: p.y, targetId }
: { kind: 'canvas', clientX: e.clientX, clientY: e.clientY, worldX: p.x, worldY: p.y },
);
}; };
const onBackgroundPointerDown = (e: React.PointerEvent) => { const onBackgroundPointerDown = (e: React.PointerEvent) => {

View File

@ -1,22 +1,32 @@
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { Typography } from 'antd'; import { Typography } from 'antd';
export type ContextMenuState = { export type ContextMenuState =
| {
kind: 'canvas';
clientX: number; clientX: number;
clientY: number; clientY: number;
worldX: number; worldX: number;
worldY: number; worldY: number;
targetId?: string; }
| {
kind: 'node';
clientX: number;
clientY: number;
worldX: number;
worldY: number;
targetId: string;
}; };
export function ContextMenu(props: { export function ContextMenu(props: {
state: ContextMenuState | null; state: ContextMenuState | null;
selectionIds: string[]; selectionIds: string[];
selectionAllLocked: boolean; selectionAllLocked: boolean;
selectionSomeLocked?: boolean; selectionSomeLocked: boolean;
selectionHasUnlocked: boolean;
selectionAllHidden: boolean; selectionAllHidden: boolean;
selectionSomeHidden?: boolean; selectionSomeHidden: boolean;
hasAnyNodes?: boolean; hasAnyNodes: boolean;
onClose: () => void; onClose: () => void;
onAddTextAt: (x: number, y: number) => void; onAddTextAt: (x: number, y: number) => void;
onSelectSingle?: (id?: string) => void; onSelectSingle?: (id?: string) => void;
@ -88,8 +98,10 @@ export function ContextMenu(props: {
if (!ctx || !position) return null; if (!ctx || !position) return null;
const hasSelection = props.selectionIds.length > 0; const hasSelection = props.selectionIds.length > 0;
const hasTarget = !!ctx.targetId; const canModifySelection = hasSelection && props.selectionHasUnlocked;
const targetInSelection = !!ctx.targetId && props.selectionIds.includes(ctx.targetId); const hasTarget = ctx.kind === 'node';
const targetId = hasTarget ? ctx.targetId : undefined;
const targetInSelection = !!targetId && props.selectionIds.includes(targetId);
const canSelectSingle = !!props.onSelectSingle; const canSelectSingle = !!props.onSelectSingle;
return ( return (
@ -124,7 +136,7 @@ export function ContextMenu(props: {
<MenuItem <MenuItem
label="Select Only" label="Select Only"
onClick={() => { onClick={() => {
props.onSelectSingle?.(ctx.targetId); props.onSelectSingle?.(targetId);
onClose(); onClose();
}} }}
/> />
@ -154,7 +166,7 @@ export function ContextMenu(props: {
<MenuItem <MenuItem
label="Duplicate" label="Duplicate"
disabled={!hasSelection || !props.onDuplicateSelected} disabled={!canModifySelection || !props.onDuplicateSelected}
onClick={() => { onClick={() => {
props.onDuplicateSelected?.(); props.onDuplicateSelected?.();
onClose(); onClose();
@ -212,7 +224,7 @@ export function ContextMenu(props: {
<MenuItem <MenuItem
label="Delete" label="Delete"
danger danger
disabled={!hasSelection || !props.onDeleteSelected} disabled={!canModifySelection || !props.onDeleteSelected}
onClick={() => { onClick={() => {
props.onDeleteSelected?.(); props.onDeleteSelected?.();
onClose(); onClose();

View File

@ -55,6 +55,7 @@ export function EditorApp() {
const selectionAllLocked = selection.length > 0 && selection.every((n) => n.locked); const selectionAllLocked = selection.length > 0 && selection.every((n) => n.locked);
const selectionSomeLocked = selection.length > 0 && selection.some((n) => n.locked) && !selectionAllLocked; const selectionSomeLocked = selection.length > 0 && selection.some((n) => n.locked) && !selectionAllLocked;
const selectionHasUnlocked = selection.length > 0 && selection.some((n) => !n.locked);
const selectionAllHidden = selection.length > 0 && selection.every((n) => n.hidden); const selectionAllHidden = selection.length > 0 && selection.every((n) => n.hidden);
const selectionSomeHidden = selection.length > 0 && selection.some((n) => n.hidden) && !selectionAllHidden; const selectionSomeHidden = selection.length > 0 && selection.some((n) => n.hidden) && !selectionAllHidden;
@ -475,6 +476,7 @@ export function EditorApp() {
} }
} }
setCtxMenu({ setCtxMenu({
kind: 'node',
clientX: e.clientX, clientX: e.clientX,
clientY: e.clientY, clientY: e.clientY,
worldX: node.rect.x + node.rect.w / 2, worldX: node.rect.x + node.rect.w / 2,
@ -512,6 +514,7 @@ export function EditorApp() {
selectionIds={state.selection.ids} selectionIds={state.selection.ids}
selectionAllLocked={selectionAllLocked} selectionAllLocked={selectionAllLocked}
selectionSomeLocked={selectionSomeLocked} selectionSomeLocked={selectionSomeLocked}
selectionHasUnlocked={selectionHasUnlocked}
selectionAllHidden={selectionAllHidden} selectionAllHidden={selectionAllHidden}
selectionSomeHidden={selectionSomeHidden} selectionSomeHidden={selectionSomeHidden}
hasAnyNodes={state.doc.screen.nodes.length > 0} hasAnyNodes={state.doc.screen.nodes.length > 0}

View File

@ -287,15 +287,19 @@ export function editorReducer(state: EditorRuntimeState, action: EditorAction):
case 'deleteSelected': { case 'deleteSelected': {
if (!state.selection.ids.length) return state; if (!state.selection.ids.length) return state;
const ids = new Set(state.selection.ids); const ids = new Set(state.selection.ids);
const deletableIds = new Set(
state.doc.screen.nodes.filter((n) => ids.has(n.id) && !n.locked).map((n) => n.id),
);
if (!deletableIds.size) return state;
return { return {
...historyPush(state), ...historyPush(state),
doc: { doc: {
screen: { screen: {
...state.doc.screen, ...state.doc.screen,
nodes: state.doc.screen.nodes.filter((n) => !ids.has(n.id)), nodes: state.doc.screen.nodes.filter((n) => !deletableIds.has(n.id)),
}, },
}, },
selection: { ids: [] }, selection: { ids: state.selection.ids.filter((id) => !deletableIds.has(id)) },
}; };
} }
@ -328,7 +332,7 @@ export function editorReducer(state: EditorRuntimeState, action: EditorAction):
const clones: WidgetNode[] = []; const clones: WidgetNode[] = [];
for (const n of state.doc.screen.nodes) { for (const n of state.doc.screen.nodes) {
if (!ids.has(n.id)) continue; if (!ids.has(n.id) || n.locked) continue;
const id = `${n.type}_${Date.now()}_${Math.random().toString(16).slice(2)}`; const id = `${n.type}_${Date.now()}_${Math.random().toString(16).slice(2)}`;
clones.push({ clones.push({
...n, ...n,

View File

@ -521,17 +521,19 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt
nodes: [], nodes: [],
}); });
const componentList = const componentListRaw =
(input as GoViewStorageLike).componentList ?? (input as GoViewStorageLike).componentList ??
(input as GoViewProjectLike).componentList ?? (input as GoViewProjectLike).componentList ??
(data?.componentList as GoViewComponentLike[] | undefined) ?? (data?.componentList as GoViewComponentLike[] | undefined) ??
(state?.componentList as GoViewComponentLike[] | undefined) ?? (state?.componentList as GoViewComponentLike[] | undefined) ??
(project?.componentList as GoViewComponentLike[] | undefined) ?? (project?.componentList as GoViewComponentLike[] | undefined) ??
[]; [];
const componentList = Array.isArray(componentListRaw) ? componentListRaw : [];
const nodes: Array<TextWidgetNode | ImageWidgetNode | IframeWidgetNode | VideoWidgetNode> = []; const nodes: Array<TextWidgetNode | ImageWidgetNode | IframeWidgetNode | VideoWidgetNode> = [];
for (const raw of componentList) { for (const raw of componentList) {
if (!raw || typeof raw !== 'object') continue;
const c = unwrapComponent(raw); const c = unwrapComponent(raw);
const option = normalizeOption(optionOf(c)); const option = normalizeOption(optionOf(c));