feat(260929-dmx): Widget-Raster horizontal doppelt so fein (48 Spalten, Version 3)
- COLS lg 48 / md 40 / sm 24 / xs 16 / xxs 4, Zeilenhoehe und Abstand unveraendert - Migration stufenweise: v1->v2 wie bisher, v2->v3 nur x/w/minW/maxW x2 - Rueckfallwerte fuer Widgets ohne Eintrag auf 8 Einheiten Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -194,7 +194,7 @@ describe('DashboardGrid', () => {
|
|||||||
expect(document.querySelector('.widget-drag-handle')).toBeInTheDocument();
|
expect(document.querySelector('.widget-drag-handle')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('quick-260916-bwo Test 4: Grid-Props — 24/20/12/8/2 Spalten, rowHeight 20, margin 12, Breakpoints unveraendert, containerPadding folgt dem margin', async () => {
|
it('quick-260916-bwo Test 4: Grid-Props — 48/40/24/16/4 Spalten (quick-260929-dmx), rowHeight 20, margin 12, Breakpoints unveraendert, containerPadding folgt dem margin', async () => {
|
||||||
captured.props = null;
|
captured.props = null;
|
||||||
const { DashboardGrid } = await import('./dashboard-grid');
|
const { DashboardGrid } = await import('./dashboard-grid');
|
||||||
render(
|
render(
|
||||||
@@ -208,7 +208,7 @@ describe('DashboardGrid', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(captured.props).not.toBeNull();
|
expect(captured.props).not.toBeNull();
|
||||||
expect(captured.props?.cols).toEqual({ lg: 24, md: 20, sm: 12, xs: 8, xxs: 2 });
|
expect(captured.props?.cols).toEqual({ lg: 48, md: 40, sm: 24, xs: 16, xxs: 4 });
|
||||||
expect(captured.props?.rowHeight).toBe(20);
|
expect(captured.props?.rowHeight).toBe(20);
|
||||||
expect(captured.props?.margin).toEqual([12, 12]);
|
expect(captured.props?.margin).toEqual([12, 12]);
|
||||||
expect(captured.props?.breakpoints).toEqual({ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 });
|
expect(captured.props?.breakpoints).toEqual({ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 });
|
||||||
@@ -503,9 +503,9 @@ describe('RESIZE_AXIS_FALLBACK (Design „Mosaik“, Runde 3)', () => {
|
|||||||
|
|
||||||
describe('centeringOffset (Design „Mosaik“, Runde 3)', () => {
|
describe('centeringOffset (Design „Mosaik“, Runde 3)', () => {
|
||||||
it('stellt belegte Spalten mittig und laesst eine volle Anordnung stehen', () => {
|
it('stellt belegte Spalten mittig und laesst eine volle Anordnung stehen', () => {
|
||||||
// 24 Spalten auf 1212 px: Spalte 38 px + 12 px Abstand = 50 px je Spalte.
|
// 48 Spalten auf 1212 px: Spalte 13 px + 12 px Abstand = 25 px je Spalte.
|
||||||
expect(centeringOffset([{ x: 0, w: 12 }], 24, 1212)).toBe(300);
|
expect(centeringOffset([{ x: 0, w: 24 }], 48, 1212)).toBe(300);
|
||||||
expect(centeringOffset([{ x: 12, w: 12 }], 24, 1212)).toBe(0);
|
expect(centeringOffset([{ x: 24, w: 24 }], 48, 1212)).toBe(0);
|
||||||
expect(centeringOffset([], 24, 1212)).toBe(0);
|
expect(centeringOffset([], 48, 1212)).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,9 +14,16 @@ import { WidgetWrapper } from './widgets/widget-wrapper';
|
|||||||
// quick-260916-bwo: Raster doppelt so fein wie zuvor (24 statt 12 Spalten,
|
// quick-260916-bwo: Raster doppelt so fein wie zuvor (24 statt 12 Spalten,
|
||||||
// 20 statt 40 px Zeilenhoehe, 8 statt 16 px Abstand). Gespeicherte Anordnungen
|
// 20 statt 40 px Zeilenhoehe, 8 statt 16 px Abstand). Gespeicherte Anordnungen
|
||||||
// in alten Einheiten werden in `@/lib/grid-layout-migration` einmalig
|
// in alten Einheiten werden in `@/lib/grid-layout-migration` einmalig
|
||||||
// umgerechnet und mit `__gridVersion: 2` markiert. BREAKPOINTS unveraendert.
|
// umgerechnet und mit `__gridVersion` markiert. BREAKPOINTS unveraendert.
|
||||||
|
//
|
||||||
|
// quick-260929-dmx (Version 3): Raster in der BREITE noch einmal doppelt so
|
||||||
|
// fein (48 statt 24 Spalten am lg-Breakpoint, uebrige Breakpoints ebenso
|
||||||
|
// verdoppelt). Zeilenhoehe (20 px) und Abstand (12 px) bleiben. Alle
|
||||||
|
// Breitenwerte (x, w, minW, maxW, WIDGET_CONSTRAINTS, Rueckfallwerte) sind
|
||||||
|
// damit in 48stel-Einheiten; gespeicherte Anordnungen rechnet die Migration
|
||||||
|
// einmalig um, jedes Widget behaelt seine Bildschirmgroesse und -position.
|
||||||
const BREAKPOINTS = { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 };
|
const BREAKPOINTS = { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 };
|
||||||
const COLS = { lg: 24, md: 20, sm: 12, xs: 8, xxs: 2 };
|
const COLS = { lg: 48, md: 40, sm: 24, xs: 16, xxs: 4 };
|
||||||
|
|
||||||
// quick-260916-dyv: Ziehen zuverlaessig.
|
// quick-260916-dyv: Ziehen zuverlaessig.
|
||||||
// - Griff ist die GANZE Karte (`widget-drag-handle` in widget-wrapper.tsx).
|
// - Griff ist die GANZE Karte (`widget-drag-handle` in widget-wrapper.tsx).
|
||||||
@@ -379,10 +386,10 @@ export function DashboardGrid({
|
|||||||
...(effectiveLayouts.lg?.find((l) => l.i === widget.id) ?? {
|
...(effectiveLayouts.lg?.find((l) => l.i === widget.id) ?? {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
w: constraints?.defaultW ?? 4,
|
w: constraints?.defaultW ?? 8,
|
||||||
h: constraints?.defaultH ?? 4,
|
h: constraints?.defaultH ?? 4,
|
||||||
}),
|
}),
|
||||||
minW: constraints?.minW ?? 4,
|
minW: constraints?.minW ?? 8,
|
||||||
minH: constraints?.minH ?? 4,
|
minH: constraints?.minH ?? 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ import {
|
|||||||
} from './grid-layout-migration';
|
} from './grid-layout-migration';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* grid-layout-migration.test — NEU (quick-260916-bwo, feineres Dashboard-Raster).
|
* grid-layout-migration.test (quick-260916-bwo, erweitert quick-260929-dmx).
|
||||||
*
|
*
|
||||||
* Sieben Tests fuer die einmalige Umrechnung gespeicherter Anordnungen von
|
* Tests fuer die stufenweise Umrechnung gespeicherter Anordnungen:
|
||||||
* den alten Einheiten (12 Spalten / 40 px) in die neuen (24 Spalten / 20 px).
|
* v1 (12 Spalten / 40 px) -> v2 (24 / 20 px, alles x2) -> v3 (48 Spalten,
|
||||||
* Kern ist T-BWO-02: die Verdopplung darf GENAU EINMAL geschehen. Der Marker
|
* nur Breite x2). Kern ist T-BWO-02: jede Stufe darf GENAU EINMAL geschehen.
|
||||||
* `__gridVersion` steht dafuer im gespeicherten JSON, nie im Zustand — Test 4
|
* Der Marker `__gridVersion` steht dafuer im gespeicherten JSON, nie im
|
||||||
* (Idempotenz) wird rot, sobald jemand die Marker-Pruefung entfernt.
|
* Zustand — der Idempotenz-Test wird rot, sobald jemand die Marker-Pruefung
|
||||||
|
* entfernt.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function altLayouts() {
|
function altLayouts() {
|
||||||
@@ -31,14 +32,14 @@ function altLayouts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('grid-layout-migration (quick-260916-bwo)', () => {
|
describe('grid-layout-migration (quick-260916-bwo)', () => {
|
||||||
it('Test 1: alte Anordnung ohne Marker -> alle Positionen und Groessen x2, migrated true, kein Marker im Ergebnis', () => {
|
it('Test 1: v1-Anordnung ohne Marker -> beide Stufen: x/w/minW/maxW x4, y/h/minH/maxH x2, migrated true, kein Marker im Ergebnis', () => {
|
||||||
const { layouts, migrated } = migrateGridLayouts(altLayouts());
|
const { layouts, migrated } = migrateGridLayouts(altLayouts());
|
||||||
|
|
||||||
expect(layouts.lg[0]).toEqual({
|
expect(layouts.lg[0]).toEqual({
|
||||||
i: 'a', x: 2, y: 4, w: 4, h: 6, minW: 4, minH: 4, moved: false, static: false,
|
i: 'a', x: 4, y: 4, w: 8, h: 6, minW: 8, minH: 4, moved: false, static: false,
|
||||||
});
|
});
|
||||||
expect(layouts.lg[1]).toEqual({ i: 'b', x: 4, y: 0, w: 12, h: 4, maxW: 24, maxH: 16 });
|
expect(layouts.lg[1]).toEqual({ i: 'b', x: 8, y: 0, w: 24, h: 4, maxW: 48, maxH: 16 });
|
||||||
expect(layouts.md[0]).toEqual({ i: 'a', x: 0, y: 0, w: 4, h: 4 });
|
expect(layouts.md[0]).toEqual({ i: 'a', x: 0, y: 0, w: 8, h: 4 });
|
||||||
expect(layouts.sm).toEqual([]);
|
expect(layouts.sm).toEqual([]);
|
||||||
expect(layouts.xs).toEqual([]);
|
expect(layouts.xs).toEqual([]);
|
||||||
expect(layouts.xxs).toEqual([]);
|
expect(layouts.xxs).toEqual([]);
|
||||||
@@ -47,15 +48,44 @@ describe('grid-layout-migration (quick-260916-bwo)', () => {
|
|||||||
expect(GRID_SCALE_FACTOR).toBe(2);
|
expect(GRID_SCALE_FACTOR).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Test 2: markierte Anordnung (__gridVersion 2) bleibt unveraendert, migrated false', () => {
|
it('Test 2: v2-Anordnung (__gridVersion 2) -> nur die zweite Stufe: x/w/minW/maxW x2, y/h/minH/maxH unveraendert, migrated true', () => {
|
||||||
const alt = altLayouts();
|
const alt = altLayouts();
|
||||||
const { layouts, migrated } = migrateGridLayouts({ ...alt, __gridVersion: 2 });
|
const { layouts, migrated } = migrateGridLayouts({ ...alt, __gridVersion: 2 });
|
||||||
|
|
||||||
|
expect(layouts.lg[0]).toEqual({
|
||||||
|
i: 'a', x: 2, y: 2, w: 4, h: 3, minW: 4, minH: 2, moved: false, static: false,
|
||||||
|
});
|
||||||
|
expect(layouts.lg[1]).toEqual({ i: 'b', x: 4, y: 0, w: 12, h: 2, maxW: 24, maxH: 8 });
|
||||||
|
expect(layouts.md[0]).toEqual({ i: 'a', x: 0, y: 0, w: 4, h: 2 });
|
||||||
|
expect(migrated).toBe(true);
|
||||||
|
expect(Object.keys(layouts)).not.toContain(GRID_VERSION_KEY);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Test 2b: v3-Anordnung (__gridVersion 3) bleibt unveraendert, migrated false', () => {
|
||||||
|
const alt = altLayouts();
|
||||||
|
const { layouts, migrated } = migrateGridLayouts({ ...alt, __gridVersion: 3 });
|
||||||
|
|
||||||
expect(layouts).toEqual(alt);
|
expect(layouts).toEqual(alt);
|
||||||
expect(migrated).toBe(false);
|
expect(migrated).toBe(false);
|
||||||
expect(Object.keys(layouts)).not.toContain(GRID_VERSION_KEY);
|
expect(Object.keys(layouts)).not.toContain(GRID_VERSION_KEY);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Test 2c: v1 und v2 -> v3 ergeben dieselbe Anordnung, wenn die v2-Eingabe die v1-Eingabe x2 ist', () => {
|
||||||
|
const v1 = altLayouts();
|
||||||
|
const v2 = {
|
||||||
|
lg: [
|
||||||
|
{ i: 'a', x: 2, y: 4, w: 4, h: 6, minW: 4, minH: 4, moved: false, static: false },
|
||||||
|
{ i: 'b', x: 4, y: 0, w: 12, h: 4, maxW: 24, maxH: 16 },
|
||||||
|
],
|
||||||
|
md: [{ i: 'a', x: 0, y: 0, w: 4, h: 4 }],
|
||||||
|
sm: [],
|
||||||
|
xs: [],
|
||||||
|
xxs: [],
|
||||||
|
__gridVersion: 2,
|
||||||
|
};
|
||||||
|
expect(migrateGridLayouts(v2).layouts).toEqual(migrateGridLayouts(v1).layouts);
|
||||||
|
});
|
||||||
|
|
||||||
it('Test 3: leere Anordnung bleibt leer, migrated false (kein Speichern noetig)', () => {
|
it('Test 3: leere Anordnung bleibt leer, migrated false (kein Speichern noetig)', () => {
|
||||||
const empty = { lg: [], md: [], sm: [], xs: [], xxs: [] };
|
const empty = { lg: [], md: [], sm: [], xs: [], xxs: [] };
|
||||||
const r1 = migrateGridLayouts(empty);
|
const r1 = migrateGridLayouts(empty);
|
||||||
@@ -73,6 +103,14 @@ describe('grid-layout-migration (quick-260916-bwo)', () => {
|
|||||||
|
|
||||||
expect(twice.layouts).toEqual(once.layouts);
|
expect(twice.layouts).toEqual(once.layouts);
|
||||||
expect(twice.migrated).toBe(false);
|
expect(twice.migrated).toBe(false);
|
||||||
|
|
||||||
|
// Auch die einstufige v2 -> v3 ist idempotent, und der Marker 3 wird geschrieben.
|
||||||
|
const onceV2 = migrateGridLayouts({ ...altLayouts(), __gridVersion: 2 });
|
||||||
|
const marked = withGridVersion(onceV2.layouts);
|
||||||
|
expect(marked[GRID_VERSION_KEY]).toBe(3);
|
||||||
|
const twiceV2 = migrateGridLayouts(marked);
|
||||||
|
expect(twiceV2.layouts).toEqual(onceV2.layouts);
|
||||||
|
expect(twiceV2.migrated).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Test 5: withGridVersion haengt den Marker an, ohne die Eingabe zu veraendern', () => {
|
it('Test 5: withGridVersion haengt den Marker an, ohne die Eingabe zu veraendern', () => {
|
||||||
@@ -80,30 +118,30 @@ describe('grid-layout-migration (quick-260916-bwo)', () => {
|
|||||||
const marked = withGridVersion(layouts);
|
const marked = withGridVersion(layouts);
|
||||||
|
|
||||||
expect(marked[GRID_VERSION_KEY]).toBe(GRID_VERSION);
|
expect(marked[GRID_VERSION_KEY]).toBe(GRID_VERSION);
|
||||||
expect(GRID_VERSION).toBe(2);
|
expect(GRID_VERSION).toBe(3);
|
||||||
expect(marked.lg).toBe(layouts.lg);
|
expect(marked.lg).toBe(layouts.lg);
|
||||||
expect(marked.md).toBe(layouts.md);
|
expect(marked.md).toBe(layouts.md);
|
||||||
expect(Object.keys(layouts)).not.toContain(GRID_VERSION_KEY);
|
expect(Object.keys(layouts)).not.toContain(GRID_VERSION_KEY);
|
||||||
expect((layouts as Record<string, unknown>)[GRID_VERSION_KEY]).toBeUndefined();
|
expect((layouts as Record<string, unknown>)[GRID_VERSION_KEY]).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Test 6: Zukunft und Robustheit — Marker 3 bleibt, Zeichenketten-Marker zaehlt nicht, nicht-numerische Felder bleiben', () => {
|
it('Test 6: Zukunft und Robustheit — Marker 4 bleibt, Zeichenketten-Marker zaehlt nicht, nicht-numerische Felder bleiben', () => {
|
||||||
const alt = altLayouts();
|
const alt = altLayouts();
|
||||||
|
|
||||||
const future = migrateGridLayouts({ ...alt, __gridVersion: 3 });
|
const future = migrateGridLayouts({ ...alt, __gridVersion: 4 });
|
||||||
expect(future.layouts).toEqual(alt);
|
expect(future.layouts).toEqual(alt);
|
||||||
expect(future.migrated).toBe(false);
|
expect(future.migrated).toBe(false);
|
||||||
|
|
||||||
const stringMarker = migrateGridLayouts({ ...alt, __gridVersion: '2' });
|
const stringMarker = migrateGridLayouts({ ...alt, __gridVersion: '2' });
|
||||||
expect(stringMarker.migrated).toBe(true);
|
expect(stringMarker.migrated).toBe(true);
|
||||||
expect(stringMarker.layouts.lg[0].x).toBe(2);
|
expect(stringMarker.layouts.lg[0].x).toBe(4);
|
||||||
expect(Object.keys(stringMarker.layouts)).not.toContain(GRID_VERSION_KEY);
|
expect(Object.keys(stringMarker.layouts)).not.toContain(GRID_VERSION_KEY);
|
||||||
|
|
||||||
const odd = migrateGridLayouts({
|
const odd = migrateGridLayouts({
|
||||||
lg: [{ i: 'c', x: 'kaputt', y: 1, w: 2, h: 2, resizeHandles: ['se'], moved: true, static: false }],
|
lg: [{ i: 'c', x: 'kaputt', y: 1, w: 2, h: 2, resizeHandles: ['se'], moved: true, static: false }],
|
||||||
});
|
});
|
||||||
expect(odd.layouts.lg[0]).toEqual({
|
expect(odd.layouts.lg[0]).toEqual({
|
||||||
i: 'c', x: 'kaputt', y: 2, w: 4, h: 4, resizeHandles: ['se'], moved: true, static: false,
|
i: 'c', x: 'kaputt', y: 2, w: 8, h: 4, resizeHandles: ['se'], moved: true, static: false,
|
||||||
});
|
});
|
||||||
expect(Number.isNaN(odd.layouts.lg[0].x)).toBe(false);
|
expect(Number.isNaN(odd.layouts.lg[0].x)).toBe(false);
|
||||||
expect(odd.migrated).toBe(true);
|
expect(odd.migrated).toBe(true);
|
||||||
@@ -112,7 +150,7 @@ describe('grid-layout-migration (quick-260916-bwo)', () => {
|
|||||||
it('Test 7: Fremdwerte — Nicht-Arrays werden weggelassen, Nicht-Objekte liefern eine leere Anordnung', () => {
|
it('Test 7: Fremdwerte — Nicht-Arrays werden weggelassen, Nicht-Objekte liefern eine leere Anordnung', () => {
|
||||||
const r = migrateGridLayouts({ lg: 'kaputt', md: null, sm: [{ i: 'a', x: 1, y: 1, w: 1, h: 1 }] });
|
const r = migrateGridLayouts({ lg: 'kaputt', md: null, sm: [{ i: 'a', x: 1, y: 1, w: 1, h: 1 }] });
|
||||||
expect(Object.keys(r.layouts)).toEqual(['sm']);
|
expect(Object.keys(r.layouts)).toEqual(['sm']);
|
||||||
expect(r.layouts.sm[0]).toEqual({ i: 'a', x: 2, y: 2, w: 2, h: 2 });
|
expect(r.layouts.sm[0]).toEqual({ i: 'a', x: 4, y: 2, w: 4, h: 2 });
|
||||||
expect(r.migrated).toBe(true);
|
expect(r.migrated).toBe(true);
|
||||||
|
|
||||||
expect(migrateGridLayouts(null)).toEqual({ layouts: {}, migrated: false });
|
expect(migrateGridLayouts(null)).toEqual({ layouts: {}, migrated: false });
|
||||||
|
|||||||
@@ -1,23 +1,35 @@
|
|||||||
/**
|
/**
|
||||||
* grid-layout-migration — einmalige Umrechnung gespeicherter Dashboard-
|
* grid-layout-migration — einmalige, stufenweise Umrechnung gespeicherter
|
||||||
* Anordnungen in die feineren Raster-Einheiten (quick-260916-bwo).
|
* Dashboard-Anordnungen in die feineren Raster-Einheiten (quick-260916-bwo,
|
||||||
|
* quick-260929-dmx).
|
||||||
*
|
*
|
||||||
* Warum: Das Raster wurde von 12 Spalten / 40 px Zeilenhoehe auf 24 Spalten /
|
* Warum: Das Raster wurde zweimal feiner. Eine in ALTEN Einheiten gespeicherte
|
||||||
* 20 px verdoppelt (`dashboard-grid.tsx`). Eine in ALTEN Einheiten gespeicherte
|
* Anordnung wuerde im neuen Raster kleiner und an anderer Position erscheinen.
|
||||||
* Anordnung wuerde im neuen Raster halb so gross und an der halben Position
|
* Deshalb wird jedes Element in jedem Breakpoint stufenweise umgerechnet,
|
||||||
* erscheinen. Deshalb werden `x, y, w, h` (und, falls vorhanden, `minW, minH,
|
* jede Stufe GENAU EINMAL:
|
||||||
* maxW, maxH`) jedes Elements in jedem Breakpoint GENAU EINMAL mit 2
|
|
||||||
* multipliziert.
|
|
||||||
*
|
*
|
||||||
* Marker: Damit die Verdopplung nur einmal geschieht, traegt das gespeicherte
|
* - Version 1 -> 2 (quick-260916-bwo): 12 Spalten / 40 px Zeilenhoehe wurden
|
||||||
* JSON den Schluessel `__gridVersion: 2`. Der Marker lebt NUR im persistierten
|
* 24 Spalten / 20 px. `x, y, w, h` (und, falls vorhanden, `minW, minH, maxW,
|
||||||
* JSON (Spalte `DashboardLayout.layouts`, Json, kein Schema noetig) — nie im
|
* maxH`) werden mit 2 multipliziert.
|
||||||
* Zustand des Stores, der mit `Object.keys` ueber die Breakpoints iteriert und
|
* - Version 2 -> 3 (quick-260929-dmx): NUR die Breite wurde noch einmal
|
||||||
* `.filter` auf jedem Wert aufruft (ein Zahlwert wuerde dort abstuerzen).
|
* verdoppelt (24 -> 48 Spalten am lg-Breakpoint, Zeilenhoehe und Abstand
|
||||||
* `migrateGridLayouts` entfernt den Marker beim Laden, `withGridVersion`
|
* unveraendert). Nur `x, w, minW, maxW` werden mit 2 multipliziert; `y, h,
|
||||||
* haengt ihn beim Speichern wieder an. Fehlt der Marker beim Speichern, wird
|
* minH, maxH` bleiben.
|
||||||
* beim naechsten Laden ERNEUT verdoppelt — deshalb muss JEDER Speichervorgang
|
*
|
||||||
* `withGridVersion` benutzen (T-BWO-02, Store-Tests pinnen das).
|
* 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))`
|
* Idempotenz: `migrateGridLayouts(withGridVersion(migrateGridLayouts(alt).layouts))`
|
||||||
* liefert dasselbe Ergebnis wie `migrateGridLayouts(alt)` mit `migrated: false`.
|
* liefert dasselbe Ergebnis wie `migrateGridLayouts(alt)` mit `migrated: false`.
|
||||||
@@ -28,7 +40,7 @@
|
|||||||
* React- oder Store-Abhaengigkeit.
|
* React- oder Store-Abhaengigkeit.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const GRID_VERSION = 2;
|
export const GRID_VERSION = 3;
|
||||||
export const GRID_VERSION_KEY = '__gridVersion';
|
export const GRID_VERSION_KEY = '__gridVersion';
|
||||||
export const GRID_SCALE_FACTOR = 2;
|
export const GRID_SCALE_FACTOR = 2;
|
||||||
|
|
||||||
@@ -43,7 +55,13 @@ export interface GridLayoutItem {
|
|||||||
|
|
||||||
export type GridLayouts = Record<string, GridLayoutItem[]>;
|
export type GridLayouts = Record<string, GridLayoutItem[]>;
|
||||||
|
|
||||||
const SCALED_FIELDS = ['x', 'y', 'w', 'h', 'minW', 'minH', 'maxW', 'maxH'] as const;
|
/** 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> {
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||||
@@ -62,7 +80,8 @@ export function migrateGridLayouts(raw: unknown): { layouts: GridLayouts; migrat
|
|||||||
const markerValue = raw[GRID_VERSION_KEY];
|
const markerValue = raw[GRID_VERSION_KEY];
|
||||||
// Nur eine Zahl ist ein Marker; alles andere (fehlend, Zeichenkette) zaehlt als alt.
|
// Nur eine Zahl ist ein Marker; alles andere (fehlend, Zeichenkette) zaehlt als alt.
|
||||||
const version = typeof markerValue === 'number' ? markerValue : 1;
|
const version = typeof markerValue === 'number' ? markerValue : 1;
|
||||||
const needsScaling = version < GRID_VERSION;
|
const steps = MIGRATION_STEPS.filter((step) => version <= step.from);
|
||||||
|
const needsScaling = steps.length > 0;
|
||||||
|
|
||||||
const layouts: GridLayouts = {};
|
const layouts: GridLayouts = {};
|
||||||
let migrated = false;
|
let migrated = false;
|
||||||
@@ -75,12 +94,14 @@ export function migrateGridLayouts(raw: unknown): { layouts: GridLayouts; migrat
|
|||||||
layouts[key] = value.map((item) => {
|
layouts[key] = value.map((item) => {
|
||||||
const copy = { ...(item as GridLayoutItem) };
|
const copy = { ...(item as GridLayoutItem) };
|
||||||
if (needsScaling) {
|
if (needsScaling) {
|
||||||
for (const field of SCALED_FIELDS) {
|
for (const step of steps) {
|
||||||
|
for (const field of step.fields) {
|
||||||
const n = copy[field];
|
const n = copy[field];
|
||||||
if (typeof n === 'number') {
|
if (typeof n === 'number') {
|
||||||
copy[field] = n * GRID_SCALE_FACTOR;
|
copy[field] = n * GRID_SCALE_FACTOR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
migrated = true;
|
migrated = true;
|
||||||
}
|
}
|
||||||
return copy;
|
return copy;
|
||||||
|
|||||||
@@ -74,12 +74,12 @@ describe('dashboard-store — einmalige Umrechnung mit Marker (quick-260916-bwo)
|
|||||||
await useDashboardStore.getState().loadDashboard();
|
await useDashboardStore.getState().loadDashboard();
|
||||||
|
|
||||||
const state = useDashboardStore.getState();
|
const state = useDashboardStore.getState();
|
||||||
expect(state.layouts.lg[0]).toEqual({ i: 'a', x: 2, y: 2, w: 4, h: 4 });
|
expect(state.layouts.lg[0]).toEqual({ i: 'a', x: 4, y: 2, w: 8, h: 4 });
|
||||||
expect(Object.keys(state.layouts)).not.toContain('__gridVersion');
|
expect(Object.keys(state.layouts)).not.toContain('__gridVersion');
|
||||||
expect(api.saveLayout).toHaveBeenCalledTimes(1);
|
expect(api.saveLayout).toHaveBeenCalledTimes(1);
|
||||||
expect(api.saveLayout).toHaveBeenCalledWith(
|
expect(api.saveLayout).toHaveBeenCalledWith(
|
||||||
'dash-1',
|
'dash-1',
|
||||||
expect.objectContaining({ __gridVersion: 2, lg: [{ i: 'a', x: 2, y: 2, w: 4, h: 4 }] }),
|
expect.objectContaining({ __gridVersion: 3, lg: [{ i: 'a', x: 4, y: 2, w: 8, h: 4 }] }),
|
||||||
);
|
);
|
||||||
expect(state.isDirty).toBe(false);
|
expect(state.isDirty).toBe(false);
|
||||||
expect(state.isLoading).toBe(false);
|
expect(state.isLoading).toBe(false);
|
||||||
@@ -88,7 +88,7 @@ describe('dashboard-store — einmalige Umrechnung mit Marker (quick-260916-bwo)
|
|||||||
|
|
||||||
it('Test 2: markierte Anordnung bleibt unveraendert, kein Speichern, kein Marker im Zustand', async () => {
|
it('Test 2: markierte Anordnung bleibt unveraendert, kein Speichern, kein Marker im Zustand', async () => {
|
||||||
vi.mocked(api.fetchLayout).mockResolvedValue({
|
vi.mocked(api.fetchLayout).mockResolvedValue({
|
||||||
lg: [{ i: 'a', x: 1, y: 1, w: 2, h: 2 }], md: [], sm: [], xs: [], xxs: [], __gridVersion: 2,
|
lg: [{ i: 'a', x: 1, y: 1, w: 2, h: 2 }], md: [], sm: [], xs: [], xxs: [], __gridVersion: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
await useDashboardStore.getState().loadDashboard();
|
await useDashboardStore.getState().loadDashboard();
|
||||||
@@ -121,7 +121,7 @@ describe('dashboard-store — einmalige Umrechnung mit Marker (quick-260916-bwo)
|
|||||||
expect(api.saveLayout).toHaveBeenCalledTimes(1);
|
expect(api.saveLayout).toHaveBeenCalledTimes(1);
|
||||||
expect(api.saveLayout).toHaveBeenCalledWith(
|
expect(api.saveLayout).toHaveBeenCalledWith(
|
||||||
'dash-1',
|
'dash-1',
|
||||||
expect.objectContaining({ __gridVersion: 2, ...layouts }),
|
expect.objectContaining({ __gridVersion: 3, ...layouts }),
|
||||||
);
|
);
|
||||||
expect(useDashboardStore.getState().isDirty).toBe(false);
|
expect(useDashboardStore.getState().isDirty).toBe(false);
|
||||||
expect(Object.keys(useDashboardStore.getState().layouts)).not.toContain('__gridVersion');
|
expect(Object.keys(useDashboardStore.getState().layouts)).not.toContain('__gridVersion');
|
||||||
@@ -137,7 +137,7 @@ describe('dashboard-store — einmalige Umrechnung mit Marker (quick-260916-bwo)
|
|||||||
await expect(useDashboardStore.getState().loadDashboard()).resolves.toBeUndefined();
|
await expect(useDashboardStore.getState().loadDashboard()).resolves.toBeUndefined();
|
||||||
|
|
||||||
const state = useDashboardStore.getState();
|
const state = useDashboardStore.getState();
|
||||||
expect(state.layouts.lg[0].x).toBe(2);
|
expect(state.layouts.lg[0].x).toBe(4);
|
||||||
expect(state.error).toBeNull();
|
expect(state.error).toBeNull();
|
||||||
expect(state.isLoading).toBe(false);
|
expect(state.isLoading).toBe(false);
|
||||||
expect(errorSpy).toHaveBeenCalledTimes(1);
|
expect(errorSpy).toHaveBeenCalledTimes(1);
|
||||||
@@ -213,7 +213,7 @@ describe('dashboard-store — Reiterwechsel (quick-260923-ad9, Task 3)', () => {
|
|||||||
await useDashboardStore.getState().loadDashboard();
|
await useDashboardStore.getState().loadDashboard();
|
||||||
|
|
||||||
vi.mocked(api.fetchLayout).mockResolvedValue({
|
vi.mocked(api.fetchLayout).mockResolvedValue({
|
||||||
lg: [{ i: 'only-on-dash-2', x: 0, y: 0, w: 2, h: 2 }], md: [], sm: [], xs: [], xxs: [], __gridVersion: 2,
|
lg: [{ i: 'only-on-dash-2', x: 0, y: 0, w: 2, h: 2 }], md: [], sm: [], xs: [], xxs: [], __gridVersion: 3,
|
||||||
});
|
});
|
||||||
vi.mocked(api.fetchWidgets).mockResolvedValue([{ id: 'w-on-dash-2', widgetType: 'clock', config: {} }]);
|
vi.mocked(api.fetchWidgets).mockResolvedValue([{ id: 'w-on-dash-2', widgetType: 'clock', config: {} }]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user