diff --git a/packages/editor/src/editor/Canvas.tsx b/packages/editor/src/editor/Canvas.tsx index 1fd29a5..cebd95b 100644 --- a/packages/editor/src/editor/Canvas.tsx +++ b/packages/editor/src/editor/Canvas.tsx @@ -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; } diff --git a/packages/sdk/src/core/migrate.ts b/packages/sdk/src/core/migrate.ts index 7a43242..435a3ff 100644 --- a/packages/sdk/src/core/migrate.ts +++ b/packages/sdk/src/core/migrate.ts @@ -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; + const rect = (anyNode.rect ?? {}) as Record; + + 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) : {}) 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.