Compare commits
4 Commits
acd3c7a05f
...
a435a30c34
| Author | SHA1 | Date | |
|---|---|---|---|
| a435a30c34 | |||
| 46ebb4e7ce | |||
| 9c9e1420fe | |||
| 97744b59cd |
@@ -479,6 +479,7 @@ Gerettet aus `.continue-here.md`. Relevant fuer die noch offenen Live-Tests.
|
||||
| 260928-ujj | **Design Mosaik uebernommen + Hintergrund pro Benutzer.** Merge design/mosaik (76d17fe, inkl. RESIZE_AXIS_FALLBACK), Spalte `User.dashboardBackground` JSONB (Migration 20260928120000), `PATCH /users/me/dashboard-background` mit `parseDashboardBackground` aus packages/shared (Preset-Liste, imageId nur UUID), Web liest aus Sitzung, alte localStorage-Wahl einmalig uebernommen. Browser: Duenen gewaehlt, DB-Zeile gesetzt, nach localStorage-Loeschen weiter sichtbar. api 1462, web 952 gruen; Freigabe als 1.5.0. | 2026-09-28 | 9fa0a3f,0aaa152,cb45d26 | [260928-ujj-design-mosaik-uebernehmen-und-als-1-5-0-](./quick/260928-ujj-design-mosaik-uebernehmen-und-als-1-5-0-/) |
|
||||
| 260929-9wc | **Eigene Module (nur lokal, nicht gepusht).** Modell `CustomModule` + Migration 20260929120000 mit RLS (Muster ProxmoxServer), `/custom-modules` (GET alle Angemeldeten, POST/PATCH/DELETE Admin, nur https ohne Zugangsdaten), `MODULE_CATEGORIES` in packages/shared, Seitenleisten-Eintrag unter gewaehlter Kategorie, Rahmen-Seite `/modules/custom/[id]` mit XFRAME_SANDBOX + no-referrer + „In neuem Tab öffnen“, Verwaltung `/admin/custom-modules`, Zugriffsklassifikation 61/224/6. Gruppen-Beschraenkung zurueckgestellt (ModuleGrant haengt an Module). api 1495, web 992 gruen; Browser dunkel 9 Schritte bestanden. | 2026-09-29 | b9d87be,e7fc4de,e48c0de | [260929-9wc-eigene-module-admin-legt-seitenleisten-e](./quick/260929-9wc-eigene-module-admin-legt-seitenleisten-e/) |
|
||||
| 260929-d37 | **Desktop-App nur einmal starten.** User-Meldung Windows 11: beim Systemstart zwei Instanzen/zwei Tray-Symbole. `tauri-plugin-single-instance` 2.4.5 als erstes Plugin, zweiter Start ruft `show_main_window` (neuer Helper, ersetzt 3 Kopien) und beendet sich. cargo build/test (44)/clippy gruen. Windows-Pruefung offen (VM 8233 oder User-PC nach naechster Desktop-Version). | 2026-09-29 | c0b145a,0751198 | [260929-d37-desktop-client-nur-einmal-starten-single](./quick/260929-d37-desktop-client-nur-einmal-starten-single/) |
|
||||
| 260929-dmx | **Widget-Raster horizontal feiner + Kalender schmaler.** COLS lg 48/md 40/sm 24/xs 16/xxs 4, GRID_VERSION 3 (v2->v3 nur x/w/minW/maxW x2), alle minW/defaultW x2, Kalender minW 8 (~250 px). Browser: Anordnung pixelgleich, Kalender bis 252 px, Schritt 33 px. Auch: Hover-Anheben der Widgets entfernt (acd3c7a, Nutzerwunsch). | 2026-09-29 | 97744b5,9c9e142,46ebb4e | [260929-dmx-widget-raster-horizontal-feiner-48-spalt](./quick/260929-dmx-widget-raster-horizontal-feiner-48-spalt/) |
|
||||
|
||||
## Deferred Items
|
||||
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
---
|
||||
quick_id: 260929-dmx
|
||||
type: quick
|
||||
wave: 1
|
||||
autonomous: true
|
||||
---
|
||||
|
||||
# Quick 260929-dmx: Widget-Raster horizontal feiner (48 Spalten), Kalender schmaler
|
||||
|
||||
## User requests (29.09.2026)
|
||||
|
||||
1. "das kalender widget soll in der breite schmäler gemacht werden können."
|
||||
2. "und mache das widget raster horizontal etwas feiner."
|
||||
|
||||
## Measurements (orchestrator, browser, lg breakpoint, grid width 1593 px, margin 12)
|
||||
|
||||
- Today: 24 cols → one width unit ≈ 66 px. Calendar minW 6 ≈ 383 px, default 8 ≈ 515 px.
|
||||
- Calendar rendered at 251 px: fully usable (month grid, header "September 2026", event list truncates titles cleanly).
|
||||
- Calendar at 185 px: header clipped, event titles reduced to one letter → too narrow.
|
||||
- Target: calendar minimum ≈ 250 px.
|
||||
|
||||
## Decision (locked)
|
||||
|
||||
Double the HORIZONTAL resolution only: `COLS = { lg: 48, md: 40, sm: 24, xs: 16, xxs: 4 }` in
|
||||
`apps/web/src/components/dashboard/dashboard-grid.tsx`. Row height (20) and margin (12) unchanged.
|
||||
Every existing widget keeps its exact on-screen size and position.
|
||||
|
||||
## Task 1: Grid version 3 (horizontal x2) with migration
|
||||
|
||||
- files: apps/web/src/lib/grid-layout-migration.ts (+ test), apps/web/src/components/dashboard/dashboard-grid.tsx (+ test), apps/web/src/lib/stores/dashboard-store.ts (only if needed)
|
||||
- action:
|
||||
- Bump `GRID_VERSION` to 3. Migration becomes stepwise and cumulative:
|
||||
v1 → v2: existing behavior (x,y,w,h,minW,minH,maxW,maxH × 2).
|
||||
v2 → v3: NEW, horizontal only: x, w, minW, maxW × 2 (y, h, minH, maxH unchanged).
|
||||
So a v1 layout gets both steps, a v2 layout only the second, a v3 layout nothing. Keep idempotence and marker semantics (marker only in persisted JSON). Update the file header comment (German, same style) to document v3.
|
||||
- `COLS` as above. Check every other place that depends on column count or widget width units:
|
||||
centering offset, `RESIZE_AXIS_FALLBACK`, `breakpointFor`, default positions when adding a widget
|
||||
(`dashboard-grid.tsx` ~380: `defaultW ?? 4`, `minW ?? 4` fallbacks → 8), empty-dashboard suggestions,
|
||||
any layout templates/seed data in apps/web or apps/api (grep `defaultW`, `w:` in dashboard code,
|
||||
`layouts` defaults in apps/api/src/dashboard). Anything expressed in width units gets × 2.
|
||||
- Tests: extend grid-layout-migration tests (v1→v3, v2→v3, v3 untouched, idempotence, marker 3 written), update dashboard-grid tests pinning cols.
|
||||
- verify: `pnpm --filter web exec vitest run src/lib src/components/dashboard` green.
|
||||
|
||||
## Task 2: Widget width constraints in new units
|
||||
|
||||
- files: apps/web/src/components/dashboard/widget-registry.tsx (+ test)
|
||||
- action: In `WIDGET_CONSTRAINTS` double every `minW` and `defaultW` (same physical size as before),
|
||||
EXCEPT calendar: `minW: 8` (≈ 251 px at lg — the measured usable minimum), `defaultW: 16` (unchanged size).
|
||||
minH/defaultH unchanged. Update the comment above calendar (German): narrower on user request 29.09., 8 of 48 ≈ 250 px measured usable.
|
||||
Existing layouts: the existing override logic in dashboard-grid (quick-260916-dyv: stored minW/minH replaced by constants in every breakpoint) must pick up the new calendar minW so existing calendars can be shrunk — verify that path with a test.
|
||||
- verify: web tests green; `pnpm turbo run type-check lint --filter web` green; biome warnings for web not above baseline 55.
|
||||
|
||||
## Task 3: CHANGELOG, rebuild, commit
|
||||
|
||||
- CHANGELOG.md under `## Unveröffentlicht` → `### Geändert` (section exists) add plain-German bullets (app text uses "Sie"):
|
||||
- Dashboard: Das Raster ist in der Breite doppelt so fein – Widgets lassen sich in kleineren Schritten breiter oder schmaler ziehen und genauer platzieren. Bestehende Anordnungen bleiben unverändert.
|
||||
- Dashboard: Das Kalender-Widget lässt sich deutlich schmaler ziehen als bisher.
|
||||
- Rebuild local stack: `docker compose up -d --build web` (plain `up` does not rebuild).
|
||||
- Commits per task, messages end with `Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>`.
|
||||
- NEVER git push (orchestrator pushes after browser check).
|
||||
- Browser check is done by the orchestrator (resize calendar to minimum, existing layout unchanged after migration, marker 3 persisted).
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
---
|
||||
quick_id: 260929-dmx
|
||||
status: complete
|
||||
commits: 3
|
||||
plan_head_before: acd3c7a05f9f01e2f5cf8ebe9aed66d8c789a055
|
||||
plan_head_after: 46ebb4e7ceafa1dc782514e487ac820166279f48
|
||||
completed: 2026-09-29
|
||||
actuals:
|
||||
tasks: 3
|
||||
commits: 3
|
||||
---
|
||||
|
||||
# Quick 260929-dmx: Widget-Raster horizontal 48 Spalten, Kalender schmaler
|
||||
|
||||
Grid version 3: horizontal resolution doubled (COLS lg 48 / md 40 / sm 24 / xs 16 / xxs 4), row height 20 and margin 12 unchanged. Stored layouts are migrated stepwise, so every existing widget keeps its on-screen size and position. Calendar minimum is now 8 of 48 columns (about 250 px at lg).
|
||||
|
||||
## Commits
|
||||
|
||||
- 97744b5 feat: grid v3 with stepwise migration, COLS, fallbacks
|
||||
- 9c9e142 feat: widget constraints in 48-column units, calendar minW 8
|
||||
- 46ebb4e docs(changelog): two bullets under "Unveröffentlicht / Geändert"
|
||||
|
||||
## Every place where width units changed
|
||||
|
||||
1. `apps/web/src/lib/grid-layout-migration.ts`: `GRID_VERSION` 2 to 3. Migration is now a step table: v1 to v2 scales x, y, w, h, minW, minH, maxW, maxH by 2; v2 to v3 scales only x, w, minW, maxW by 2. A v1 layout gets both steps, a v2 layout only the second, a v3 layout nothing. Marker semantics unchanged (marker only in the persisted JSON, stripped on load, re-added by `withGridVersion`). Header comment updated.
|
||||
2. `apps/web/src/components/dashboard/dashboard-grid.tsx`:
|
||||
- `COLS` is `{ lg: 48, md: 40, sm: 24, xs: 16, xxs: 4 }`.
|
||||
- Fallback for a widget without a layout entry: `defaultW ?? 8` and `minW ?? 8` (was 4/4). The `?? 4` fallbacks for height stay.
|
||||
- Comment block above BREAKPOINTS/COLS extended.
|
||||
3. `apps/web/src/components/dashboard/widget-registry.tsx` (`WIDGET_CONSTRAINTS`, minW/defaultW doubled, heights untouched):
|
||||
- clock 4/8
|
||||
- search 12/24
|
||||
- calendar minW 8, defaultW 16. Calendar is the only one that is not a plain doubling for minW: 8 instead of 12.
|
||||
- note 8/12
|
||||
- calculator 6/12
|
||||
- favorites 2/12
|
||||
- stopwatch 8/12
|
||||
- picture-frame 8/16
|
||||
- xframe 8/24
|
||||
- proxmox 6/16
|
||||
- Comments updated, German, including the calendar note (narrower on user request 29.09., 8 of 48 is about 250 px measured usable).
|
||||
4. `dashboard-store.ts` needed no code change. `addWidget` reads `defaultW` from `WIDGET_CONSTRAINTS`, so new widgets are placed in the new units automatically. Load and save already route through `migrateGridLayouts` and `withGridVersion`.
|
||||
5. `centeringOffset` needed no code change. It takes `cols` as a parameter and gets the new `COLS[breakpoint]`.
|
||||
6. `breakpointFor` needed no change. It depends only on BREAKPOINTS, not on the column count.
|
||||
|
||||
The existing override in `applyConstraintMinima` (quick-260916-dyv) already replaces the stored minW/minH from the constants in every breakpoint, so existing calendars pick up minW 8 with no code change there. This is verified by a new test.
|
||||
|
||||
## Tests
|
||||
|
||||
- `grid-layout-migration.test.ts`: v1 to v3 (x/w/minW/maxW times 4, y/h/minH/maxH times 2), v2 to v3, v3 untouched, v1 result equals v2 result, idempotence for both paths with marker 3 written, future marker 4 untouched, string marker, foreign values.
|
||||
- `dashboard-store.test.ts`: marker 3, new expected values, new-widget default of 8 wide.
|
||||
- `dashboard-grid.test.tsx`: COLS pin, `centeringOffset` with 48 columns, data-grid fallbacks, minima overrides, new Test 9c (existing calendar with stored minW 12 gets minW 8 in every breakpoint, w 6 raised to 8, w 16 kept).
|
||||
- `widget-registry.test.tsx`: constraints table.
|
||||
- Verification: `pnpm --filter web exec vitest run src/lib src/components/dashboard` green (513). The full web suite is green (995). Type-check for `@tessera/web` is green. Biome shows 55 warnings, equal to the baseline. Note: the turbo filter name is `@tessera/web`, not `web`.
|
||||
|
||||
## Rebuild
|
||||
|
||||
`docker compose up -d --build web` ran, and the web container is up. Browser check is left to the orchestrator (calendar at minimum, existing layout unchanged after migration, marker 3 persisted).
|
||||
|
||||
## Found but deliberately left
|
||||
|
||||
- `apps/api/src/dashboard/dashboard.service.spec.ts:834` stores `__gridVersion: 2` as a passthrough fixture. The API only passes the JSON through, so the value is arbitrary, and I did not touch it.
|
||||
- `RESIZE_AXIS_FALLBACK` and its tests use abstract grid numbers and are unit-independent, so nothing was changed. Its resize logic has no column dependency.
|
||||
- Historical comments that mention "24 Spalten" (the quick-260922-vdk explanation in `dashboard-grid.tsx`, the quick-260916-bwo test description, the bwo header text) describe past states and were left. The vdk comment's numbers (24 columns, 50 px) describe the old bug, not the current state.
|
||||
- Widget internals (calendar, favorites, calculator) use pixel-based or container-based layout, not grid units, so no change was needed.
|
||||
- md/sm/xs/xxs columns are doubled proportionally with lg. Only lg was measured, and I did not check the smaller breakpoints in a browser.
|
||||
- API/`seed`: no default layouts or seed data with width units exist in apps/api (empty defaults `{ lg: [], ... }` only).
|
||||
- A brand-new empty v2 layout is not re-saved with marker 3 on load (`migrated` stays false for empty layouts, existing behaviour). The marker gets written on the next save.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None. The plan was executed as written. The SUMMARY, STATE, PLAN and ROADMAP files were not committed, as instructed.
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
Commits 97744b5, 9c9e142, 46ebb4e exist. Changed files exist. Working tree contains only the untracked quick-task directory.
|
||||
|
||||
## Browser-Pruefung (Orchestrator, 29.09., dunkel, lg 1888 px)
|
||||
|
||||
- Bestehende Anordnung nach Migration pixelgenau gleich (6 Widgets, left/top/width/height vor und nach identisch); DB: `__gridVersion` 3, lg-w verdoppelt.
|
||||
- Kalender im Bearbeitungsmodus nach links gezogen: stoppt bei 252 px (vorher Minimum 383 px), Monatsraster, Kopf und Terminliste sauber lesbar.
|
||||
- Schrittweite beim Ziehen 33 px (284/317/350), vorher 66 px.
|
||||
- Kalender danach wieder auf 515 px gezogen, Testzustand zurueckgesetzt.
|
||||
- Nicht im Browser geprueft: kleinere Breakpoints (md/sm/xs/xxs).
|
||||
@@ -11,6 +11,8 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T
|
||||
### Geändert
|
||||
|
||||
- Dashboard: Die Widgets bleiben beim Darüberfahren mit der Maus ruhig stehen, sie heben sich nicht mehr an.
|
||||
- Dashboard: Das Raster ist in der Breite doppelt so fein – Widgets lassen sich in kleineren Schritten breiter oder schmaler ziehen und genauer platzieren. Bestehende Anordnungen bleiben unverändert.
|
||||
- Dashboard: Das Kalender-Widget lässt sich deutlich schmaler ziehen als bisher.
|
||||
|
||||
### Behoben
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ describe('DashboardGrid', () => {
|
||||
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;
|
||||
const { DashboardGrid } = await import('./dashboard-grid');
|
||||
render(
|
||||
@@ -208,7 +208,7 @@ describe('DashboardGrid', () => {
|
||||
);
|
||||
|
||||
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?.margin).toEqual([12, 12]);
|
||||
expect(captured.props?.breakpoints).toEqual({ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 });
|
||||
@@ -233,8 +233,9 @@ describe('DashboardGrid', () => {
|
||||
const first = children[0];
|
||||
expect(isValidElement(first)).toBe(true);
|
||||
const props = (first as React.ReactElement<Record<string, unknown>>).props;
|
||||
// quick-260916-dyv: Uhr-Minimum 2x2 (vorher verdoppelt 4x4), Vorgabe 4x4 unveraendert.
|
||||
expect(props['data-grid']).toEqual({ x: 0, y: 0, w: 4, h: 4, minW: 2, minH: 2 });
|
||||
// quick-260916-dyv: Uhr-Minimum 2 Zeilen hoch; quick-260929-dmx: Breiten im
|
||||
// 48er-Raster verdoppelt (Minimum 4, Vorgabe 8), Hoehen unveraendert.
|
||||
expect(props['data-grid']).toEqual({ x: 0, y: 0, w: 8, h: 4, minW: 4, minH: 2 });
|
||||
});
|
||||
|
||||
it('quick-260916-dyv Test 9: gespeicherte minW/minH werden in JEDEM Breakpoint aus WIDGET_CONSTRAINTS ueberschrieben, x/y/w/h bleiben, unbekannte Typen und Schluessel unveraendert', async () => {
|
||||
@@ -265,9 +266,9 @@ describe('DashboardGrid', () => {
|
||||
|
||||
const passed = captured.props?.layouts as Record<string, Array<Record<string, unknown>>>;
|
||||
expect(Object.keys(passed)).toEqual(Object.keys(layouts));
|
||||
expect(passed.lg[0]).toEqual({ i: 'inst-1', x: 2, y: 4, w: 6, h: 6, minW: 2, minH: 2 });
|
||||
expect(passed.md[0]).toEqual({ i: 'inst-1', x: 0, y: 0, w: 4, h: 4, minW: 2, minH: 2 });
|
||||
expect(passed.md[0].minW).toBe(2);
|
||||
expect(passed.lg[0]).toEqual({ i: 'inst-1', x: 2, y: 4, w: 6, h: 6, minW: 4, minH: 2 });
|
||||
expect(passed.md[0]).toEqual({ i: 'inst-1', x: 0, y: 0, w: 4, h: 4, minW: 4, minH: 2 });
|
||||
expect(passed.md[0].minW).toBe(4);
|
||||
// Unbekannter Typ: Eintrag bleibt woertlich erhalten (kein Absturz, T-DYV-01).
|
||||
expect(passed.lg[1]).toEqual({ i: 'inst-x', x: 10, y: 0, w: 5, h: 5, minW: 7, minH: 7 });
|
||||
expect(passed.sm).toEqual([]);
|
||||
@@ -278,9 +279,9 @@ describe('DashboardGrid', () => {
|
||||
const { DashboardGrid } = await import('./dashboard-grid');
|
||||
const layouts = {
|
||||
lg: [
|
||||
// Rechner: gespeichert mit h 8 (bwo-Minimum), neues Minimum 3x9 -> h wird 9, w 6 bleibt.
|
||||
// Rechner: gespeichert mit h 8 (bwo-Minimum), neues Minimum 6x10 -> h wird 10, w 6 bleibt.
|
||||
{ i: 'calc-1', x: 0, y: 0, w: 6, h: 8, minW: 4, minH: 8 },
|
||||
// Suche: gespeichert mit w 4 (unter minW 6) -> w wird 6, h 4 bleibt.
|
||||
// Suche: gespeichert mit w 4 (unter minW 12) -> w wird 12, h 4 bleibt.
|
||||
{ i: 'srch-1', x: 6, y: 0, w: 4, h: 4, minW: 6, minH: 4 },
|
||||
],
|
||||
md: [{ i: 'calc-1', x: 0, y: 0, w: 3, h: 7, minW: 4, minH: 8 }],
|
||||
@@ -302,15 +303,53 @@ describe('DashboardGrid', () => {
|
||||
);
|
||||
|
||||
const passed = captured.props?.layouts as Record<string, Array<Record<string, unknown>>>;
|
||||
expect(passed.lg[0]).toEqual({ i: 'calc-1', x: 0, y: 0, w: 6, h: 10, minW: 3, minH: 10 });
|
||||
expect(passed.lg[1]).toEqual({ i: 'srch-1', x: 6, y: 0, w: 6, h: 4, minW: 6, minH: 2 });
|
||||
// Jeder Breakpoint: md-Rechner 3x7 -> 3x10 (w schon = minW, h angehoben).
|
||||
expect(passed.md[0]).toEqual({ i: 'calc-1', x: 0, y: 0, w: 3, h: 10, minW: 3, minH: 10 });
|
||||
expect(passed.lg[0]).toEqual({ i: 'calc-1', x: 0, y: 0, w: 6, h: 10, minW: 6, minH: 10 });
|
||||
expect(passed.lg[1]).toEqual({ i: 'srch-1', x: 6, y: 0, w: 12, h: 4, minW: 12, minH: 2 });
|
||||
// Jeder Breakpoint: md-Rechner 3x7 -> 6x10 (w und h auf das Minimum angehoben).
|
||||
expect(passed.md[0]).toEqual({ i: 'calc-1', x: 0, y: 0, w: 6, h: 10, minW: 6, minH: 10 });
|
||||
// Eingabe unveraendert (keine Mutation des Store-Objekts).
|
||||
expect(layouts.lg[0].h).toBe(8);
|
||||
expect(layouts.lg[0].minH).toBe(8);
|
||||
});
|
||||
|
||||
it('quick-260929-dmx Test 9c: bestehender Kalender (migrierte Werte minW 12, w 16) bekommt minW 8 in JEDEM Breakpoint und kann schmaler gezogen werden; zu schmale Breite wird auf 8 angehoben', async () => {
|
||||
captured.props = null;
|
||||
const { DashboardGrid } = await import('./dashboard-grid');
|
||||
// So sieht ein aus v2 (minW 6 x2) migrierter Kalender im Zustand aus.
|
||||
const layouts = {
|
||||
lg: [
|
||||
{ i: 'cal-1', x: 4, y: 0, w: 16, h: 16, minW: 12, minH: 8 },
|
||||
{ i: 'cal-2', x: 24, y: 0, w: 6, h: 16, minW: 12, minH: 8 },
|
||||
],
|
||||
md: [{ i: 'cal-1', x: 0, y: 0, w: 16, h: 16, minW: 12, minH: 8 }],
|
||||
sm: [],
|
||||
xs: [],
|
||||
xxs: [],
|
||||
};
|
||||
render(
|
||||
<DashboardGrid
|
||||
layouts={layouts}
|
||||
widgets={[
|
||||
{ id: 'cal-1', widgetType: 'calendar', config: {} },
|
||||
{ id: 'cal-2', widgetType: 'calendar', config: {} },
|
||||
]}
|
||||
isEditMode
|
||||
onLayoutChange={vi.fn()}
|
||||
onRemoveWidget={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const passed = captured.props?.layouts as Record<string, Array<Record<string, unknown>>>;
|
||||
// Breite bleibt (Bildschirmgroesse unveraendert), nur das Minimum sinkt.
|
||||
expect(passed.lg[0]).toEqual({ i: 'cal-1', x: 4, y: 0, w: 16, h: 16, minW: 8, minH: 8 });
|
||||
expect(passed.md[0]).toEqual({ i: 'cal-1', x: 0, y: 0, w: 16, h: 16, minW: 8, minH: 8 });
|
||||
expect(passed.lg[1]).toEqual({ i: 'cal-2', x: 24, y: 0, w: 8, h: 16, minW: 8, minH: 8 });
|
||||
// Auch der data-grid-Wert der Kinder traegt das neue Minimum.
|
||||
const children = Children.toArray(captured.props?.children as React.ReactNode);
|
||||
const grid = (children[0] as React.ReactElement<Record<string, unknown>>).props['data-grid'] as Record<string, unknown>;
|
||||
expect(grid.minW).toBe(8);
|
||||
});
|
||||
|
||||
const CANCEL = 'input, textarea, select, button, a, [contenteditable], [data-no-drag], .widgetNoDrag';
|
||||
|
||||
it('quick-260916-dyv Test 6: dragConfig-Pin — handle Karte, cancel fuer Interaktives, threshold 3; resizeConfig folgt dem Bearbeitungsmodus', async () => {
|
||||
@@ -503,9 +542,9 @@ describe('RESIZE_AXIS_FALLBACK (Design „Mosaik“, Runde 3)', () => {
|
||||
|
||||
describe('centeringOffset (Design „Mosaik“, Runde 3)', () => {
|
||||
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.
|
||||
expect(centeringOffset([{ x: 0, w: 12 }], 24, 1212)).toBe(300);
|
||||
expect(centeringOffset([{ x: 12, w: 12 }], 24, 1212)).toBe(0);
|
||||
expect(centeringOffset([], 24, 1212)).toBe(0);
|
||||
// 48 Spalten auf 1212 px: Spalte 13 px + 12 px Abstand = 25 px je Spalte.
|
||||
expect(centeringOffset([{ x: 0, w: 24 }], 48, 1212)).toBe(300);
|
||||
expect(centeringOffset([{ x: 24, w: 24 }], 48, 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,
|
||||
// 20 statt 40 px Zeilenhoehe, 8 statt 16 px Abstand). Gespeicherte Anordnungen
|
||||
// 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 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.
|
||||
// - 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) ?? {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: constraints?.defaultW ?? 4,
|
||||
w: constraints?.defaultW ?? 8,
|
||||
h: constraints?.defaultH ?? 4,
|
||||
}),
|
||||
minW: constraints?.minW ?? 4,
|
||||
minW: constraints?.minW ?? 8,
|
||||
minH: constraints?.minH ?? 4,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -67,21 +67,21 @@ describe('WIDGET_CONSTRAINTS (DASH-11)', () => {
|
||||
});
|
||||
|
||||
it('Test A (quick-260916-dyv): Minima = kleinste bedienbare Kachel je Typ, Vorgaben unveraendert (quick-260916-iex: Link-Widget entfernt; quick-260921-pi9: Bilderrahmen dazu; quick-260921-qd3: XFrame dazu; quick-260924-i8v: Proxmox dazu, zehn Typen)', () => {
|
||||
// Raster 24 Spalten / 20 px (quick-260916-bwo). Die Minima sind seit
|
||||
// quick-260916-dyv inhaltsgetrieben (Rechnung im Plan), die Vorgaben
|
||||
// (defaultW/defaultH) bleiben das Doppelte der alten 12-Spalten-Werte.
|
||||
// 3 und 9 sind ungerade — die bwo-Pruefung "jeder Wert ist gerade" entfaellt.
|
||||
// Raster 48 Spalten / 20 px (quick-260929-dmx, vorher 24 Spalten). Alle
|
||||
// Breitenwerte sind gegenueber dem 24er-Raster verdoppelt (gleiche
|
||||
// Bildschirmbreite), Hoehen unveraendert; einzige Ausnahme Kalender-minW 8
|
||||
// (rund 250 px, gemessene kleinste benutzbare Breite; vorher 6 von 24).
|
||||
expect(WIDGET_CONSTRAINTS).toEqual({
|
||||
clock: { minW: 2, minH: 2, defaultW: 4, defaultH: 4 },
|
||||
search: { minW: 6, minH: 2, defaultW: 12, defaultH: 4 },
|
||||
calendar: { minW: 6, minH: 8, defaultW: 8, defaultH: 16 },
|
||||
note: { minW: 4, minH: 4, defaultW: 6, defaultH: 8 },
|
||||
calculator: { minW: 3, minH: 10, defaultW: 6, defaultH: 10 },
|
||||
favorites: { minW: 1, minH: 3, defaultW: 6, defaultH: 10 },
|
||||
stopwatch: { minW: 4, minH: 3, defaultW: 6, defaultH: 6 },
|
||||
'picture-frame': { minW: 4, minH: 4, defaultW: 8, defaultH: 12 },
|
||||
xframe: { minW: 4, minH: 4, defaultW: 12, defaultH: 12 },
|
||||
proxmox: { minW: 3, minH: 4, defaultW: 8, defaultH: 8 },
|
||||
clock: { minW: 4, minH: 2, defaultW: 8, defaultH: 4 },
|
||||
search: { minW: 12, minH: 2, defaultW: 24, defaultH: 4 },
|
||||
calendar: { minW: 8, minH: 8, defaultW: 16, defaultH: 16 },
|
||||
note: { minW: 8, minH: 4, defaultW: 12, defaultH: 8 },
|
||||
calculator: { minW: 6, minH: 10, defaultW: 12, defaultH: 10 },
|
||||
favorites: { minW: 2, minH: 3, defaultW: 12, defaultH: 10 },
|
||||
stopwatch: { minW: 8, minH: 3, defaultW: 12, defaultH: 6 },
|
||||
'picture-frame': { minW: 8, minH: 4, defaultW: 16, defaultH: 12 },
|
||||
xframe: { minW: 8, minH: 4, defaultW: 24, defaultH: 12 },
|
||||
proxmox: { minW: 6, minH: 4, defaultW: 16, defaultH: 8 },
|
||||
});
|
||||
|
||||
let counted = 0;
|
||||
|
||||
@@ -40,45 +40,55 @@ export const WIDGET_CONSTRAINTS: Record<
|
||||
{ minW: number; minH: number; defaultW: number; defaultH: number }
|
||||
> = {
|
||||
// quick-260916-dyv: minW/minH = kleinste noch bedienbare Kachel je Typ im
|
||||
// 24-Spalten/20-px-Raster, aus dem Innenaufbau gerechnet (Suche: Auswahl 120 +
|
||||
// Raster, aus dem Innenaufbau gerechnet (Suche: Auswahl 120 +
|
||||
// Eingabe + Knopf; Rechner: Anzeige 40 + Speicherzeile 28 + 5 Tastenreihen 28 =
|
||||
// 268 px -> 10 Zeilen; Stoppuhr: kompakte Bedienleiste). defaultW/defaultH =
|
||||
// altes 12-Spalten-Mass x2, unveraendert. Gespeicherte minW/minH werden in
|
||||
// dashboard-grid.tsx aus dieser Tabelle ueberschrieben.
|
||||
// quick-260929-dmx: Raster in der Breite doppelt so fein (48 Spalten am
|
||||
// lg-Breakpoint). Alle Breitenwerte (minW, defaultW) sind gegenueber dem
|
||||
// 24-Spalten-Raster verdoppelt, die Kacheln behalten ihre Bildschirmbreite;
|
||||
// Hoehen (minH, defaultH) unveraendert. Einzige Ausnahme: Kalender-minW.
|
||||
// Existing widgets
|
||||
clock: { minW: 2, minH: 2, defaultW: 4, defaultH: 4 },
|
||||
search: { minW: 6, minH: 2, defaultW: 12, defaultH: 4 },
|
||||
clock: { minW: 4, minH: 2, defaultW: 8, defaultH: 4 },
|
||||
search: { minW: 12, minH: 2, defaultW: 24, defaultH: 4 },
|
||||
// quick-260916-htc: Monatsraster braucht Breite fuer 7 Spalten und Hoehe
|
||||
// fuer Nav-Zeile + Wochentagskopf + 6 Rasterzeilen + Naechste-Termine-Block.
|
||||
// Runde 3 (Design „Mosaik“): defaultH 16 statt 12 — mit 12 passte unter
|
||||
// den Monat nur ein Termin.
|
||||
calendar: { minW: 6, minH: 8, defaultW: 8, defaultH: 16 },
|
||||
note: { minW: 4, minH: 4, defaultW: 6, defaultH: 8 },
|
||||
// quick-260929-dmx: auf Nutzerwunsch (29.09.) schmaler ziehbar — minW 8 von
|
||||
// 48 Spalten = rund 250 px am lg-Breakpoint (gemessen: Monatsraster,
|
||||
// Ueberschrift und Terminliste sind dort noch benutzbar; bei rund 185 px
|
||||
// wird die Ueberschrift abgeschnitten). defaultW 16 = unveraenderte Breite.
|
||||
calendar: { minW: 8, minH: 8, defaultW: 16, defaultH: 16 },
|
||||
note: { minW: 8, minH: 4, defaultW: 12, defaultH: 8 },
|
||||
// Phase 8 new widgets (D-01)
|
||||
// minH 10, nicht 9 (260916-dyv, Browser-Messung des Orchestrators): der Rechner hat
|
||||
// SECHS Tastenreihen (%/CE/C/Loeschen, 1/x/x2/Wurzel/Division, 7-9, 4-6, 1-3, 0/Komma/=),
|
||||
// Inhalt 268 px; bei 9 Zeilen (244 px) fehlte die unterste Reihe um 25 px.
|
||||
calculator: { minW: 3, minH: 10, defaultW: 6, defaultH: 10 },
|
||||
// Favoriten: minW 1 statt 3 (Nutzerwunsch 23.09.) — bei kurzen Linknamen
|
||||
// blieb rechts viel Leerraum. Der Titel kuerzt mit Auslassungszeichen,
|
||||
// in der schmalsten Stufe bleibt das Symbol als Leiste stehen.
|
||||
favorites: { minW: 1, minH: 3, defaultW: 6, defaultH: 10 },
|
||||
stopwatch: { minW: 4, minH: 3, defaultW: 6, defaultH: 6 },
|
||||
// quick-260921-pi9: ein Bild braucht Flaeche — 8x12 entspricht der
|
||||
// Kalender-Vorgabe (rund 530x330 px, Querformat wie ein Foto); 8x8 war im
|
||||
// Browser-Rundgang mit 216 px Hoehe zu flach. 4x4 ist die kleinste Kachel,
|
||||
// in der Bild und Unterschrift-Streifen noch erkennbar sind.
|
||||
'picture-frame': { minW: 4, minH: 4, defaultW: 8, defaultH: 12 },
|
||||
// quick-260921-qd3: eine Webseite braucht Breite UND Hoehe — 12x12 = halbe
|
||||
// Rasterbreite bei rund 330 px Hoehe (8 Zeilen waeren nur 216 px, zu flach
|
||||
// fuer eine Seite; vgl. Bilderrahmen 8x12). 4x4 kleinste Kachel, in der ein
|
||||
// Rahmen noch Sinn hat.
|
||||
xframe: { minW: 4, minH: 4, defaultW: 12, defaultH: 12 },
|
||||
calculator: { minW: 6, minH: 10, defaultW: 12, defaultH: 10 },
|
||||
// Favoriten: minW 1 (im 24er-Raster, jetzt 2 von 48) statt 3 (Nutzerwunsch
|
||||
// 23.09.) — bei kurzen Linknamen blieb rechts viel Leerraum. Der Titel
|
||||
// kuerzt mit Auslassungszeichen, in der schmalsten Stufe bleibt das Symbol
|
||||
// als Leiste stehen.
|
||||
favorites: { minW: 2, minH: 3, defaultW: 12, defaultH: 10 },
|
||||
stopwatch: { minW: 8, minH: 3, defaultW: 12, defaultH: 6 },
|
||||
// quick-260921-pi9: ein Bild braucht Flaeche — 16x12 (im 48er-Raster)
|
||||
// entspricht der Kalender-Vorgabe (rund 530x330 px, Querformat wie ein
|
||||
// Foto); Hoehe 8 war im Browser-Rundgang mit 216 px zu flach. 8x4 ist die
|
||||
// kleinste Kachel, in der Bild und Unterschrift-Streifen noch erkennbar sind.
|
||||
'picture-frame': { minW: 8, minH: 4, defaultW: 16, defaultH: 12 },
|
||||
// quick-260921-qd3: eine Webseite braucht Breite UND Hoehe — 24x12 im
|
||||
// 48er-Raster = halbe Rasterbreite bei rund 330 px Hoehe (8 Zeilen waeren
|
||||
// nur 216 px, zu flach fuer eine Seite; vgl. Bilderrahmen). 8x4 kleinste
|
||||
// Kachel, in der ein Rahmen noch Sinn hat.
|
||||
xframe: { minW: 8, minH: 4, defaultW: 24, defaultH: 12 },
|
||||
// quick-260924-i8v: 4 Zeilen = 104 px reichen genau fuer Balken und
|
||||
// Zusammenfassung (die Serverliste blendet sich darunter per Container-
|
||||
// Query aus); 8x8 = rund 456x216 px bei 1400 px Breite zeigt etwa sechs
|
||||
// Serverzeilen; 3 Spalten = rund 166 px zeigen nur Punkte und Namen.
|
||||
proxmox: { minW: 3, minH: 4, defaultW: 8, defaultH: 8 },
|
||||
// Query aus); 16x8 (im 48er-Raster) = rund 456x216 px bei 1400 px Breite
|
||||
// zeigt etwa sechs Serverzeilen; 6 Spalten = rund 166 px zeigen nur Punkte
|
||||
// und Namen.
|
||||
proxmox: { minW: 6, minH: 4, defaultW: 16, defaultH: 8 },
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,13 +8,14 @@ import {
|
||||
} 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
|
||||
* den alten Einheiten (12 Spalten / 40 px) in die neuen (24 Spalten / 20 px).
|
||||
* Kern ist T-BWO-02: die Verdopplung darf GENAU EINMAL geschehen. Der Marker
|
||||
* `__gridVersion` steht dafuer im gespeicherten JSON, nie im Zustand — Test 4
|
||||
* (Idempotenz) wird rot, sobald jemand die Marker-Pruefung entfernt.
|
||||
* Tests fuer die stufenweise Umrechnung gespeicherter Anordnungen:
|
||||
* v1 (12 Spalten / 40 px) -> v2 (24 / 20 px, alles x2) -> v3 (48 Spalten,
|
||||
* nur Breite x2). Kern ist T-BWO-02: jede Stufe darf GENAU EINMAL geschehen.
|
||||
* Der Marker `__gridVersion` steht dafuer im gespeicherten JSON, nie im
|
||||
* Zustand — der Idempotenz-Test wird rot, sobald jemand die Marker-Pruefung
|
||||
* entfernt.
|
||||
*/
|
||||
|
||||
function altLayouts() {
|
||||
@@ -31,14 +32,14 @@ function altLayouts() {
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
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.md[0]).toEqual({ i: 'a', x: 0, y: 0, w: 4, h: 4 });
|
||||
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: 8, h: 4 });
|
||||
expect(layouts.sm).toEqual([]);
|
||||
expect(layouts.xs).toEqual([]);
|
||||
expect(layouts.xxs).toEqual([]);
|
||||
@@ -47,15 +48,44 @@ describe('grid-layout-migration (quick-260916-bwo)', () => {
|
||||
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 { 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(migrated).toBe(false);
|
||||
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)', () => {
|
||||
const empty = { lg: [], md: [], sm: [], xs: [], xxs: [] };
|
||||
const r1 = migrateGridLayouts(empty);
|
||||
@@ -73,6 +103,14 @@ describe('grid-layout-migration (quick-260916-bwo)', () => {
|
||||
|
||||
expect(twice.layouts).toEqual(once.layouts);
|
||||
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', () => {
|
||||
@@ -80,30 +118,30 @@ describe('grid-layout-migration (quick-260916-bwo)', () => {
|
||||
const marked = withGridVersion(layouts);
|
||||
|
||||
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.md).toBe(layouts.md);
|
||||
expect(Object.keys(layouts)).not.toContain(GRID_VERSION_KEY);
|
||||
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 future = migrateGridLayouts({ ...alt, __gridVersion: 3 });
|
||||
const future = migrateGridLayouts({ ...alt, __gridVersion: 4 });
|
||||
expect(future.layouts).toEqual(alt);
|
||||
expect(future.migrated).toBe(false);
|
||||
|
||||
const stringMarker = migrateGridLayouts({ ...alt, __gridVersion: '2' });
|
||||
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);
|
||||
|
||||
const odd = migrateGridLayouts({
|
||||
lg: [{ i: 'c', x: 'kaputt', y: 1, w: 2, h: 2, resizeHandles: ['se'], moved: true, static: false }],
|
||||
});
|
||||
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(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', () => {
|
||||
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(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(migrateGridLayouts(null)).toEqual({ layouts: {}, migrated: false });
|
||||
|
||||
@@ -1,23 +1,35 @@
|
||||
/**
|
||||
* grid-layout-migration — einmalige Umrechnung gespeicherter Dashboard-
|
||||
* Anordnungen in die feineren Raster-Einheiten (quick-260916-bwo).
|
||||
* grid-layout-migration — einmalige, stufenweise Umrechnung gespeicherter
|
||||
* 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 /
|
||||
* 20 px verdoppelt (`dashboard-grid.tsx`). Eine in ALTEN Einheiten gespeicherte
|
||||
* Anordnung wuerde im neuen Raster halb so gross und an der halben Position
|
||||
* erscheinen. Deshalb werden `x, y, w, h` (und, falls vorhanden, `minW, minH,
|
||||
* maxW, maxH`) jedes Elements in jedem Breakpoint GENAU EINMAL mit 2
|
||||
* multipliziert.
|
||||
* 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:
|
||||
*
|
||||
* Marker: Damit die Verdopplung nur einmal geschieht, traegt das gespeicherte
|
||||
* JSON den Schluessel `__gridVersion: 2`. 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 verdoppelt — deshalb muss JEDER Speichervorgang
|
||||
* `withGridVersion` benutzen (T-BWO-02, Store-Tests pinnen das).
|
||||
* - 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`.
|
||||
@@ -28,7 +40,7 @@
|
||||
* React- oder Store-Abhaengigkeit.
|
||||
*/
|
||||
|
||||
export const GRID_VERSION = 2;
|
||||
export const GRID_VERSION = 3;
|
||||
export const GRID_VERSION_KEY = '__gridVersion';
|
||||
export const GRID_SCALE_FACTOR = 2;
|
||||
|
||||
@@ -43,7 +55,13 @@ export interface 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> {
|
||||
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];
|
||||
// Nur eine Zahl ist ein Marker; alles andere (fehlend, Zeichenkette) zaehlt als alt.
|
||||
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 = {};
|
||||
let migrated = false;
|
||||
@@ -75,12 +94,14 @@ export function migrateGridLayouts(raw: unknown): { layouts: GridLayouts; migrat
|
||||
layouts[key] = value.map((item) => {
|
||||
const copy = { ...(item as GridLayoutItem) };
|
||||
if (needsScaling) {
|
||||
for (const field of SCALED_FIELDS) {
|
||||
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;
|
||||
|
||||
@@ -74,12 +74,12 @@ describe('dashboard-store — einmalige Umrechnung mit Marker (quick-260916-bwo)
|
||||
await useDashboardStore.getState().loadDashboard();
|
||||
|
||||
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(api.saveLayout).toHaveBeenCalledTimes(1);
|
||||
expect(api.saveLayout).toHaveBeenCalledWith(
|
||||
'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.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 () => {
|
||||
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();
|
||||
@@ -121,7 +121,7 @@ describe('dashboard-store — einmalige Umrechnung mit Marker (quick-260916-bwo)
|
||||
expect(api.saveLayout).toHaveBeenCalledTimes(1);
|
||||
expect(api.saveLayout).toHaveBeenCalledWith(
|
||||
'dash-1',
|
||||
expect.objectContaining({ __gridVersion: 2, ...layouts }),
|
||||
expect.objectContaining({ __gridVersion: 3, ...layouts }),
|
||||
);
|
||||
expect(useDashboardStore.getState().isDirty).toBe(false);
|
||||
expect(Object.keys(useDashboardStore.getState().layouts)).not.toContain('__gridVersion');
|
||||
@@ -137,13 +137,13 @@ describe('dashboard-store — einmalige Umrechnung mit Marker (quick-260916-bwo)
|
||||
await expect(useDashboardStore.getState().loadDashboard()).resolves.toBeUndefined();
|
||||
|
||||
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.isLoading).toBe(false);
|
||||
expect(errorSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Test 6: neues Widget wird in den neuen (verdoppelten) Vorgabegroessen fuer den aktiven Reiter angelegt', async () => {
|
||||
it('Test 6: neues Widget wird in den Vorgabegroessen des 48er-Rasters (quick-260929-dmx) fuer den aktiven Reiter angelegt', async () => {
|
||||
await useDashboardStore.getState().loadDashboard();
|
||||
vi.mocked(api.addWidget).mockResolvedValue({ id: 'n1', widgetType: 'clock', config: {} });
|
||||
|
||||
@@ -151,7 +151,7 @@ describe('dashboard-store — einmalige Umrechnung mit Marker (quick-260916-bwo)
|
||||
|
||||
const state = useDashboardStore.getState();
|
||||
expect(api.addWidget).toHaveBeenCalledWith('dash-1', 'clock');
|
||||
expect(state.layouts.lg).toContainEqual({ i: 'n1', x: 0, y: 0, w: 4, h: 4 });
|
||||
expect(state.layouts.lg).toContainEqual({ i: 'n1', x: 0, y: 0, w: 8, h: 4 });
|
||||
expect(state.isDirty).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -213,7 +213,7 @@ describe('dashboard-store — Reiterwechsel (quick-260923-ad9, Task 3)', () => {
|
||||
await useDashboardStore.getState().loadDashboard();
|
||||
|
||||
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: {} }]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user