feat(dashboard): add revision-safe interactive widget grid
This commit is contained in:
@@ -13,17 +13,30 @@ export const WIDGETS = [
|
||||
] as const;
|
||||
|
||||
export type WidgetId = typeof WIDGETS[number]['id'];
|
||||
export type DashboardWidget = { id: WidgetId; visible: boolean; size: WidgetSize };
|
||||
export type DashboardLayout = { version: 1; revision: number; widgets: DashboardWidget[] };
|
||||
export type DashboardWidget = { id: WidgetId; visible: boolean; size: WidgetSize; x: number; y: number; w: number; h: number };
|
||||
export type DashboardLayout = { version: 2; revision: number; widgets: DashboardWidget[] };
|
||||
|
||||
export function arrangeDashboardWidgets(widgets: DashboardWidget[]): DashboardWidget[] {
|
||||
let x = 0, y = 0, rowHeight = 0;
|
||||
return widgets.map(widget => {
|
||||
if (!widget.visible) return { ...widget };
|
||||
if (x + widget.w > 12) { x = 0; y += rowHeight; rowHeight = 0; }
|
||||
const placed = { ...widget, x, y };
|
||||
x += widget.w;
|
||||
rowHeight = Math.max(rowHeight, widget.h);
|
||||
return placed;
|
||||
});
|
||||
}
|
||||
|
||||
export function defaultDashboardLayout(): DashboardLayout {
|
||||
return { version: 1, revision: 0, widgets: WIDGETS.map(w => ({ id: w.id, visible: true, size: w.defaultSize })) };
|
||||
return { version: 2, revision: 0, widgets: arrangeDashboardWidgets(WIDGETS.map(w => ({ id: w.id, visible: true, size: w.defaultSize,
|
||||
x: 0, y: 0, w: w.defaultSize === 'full' ? 12 : 6, h: w.id === 'cards' ? 5 : 8 }))) };
|
||||
}
|
||||
|
||||
export function normalizeDashboardLayout(input: unknown): DashboardLayout {
|
||||
if (!input || typeof input !== 'object') return defaultDashboardLayout();
|
||||
const value = input as Record<string, unknown>;
|
||||
if (value.version !== 1 || !Number.isSafeInteger(value.revision) || (value.revision as number) < 0 || !Array.isArray(value.widgets)) return defaultDashboardLayout();
|
||||
if ((value.version !== 1 && value.version !== 2) || !Number.isSafeInteger(value.revision) || (value.revision as number) < 0 || !Array.isArray(value.widgets)) return defaultDashboardLayout();
|
||||
const revision = value.revision as number;
|
||||
const widgets: DashboardWidget[] = [];
|
||||
const seen = new Set<WidgetId>();
|
||||
@@ -32,11 +45,17 @@ export function normalizeDashboardLayout(input: unknown): DashboardLayout {
|
||||
const definition = WIDGETS.find(w => w.id === item.id);
|
||||
if (!definition || seen.has(definition.id)) continue;
|
||||
seen.add(definition.id);
|
||||
const size: WidgetSize = (definition.sizes as readonly string[]).includes(item.size) ? item.size : definition.defaultSize;
|
||||
const valid = value.version === 2 && [item.x, item.y, item.w, item.h].every(Number.isInteger)
|
||||
&& item.x >= 0 && item.y >= 0 && item.w >= 4 && item.w <= 12 && item.h >= 4 && item.h <= 24
|
||||
&& item.x + item.w <= 12 && item.y + item.h <= 256 && (definition.id !== 'cards' || item.w === 12);
|
||||
const w = valid ? item.w : size === 'full' ? 12 : 6;
|
||||
widgets.push({ id: definition.id, visible: typeof item.visible === 'boolean' ? item.visible : revision === 0,
|
||||
size: (definition.sizes as readonly string[]).includes(item.size) ? item.size : definition.defaultSize });
|
||||
size: w === 12 ? 'full' : 'standard', x: valid ? item.x : 0, y: valid ? item.y : 0, w,
|
||||
h: valid ? item.h : definition.id === 'cards' ? 5 : 8 });
|
||||
}
|
||||
for (const definition of WIDGETS) {
|
||||
if (!seen.has(definition.id)) widgets.push({ id: definition.id, visible: revision === 0, size: definition.defaultSize });
|
||||
if (!seen.has(definition.id)) widgets.push({ ...defaultDashboardLayout().widgets.find(w => w.id === definition.id)!, visible: revision === 0 });
|
||||
}
|
||||
return { version: 1, revision, widgets };
|
||||
return { version: 2, revision, widgets: value.version === 1 ? arrangeDashboardWidgets(widgets) : widgets };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user