refactor: improve selection parity and sanitize imports
This commit is contained in:
parent
e58b240778
commit
06868fed42
@ -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;
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user