af87c2c112
Je Reiter wird die Flaeche des ersten Desktop-Bildschirms als __canvas im Layout-JSON gespeichert (ohne API/DB-Aenderung, GRID_VERSION bleibt 3). Das Raster rendert in Leinwandbreite und wird per transform: scale(min(bw/cw, bh/ch)) eingepasst, Schrift eingeschlossen, ohne Scrollen; unter 768 px wie bisher. Ziehen/Groesse aendern unter Skalierung ueber eine eigene Positionsstrategie (createScaledStrategy aus react-grid-layout 2.2.3 rechnet den Rasterversatz falsch). Im Browser nachgewiesen: 1920x1080 -> 1366x768 (Faktor 0,66), Ziehen +200 px folgt der Maus, 700 px ohne Skalierung. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
178 lines
7.4 KiB
TypeScript
178 lines
7.4 KiB
TypeScript
/**
|
|
* grid-layout-migration — einmalige, stufenweise Umrechnung gespeicherter
|
|
* Dashboard-Anordnungen in die feineren Raster-Einheiten (quick-260916-bwo,
|
|
* quick-260929-dmx).
|
|
*
|
|
* Warum: Das Raster wurde zweimal feiner. Eine in ALTEN Einheiten gespeicherte
|
|
* Anordnung wuerde im neuen Raster kleiner und an anderer Position erscheinen.
|
|
* Deshalb wird jedes Element in jedem Breakpoint stufenweise umgerechnet,
|
|
* jede Stufe GENAU EINMAL:
|
|
*
|
|
* - Version 1 -> 2 (quick-260916-bwo): 12 Spalten / 40 px Zeilenhoehe wurden
|
|
* 24 Spalten / 20 px. `x, y, w, h` (und, falls vorhanden, `minW, minH, maxW,
|
|
* maxH`) werden mit 2 multipliziert.
|
|
* - Version 2 -> 3 (quick-260929-dmx): NUR die Breite wurde noch einmal
|
|
* verdoppelt (24 -> 48 Spalten am lg-Breakpoint, Zeilenhoehe und Abstand
|
|
* unveraendert). Nur `x, w, minW, maxW` werden mit 2 multipliziert; `y, h,
|
|
* minH, maxH` bleiben.
|
|
*
|
|
* Die Stufen sind kumulativ: eine Version-1-Anordnung durchlaeuft beide
|
|
* (x/w/minW/maxW also x4, y/h/minH/maxH x2), eine Version-2-Anordnung nur die
|
|
* zweite, eine Version-3-Anordnung keine.
|
|
*
|
|
* Marker: Damit jede Stufe nur einmal geschieht, traegt das gespeicherte JSON
|
|
* den Schluessel `__gridVersion` (aktuell 3). Der Marker lebt NUR im
|
|
* persistierten JSON (Spalte `DashboardLayout.layouts`, Json, kein Schema
|
|
* noetig) — nie im Zustand des Stores, der mit `Object.keys` ueber die
|
|
* Breakpoints iteriert und `.filter` auf jedem Wert aufruft (ein Zahlwert
|
|
* wuerde dort abstuerzen). `migrateGridLayouts` entfernt den Marker beim
|
|
* Laden, `withGridVersion` haengt ihn beim Speichern wieder an. Fehlt der
|
|
* Marker beim Speichern, wird beim naechsten Laden ERNEUT umgerechnet —
|
|
* deshalb muss JEDER Speichervorgang `withGridVersion` benutzen (T-BWO-02,
|
|
* Store-Tests pinnen das).
|
|
*
|
|
* Idempotenz: `migrateGridLayouts(withGridVersion(migrateGridLayouts(alt).layouts))`
|
|
* liefert dasselbe Ergebnis wie `migrateGridLayouts(alt)` mit `migrated: false`.
|
|
*
|
|
* Neuere Marke (quick-260930): Traegt das JSON eine HOEHERE Marke als
|
|
* `GRID_VERSION`, hat es ein neuerer Programmstand geschrieben (etwa ein
|
|
* frisch geladener Tab neben einem alten). Umgerechnet wird dann nichts, aber
|
|
* `newer: true` gemeldet — der Store darf diese Anordnung NIE speichern: er
|
|
* wuerde sie mit der alten Marke zurueckschreiben, und der neue Stand wuerde
|
|
* sie beim naechsten Laden ein zweites Mal skalieren.
|
|
*
|
|
* Leinwand (Bezugsgroesse, "wie ein Bild mitskalieren"): Zusaetzlich kann das
|
|
* JSON den Schluessel `__canvas: { w, h }` tragen — die verfuegbare Flaeche
|
|
* (Pixel), auf der die Anordnung eingerichtet wurde. Das Raster zeichnet
|
|
* sich auf JEDEM Desktop-Bildschirm mit genau dieser Breite und skaliert das
|
|
* Ergebnis per `transform: scale` in die dortige Flaeche (siehe
|
|
* dashboard-grid.tsx). Wie der Marker lebt `__canvas` nur im JSON, nicht in
|
|
* `layouts`: `migrateGridLayouts` liefert ihn getrennt als `canvas`,
|
|
* `withGridVersion(layouts, canvas)` schreibt ihn zurueck — JEDER
|
|
* Speicherpfad muss ihn mitgeben, sonst geht er verloren. Kein neuer
|
|
* `GRID_VERSION`: aeltere Programmstaende ueberspringen den Schluessel, weil
|
|
* sein Wert kein Array ist (`!Array.isArray(value)` unten) — und schreiben
|
|
* ihn beim Speichern nicht zurueck; dann wird er einfach neu erfasst.
|
|
*
|
|
* Ort: Frontend, weil die Raster-Einheiten Frontend-Konstanten sind, die API
|
|
* das JSON nur durchreicht (`@IsObject()`) und so kein Schreiben auf einem
|
|
* GET und keine Aenderung am API-Dienst noetig ist. Reine Funktionen ohne
|
|
* React- oder Store-Abhaengigkeit.
|
|
*/
|
|
|
|
export const GRID_VERSION = 3;
|
|
export const GRID_VERSION_KEY = '__gridVersion';
|
|
export const GRID_SCALE_FACTOR = 2;
|
|
export const GRID_CANVAS_KEY = '__canvas';
|
|
/** Unter dieser Breite gilt die Handy-/Schmalansicht, dort wird nicht skaliert. */
|
|
export const CANVAS_MIN_WIDTH = 768;
|
|
/** Kleinere Hoehen sind Messfehler (Fenster kurz zusammengeklappt o. ae.). */
|
|
export const CANVAS_MIN_HEIGHT = 200;
|
|
|
|
/** Bezugsflaeche in Pixeln, auf der die Anordnung eingerichtet wurde. */
|
|
export interface GridCanvas {
|
|
w: number;
|
|
h: number;
|
|
}
|
|
|
|
export interface GridLayoutItem {
|
|
i: string;
|
|
x: number;
|
|
y: number;
|
|
w: number;
|
|
h: number;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
export type GridLayouts = Record<string, GridLayoutItem[]>;
|
|
|
|
/** Felder je Umrechnungsstufe; `from` ist die Version, von der die Stufe ausgeht. */
|
|
const MIGRATION_STEPS: ReadonlyArray<{ from: number; fields: readonly string[] }> = [
|
|
// 1 -> 2 (quick-260916-bwo): Spalten UND Zeilen verdoppelt.
|
|
{ from: 1, fields: ['x', 'y', 'w', 'h', 'minW', 'minH', 'maxW', 'maxH'] },
|
|
// 2 -> 3 (quick-260929-dmx): nur die Breite verdoppelt.
|
|
{ from: 2, fields: ['x', 'w', 'minW', 'maxW'] },
|
|
];
|
|
|
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
/**
|
|
* Prueft eine Leinwand: endliche Zahlen, w >= CANVAS_MIN_WIDTH,
|
|
* h >= CANVAS_MIN_HEIGHT. Alles andere -> null (wird dann neu erfasst).
|
|
*/
|
|
export function parseGridCanvas(value: unknown): GridCanvas | null {
|
|
if (!isPlainObject(value)) return null;
|
|
const { w, h } = value;
|
|
if (typeof w !== 'number' || typeof h !== 'number') return null;
|
|
if (!Number.isFinite(w) || !Number.isFinite(h)) return null;
|
|
if (w < CANVAS_MIN_WIDTH || h < CANVAS_MIN_HEIGHT) return null;
|
|
return { w, h };
|
|
}
|
|
|
|
/**
|
|
* Rechnet eine rohe (aus der API geladene) Anordnung in die aktuellen
|
|
* Raster-Einheiten um. Liefert die Anordnung OHNE Marker und die Angabe, ob
|
|
* etwas verdoppelt wurde (dann muss der Aufrufer sofort mit Marker speichern).
|
|
*/
|
|
export function migrateGridLayouts(raw: unknown): {
|
|
layouts: GridLayouts;
|
|
migrated: boolean;
|
|
/** Marke hoeher als `GRID_VERSION` — Anordnung stammt von einem neueren Programmstand. */
|
|
newer: boolean;
|
|
/** Gueltige Leinwand aus `__canvas`, sonst null. */
|
|
canvas: GridCanvas | null;
|
|
} {
|
|
if (!isPlainObject(raw)) {
|
|
return { layouts: {}, migrated: false, newer: false, canvas: null };
|
|
}
|
|
|
|
const markerValue = raw[GRID_VERSION_KEY];
|
|
// Nur eine Zahl ist ein Marker; alles andere (fehlend, Zeichenkette) zaehlt als alt.
|
|
const version = typeof markerValue === 'number' ? markerValue : 1;
|
|
const newer = version > GRID_VERSION;
|
|
const steps = MIGRATION_STEPS.filter((step) => version <= step.from);
|
|
const needsScaling = steps.length > 0;
|
|
|
|
const layouts: GridLayouts = {};
|
|
let migrated = false;
|
|
|
|
for (const key of Object.keys(raw)) {
|
|
if (key === GRID_VERSION_KEY || key === GRID_CANVAS_KEY) continue;
|
|
const value = raw[key];
|
|
if (!Array.isArray(value)) continue;
|
|
|
|
layouts[key] = value.map((item) => {
|
|
const copy = { ...(item as GridLayoutItem) };
|
|
if (needsScaling) {
|
|
for (const step of steps) {
|
|
for (const field of step.fields) {
|
|
const n = copy[field];
|
|
if (typeof n === 'number') {
|
|
copy[field] = n * GRID_SCALE_FACTOR;
|
|
}
|
|
}
|
|
}
|
|
migrated = true;
|
|
}
|
|
return copy;
|
|
});
|
|
}
|
|
|
|
return { layouts, migrated, newer, canvas: parseGridCanvas(raw[GRID_CANVAS_KEY]) };
|
|
}
|
|
|
|
/**
|
|
* Haengt den Marker (und, falls vorhanden, die Leinwand) fuer das Speichern
|
|
* an, ohne die Eingabe zu veraendern.
|
|
*/
|
|
export function withGridVersion(
|
|
layouts: GridLayouts,
|
|
canvas?: GridCanvas | null,
|
|
): Record<string, unknown> {
|
|
const result: Record<string, unknown> = { ...layouts, [GRID_VERSION_KEY]: GRID_VERSION };
|
|
if (canvas) result[GRID_CANVAS_KEY] = { w: canvas.w, h: canvas.h };
|
|
return result;
|
|
}
|