blog2video-test / frontend /src /utils /mergeLayoutSchemaDefaults.ts
Muhammad-Mehdi's picture
Merge branch 'develop' into fix/football
db2a73a
Raw
History Blame Contribute Delete
3.59 kB
/**
* Merge meta.json `layout_prop_schema[layout].defaults` under stored layoutProps,
* matching `backend/app/services/remotion.py` (`write_remotion_data`, ~983–1002).
*
* Keeps frontend preview / SceneEditModal in sync with the rendered MP4 for
* chart colors, axis captions, example tables, font sizes, etc.
*/
export type LayoutPropSchema = Record<string, { defaults?: Record<string, unknown> }>;
// Economist wordmark/dateline/teasers are LLM-authored content, never defaults —
// mirror `_ECONOMIST_CONTENT_SKIP_KEYS` in backend/app/services/remotion.py so the
// preview never injects the sample "The Brief" wordmark onto the cover / sign-off.
const ECONOMIST_CONTENT_SKIP_KEYS = new Set(["wordmark", "dateline", "teasers"]);
function resolveDefaultsForAspect(
defaults: Record<string, unknown>,
aspectRatio: string,
): Record<string, unknown> {
const ar = aspectRatio === "portrait" ? "portrait" : "landscape";
const resolved: Record<string, unknown> = {};
for (const [key, value] of Object.entries(defaults)) {
if (
value &&
typeof value === "object" &&
!Array.isArray(value) &&
"portrait" in value &&
"landscape" in value
) {
const responsive = value as { portrait?: unknown; landscape?: unknown };
resolved[key] = responsive[ar] ?? responsive.landscape;
} else {
resolved[key] = value;
}
}
return resolved;
}
/**
* Apply layout schema defaults under `layoutProps` (stored props win on conflict).
*/
export function mergeLayoutSchemaDefaults(
layoutProps: Record<string, unknown>,
layoutId: string | null | undefined,
schema: LayoutPropSchema | null | undefined,
aspectRatio = "landscape",
templateId?: string | null,
): Record<string, unknown> {
if (!layoutId || !schema || Object.keys(schema).length === 0) return layoutProps;
const defaults = schema[layoutId]?.defaults;
if (!defaults || Object.keys(defaults).length === 0) return layoutProps;
const resolved = resolveDefaultsForAspect(defaults, aspectRatio);
// Drop content-only defaults the renderer also refuses to inject, so the
// preview matches the MP4 (e.g. economist never auto-stamps a brand wordmark).
if (String(templateId ?? "").toLowerCase() === "economist") {
for (const key of ECONOMIST_CONTENT_SKIP_KEYS) delete resolved[key];
}
const merged = { ...resolved, ...layoutProps };
// Keep example chart data when stored chartTable has no rows (legacy partial merge).
const existingTable = layoutProps.chartTable;
const existingTableHasRows =
existingTable &&
typeof existingTable === "object" &&
Array.isArray((existingTable as { rows?: unknown }).rows) &&
((existingTable as { rows: unknown[] }).rows.length > 0);
if (!existingTableHasRows && resolved.chartTable && typeof resolved.chartTable === "object") {
merged.chartTable = resolved.chartTable;
}
const existingTickerTable = layoutProps.tickerTable;
const existingTickerTableHasRows =
existingTickerTable &&
typeof existingTickerTable === "object" &&
Array.isArray((existingTickerTable as { rows?: unknown }).rows) &&
((existingTickerTable as { rows: unknown[] }).rows.some((row) =>
Array.isArray(row) && row.some((cell) => String(cell ?? "").trim() !== ""),
));
if (!existingTickerTableHasRows && resolved.tickerTable && typeof resolved.tickerTable === "object") {
merged.tickerTable = resolved.tickerTable;
}
if (!("chartType" in layoutProps) && typeof resolved.chartType === "string") {
merged.chartType = resolved.chartType;
}
return merged;
}