refactor: improve selection parity and sanitize imports

This commit is contained in:
clawdbot 2026-01-28 08:16:05 +08:00
parent e58b240778
commit 06868fed42
2 changed files with 73 additions and 6 deletions

View File

@ -297,12 +297,9 @@ export function Canvas(props: CanvasProps) {
// If the node is already in the current selection, keep the selection as-is.
// This matches goView-ish multi-selection behavior (drag moves the whole selection).
// Even if the clicked node is locked, we still allow starting a drag so other
// unlocked nodes in the selection can move (the reducer skips locked/hidden nodes).
if (props.selectionIds.includes(node.id)) {
if (node.locked) {
// Locked nodes are selectable, but should not start a move drag.
// Keep the current selection intact for multi-select parity.
return;
}
props.onBeginMove(e);
return;
}

View File

@ -1,5 +1,73 @@
import { ASTRALVIEW_SCHEMA_VERSION, type Screen } from './schema';
/**
* Placeholder for future schema migrations.
* Keep it pure and versioned.
*/
function toNumber(v: unknown, fallback: number): number {
return typeof v === 'number' && Number.isFinite(v) ? v : fallback;
}
function asString(v: unknown): string {
return typeof v === 'string' ? v : '';
}
function sanitizeScreenV1(input: Screen): Screen {
const allowedTypes = new Set(['text', 'image', 'iframe', 'video']);
const nodes = Array.isArray(input.nodes) ? input.nodes : [];
const sanitizedNodes = nodes
.filter((n): n is Screen['nodes'][number] => !!n && typeof n === 'object')
.filter((n) => allowedTypes.has((n as { type?: unknown }).type as string))
.map((n, i) => {
const anyNode = n as unknown as Record<string, unknown>;
const rect = (anyNode.rect ?? {}) as Record<string, unknown>;
const base = {
...n,
id: asString(anyNode.id) || `import_${Date.now()}_${i}`,
rect: {
x: toNumber(rect.x, 0),
y: toNumber(rect.y, 0),
w: toNumber(rect.w, 100),
h: toNumber(rect.h, 60),
},
};
// Ensure required props exist for media widgets to prevent runtime crashes.
const props = (base as unknown as { props?: unknown }).props;
const propsObj = (props && typeof props === 'object' ? (props as Record<string, unknown>) : {}) as Record<
string,
unknown
>;
if (base.type === 'image') {
return { ...base, props: { ...propsObj, src: asString(propsObj.src) } };
}
if (base.type === 'iframe') {
return { ...base, props: { ...propsObj, src: asString(propsObj.src) } };
}
if (base.type === 'video') {
return { ...base, props: { ...propsObj, src: asString(propsObj.src) } };
}
if (base.type === 'text') {
return { ...base, props: { ...propsObj, text: asString(propsObj.text) || '' } };
}
return base;
});
return {
...input,
id: input.id || `av_${Date.now()}`,
name: input.name || 'Untitled Screen',
width: toNumber(input.width, 1920),
height: toNumber(input.height, 1080),
nodes: sanitizedNodes,
};
}
/**
* Placeholder for future schema migrations.
* Keep it pure and versioned.
@ -11,7 +79,9 @@ export function migrateScreen(input: unknown): Screen {
}
const version = (s as Screen).version;
if (version === ASTRALVIEW_SCHEMA_VERSION) {
return s as Screen;
// Even for the current version, we defensively sanitize to avoid runtime
// "assertNever" crashes when users import malformed JSON.
return sanitizeScreenV1(s as Screen);
}
// Future: apply incremental migrations.