feat(quick-260916-bwo): Dashboard-Raster verdoppelt (24 Spalten, 20 px, 8 px Abstand), Konstanten x2, einmalige Umrechnung gespeicherter Anordnungen mit Marker __gridVersion
- dashboard-grid.tsx: COLS 24/20/12/8/2, rowHeight 20, margin 8 (containerPadding folgt), Rueckfallwerte 4 - widget-registry.tsx: alle 32 Werte in WIDGET_CONSTRAINTS verdoppelt - grid-layout-migration.ts (neu): migrateGridLayouts/withGridVersion, Marker nur im JSON, Idempotenz (T-BWO-02) - dashboard-store.ts: Umrechnung beim Laden, Sofort-Speichern mit Marker, withGridVersion bei jedem saveLayout - Tests: Migration 7 (neu), Store 6 (neu), Registry +1 (Tabelle), Grid +2 (Props ueber Mock), API-Spec +2 (Durchreichung __gridVersion, timeFontSizePt) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
GRID_SCALE_FACTOR,
|
||||
GRID_VERSION,
|
||||
GRID_VERSION_KEY,
|
||||
migrateGridLayouts,
|
||||
withGridVersion,
|
||||
} from './grid-layout-migration';
|
||||
|
||||
/**
|
||||
* grid-layout-migration.test — NEU (quick-260916-bwo, feineres Dashboard-Raster).
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
function altLayouts() {
|
||||
return {
|
||||
lg: [
|
||||
{ i: 'a', x: 1, y: 2, w: 2, h: 3, minW: 2, minH: 2, moved: false, static: false },
|
||||
{ i: 'b', x: 2, y: 0, w: 6, h: 2, maxW: 12, maxH: 8 },
|
||||
],
|
||||
md: [{ i: 'a', x: 0, y: 0, w: 2, h: 2 }],
|
||||
sm: [],
|
||||
xs: [],
|
||||
xxs: [],
|
||||
};
|
||||
}
|
||||
|
||||
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', () => {
|
||||
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,
|
||||
});
|
||||
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.sm).toEqual([]);
|
||||
expect(layouts.xs).toEqual([]);
|
||||
expect(layouts.xxs).toEqual([]);
|
||||
expect(migrated).toBe(true);
|
||||
expect(Object.keys(layouts)).not.toContain(GRID_VERSION_KEY);
|
||||
expect(GRID_SCALE_FACTOR).toBe(2);
|
||||
});
|
||||
|
||||
it('Test 2: markierte Anordnung (__gridVersion 2) bleibt unveraendert, migrated false', () => {
|
||||
const alt = altLayouts();
|
||||
const { layouts, migrated } = migrateGridLayouts({ ...alt, __gridVersion: 2 });
|
||||
|
||||
expect(layouts).toEqual(alt);
|
||||
expect(migrated).toBe(false);
|
||||
expect(Object.keys(layouts)).not.toContain(GRID_VERSION_KEY);
|
||||
});
|
||||
|
||||
it('Test 3: leere Anordnung bleibt leer, migrated false (kein Speichern noetig)', () => {
|
||||
const empty = { lg: [], md: [], sm: [], xs: [], xxs: [] };
|
||||
const r1 = migrateGridLayouts(empty);
|
||||
expect(r1.layouts).toEqual(empty);
|
||||
expect(r1.migrated).toBe(false);
|
||||
|
||||
const r2 = migrateGridLayouts({});
|
||||
expect(r2.layouts).toEqual({});
|
||||
expect(r2.migrated).toBe(false);
|
||||
});
|
||||
|
||||
it('Test 4: Idempotenz — einmal umgerechnet und markiert wird nicht erneut verdoppelt (T-BWO-02)', () => {
|
||||
const once = migrateGridLayouts(altLayouts());
|
||||
const twice = migrateGridLayouts(withGridVersion(once.layouts));
|
||||
|
||||
expect(twice.layouts).toEqual(once.layouts);
|
||||
expect(twice.migrated).toBe(false);
|
||||
});
|
||||
|
||||
it('Test 5: withGridVersion haengt den Marker an, ohne die Eingabe zu veraendern', () => {
|
||||
const { layouts } = migrateGridLayouts(altLayouts());
|
||||
const marked = withGridVersion(layouts);
|
||||
|
||||
expect(marked[GRID_VERSION_KEY]).toBe(GRID_VERSION);
|
||||
expect(GRID_VERSION).toBe(2);
|
||||
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', () => {
|
||||
const alt = altLayouts();
|
||||
|
||||
const future = migrateGridLayouts({ ...alt, __gridVersion: 3 });
|
||||
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(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,
|
||||
});
|
||||
expect(Number.isNaN(odd.layouts.lg[0].x)).toBe(false);
|
||||
expect(odd.migrated).toBe(true);
|
||||
});
|
||||
|
||||
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.migrated).toBe(true);
|
||||
|
||||
expect(migrateGridLayouts(null)).toEqual({ layouts: {}, migrated: false });
|
||||
expect(migrateGridLayouts(undefined)).toEqual({ layouts: {}, migrated: false });
|
||||
expect(migrateGridLayouts(42)).toEqual({ layouts: {}, migrated: false });
|
||||
expect(migrateGridLayouts([1, 2])).toEqual({ layouts: {}, migrated: false });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* grid-layout-migration — einmalige Umrechnung gespeicherter Dashboard-
|
||||
* Anordnungen in die feineren Raster-Einheiten (quick-260916-bwo).
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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).
|
||||
*
|
||||
* Idempotenz: `migrateGridLayouts(withGridVersion(migrateGridLayouts(alt).layouts))`
|
||||
* liefert dasselbe Ergebnis wie `migrateGridLayouts(alt)` mit `migrated: false`.
|
||||
*
|
||||
* 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 = 2;
|
||||
export const GRID_VERSION_KEY = '__gridVersion';
|
||||
export const GRID_SCALE_FACTOR = 2;
|
||||
|
||||
export interface GridLayoutItem {
|
||||
i: string;
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export type GridLayouts = Record<string, GridLayoutItem[]>;
|
||||
|
||||
const SCALED_FIELDS = ['x', 'y', 'w', 'h', 'minW', 'minH', 'maxW', 'maxH'] as const;
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 } {
|
||||
if (!isPlainObject(raw)) {
|
||||
return { layouts: {}, migrated: false };
|
||||
}
|
||||
|
||||
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 layouts: GridLayouts = {};
|
||||
let migrated = false;
|
||||
|
||||
for (const key of Object.keys(raw)) {
|
||||
if (key === GRID_VERSION_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 field of SCALED_FIELDS) {
|
||||
const n = copy[field];
|
||||
if (typeof n === 'number') {
|
||||
copy[field] = n * GRID_SCALE_FACTOR;
|
||||
}
|
||||
}
|
||||
migrated = true;
|
||||
}
|
||||
return copy;
|
||||
});
|
||||
}
|
||||
|
||||
return { layouts, migrated };
|
||||
}
|
||||
|
||||
/**
|
||||
* Haengt den Marker fuer das Speichern an, ohne die Eingabe zu veraendern.
|
||||
*/
|
||||
export function withGridVersion(layouts: GridLayouts): Record<string, unknown> {
|
||||
return { ...layouts, [GRID_VERSION_KEY]: GRID_VERSION };
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
/**
|
||||
* dashboard-store.test — NEU (quick-260916-bwo, feineres Dashboard-Raster).
|
||||
*
|
||||
* Sechs Tests fuer die Anbindung der einmaligen Umrechnung im Store:
|
||||
* `loadDashboard` rechnet alte Anordnungen um und speichert SOFORT mit
|
||||
* Marker; `saveLayout` traegt den Marker bei JEDEM Speichern (T-BWO-02 —
|
||||
* fehlt er, wuerde das naechste Laden erneut verdoppeln); der Zustand
|
||||
* selbst bleibt markerfrei (der Store iteriert mit Object.keys ueber die
|
||||
* Breakpoints); `addWidget` legt neue Eintraege in den verdoppelten
|
||||
* Vorgabegroessen an. Der Store hatte bisher keine Testdatei.
|
||||
*/
|
||||
vi.mock('@/lib/dashboard-api', () => ({
|
||||
fetchLayout: vi.fn(),
|
||||
fetchWidgets: vi.fn(),
|
||||
saveLayout: vi.fn(),
|
||||
addWidget: vi.fn(),
|
||||
removeWidget: vi.fn(),
|
||||
updateWidgetConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
import * as api from '@/lib/dashboard-api';
|
||||
|
||||
const { useDashboardStore } = await import('./dashboard-store');
|
||||
|
||||
const EMPTY = { lg: [], md: [], sm: [], xs: [], xxs: [] };
|
||||
|
||||
beforeEach(() => {
|
||||
useDashboardStore.setState({
|
||||
layouts: { lg: [], md: [], sm: [], xs: [], xxs: [] },
|
||||
widgets: [],
|
||||
isEditMode: false,
|
||||
isDirty: false,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(api.fetchWidgets).mockResolvedValue([]);
|
||||
vi.mocked(api.saveLayout).mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('dashboard-store — einmalige Umrechnung mit Marker (quick-260916-bwo)', () => {
|
||||
it('Test 1: alte Anordnung wird beim Laden umgerechnet und SOFORT mit Marker gespeichert', async () => {
|
||||
vi.mocked(api.fetchLayout).mockResolvedValue({
|
||||
lg: [{ i: 'a', x: 1, y: 1, w: 2, h: 2 }], md: [], sm: [], xs: [], xxs: [],
|
||||
});
|
||||
|
||||
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(Object.keys(state.layouts)).not.toContain('__gridVersion');
|
||||
expect(api.saveLayout).toHaveBeenCalledTimes(1);
|
||||
expect(api.saveLayout).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ __gridVersion: 2, lg: [{ i: 'a', x: 2, y: 2, w: 4, h: 4 }] }),
|
||||
);
|
||||
expect(state.isDirty).toBe(false);
|
||||
expect(state.isLoading).toBe(false);
|
||||
expect(state.error).toBeNull();
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
await useDashboardStore.getState().loadDashboard();
|
||||
|
||||
const state = useDashboardStore.getState();
|
||||
expect(state.layouts.lg[0]).toEqual({ i: 'a', x: 1, y: 1, w: 2, h: 2 });
|
||||
expect(api.saveLayout).not.toHaveBeenCalled();
|
||||
expect(Object.keys(state.layouts)).not.toContain('__gridVersion');
|
||||
});
|
||||
|
||||
it('Test 3: leere Vorgabe-Anordnung ohne Marker -> kein Speichern', async () => {
|
||||
vi.mocked(api.fetchLayout).mockResolvedValue({ ...EMPTY });
|
||||
|
||||
await useDashboardStore.getState().loadDashboard();
|
||||
|
||||
expect(api.saveLayout).not.toHaveBeenCalled();
|
||||
expect(useDashboardStore.getState().layouts).toEqual(EMPTY);
|
||||
});
|
||||
|
||||
it('Test 4: jedes Speichern traegt den Marker (T-BWO-02), der Zustand bleibt markerfrei', async () => {
|
||||
const layouts = { lg: [{ i: 'a', x: 2, y: 2, w: 4, h: 4 }], md: [], sm: [], xs: [], xxs: [] };
|
||||
useDashboardStore.getState().updateLayouts(layouts);
|
||||
expect(useDashboardStore.getState().isDirty).toBe(true);
|
||||
|
||||
await useDashboardStore.getState().saveLayout();
|
||||
|
||||
expect(api.saveLayout).toHaveBeenCalledTimes(1);
|
||||
expect(api.saveLayout).toHaveBeenCalledWith(expect.objectContaining({ __gridVersion: 2, ...layouts }));
|
||||
expect(useDashboardStore.getState().isDirty).toBe(false);
|
||||
expect(Object.keys(useDashboardStore.getState().layouts)).not.toContain('__gridVersion');
|
||||
});
|
||||
|
||||
it('Test 5: scheitert das Sofort-Speichern, bleibt der Zustand umgerechnet, kein Fehlerzustand, console.error einmal', async () => {
|
||||
vi.mocked(api.fetchLayout).mockResolvedValue({
|
||||
lg: [{ i: 'a', x: 1, y: 1, w: 2, h: 2 }], md: [], sm: [], xs: [], xxs: [],
|
||||
});
|
||||
vi.mocked(api.saveLayout).mockRejectedValue(new Error('PUT failed'));
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
await expect(useDashboardStore.getState().loadDashboard()).resolves.toBeUndefined();
|
||||
|
||||
const state = useDashboardStore.getState();
|
||||
expect(state.layouts.lg[0].x).toBe(2);
|
||||
expect(state.error).toBeNull();
|
||||
expect(state.isLoading).toBe(false);
|
||||
expect(errorSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Test 6: neues Widget wird in den neuen (verdoppelten) Vorgabegroessen angelegt', async () => {
|
||||
vi.mocked(api.addWidget).mockResolvedValue({ id: 'n1', widgetType: 'clock', config: {} });
|
||||
|
||||
await useDashboardStore.getState().addWidget('clock');
|
||||
|
||||
const state = useDashboardStore.getState();
|
||||
expect(state.layouts.lg).toContainEqual({ i: 'n1', x: 0, y: 0, w: 4, h: 4 });
|
||||
expect(state.isDirty).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { create } from 'zustand';
|
||||
import * as api from '@/lib/dashboard-api';
|
||||
import type { WidgetType } from '@/components/dashboard/widget-registry';
|
||||
import { WIDGET_CONSTRAINTS } from '@/components/dashboard/widget-registry';
|
||||
import { migrateGridLayouts, withGridVersion } from '@/lib/grid-layout-migration';
|
||||
|
||||
export interface WidgetInstance {
|
||||
id: string;
|
||||
@@ -28,6 +29,14 @@ interface DashboardState {
|
||||
/**
|
||||
* Dashboard Zustand store. NO persist middleware — layout comes from DB (D-05).
|
||||
* Save only on exiting edit mode (D-01), not on every drag.
|
||||
*
|
||||
* quick-260916-bwo: Gespeicherte Anordnungen in alten Raster-Einheiten werden
|
||||
* beim Laden einmalig umgerechnet (`migrateGridLayouts`) und sofort mit dem
|
||||
* Marker `__gridVersion` zurueckgeschrieben. Der Marker lebt NUR im
|
||||
* gespeicherten JSON, nie im Zustand (dieser Store iteriert mit Object.keys
|
||||
* ueber die Breakpoints). Fehlt der Marker beim Speichern, wird beim naechsten
|
||||
* Laden erneut verdoppelt — deshalb `withGridVersion` an BEIDEN Speicherstellen
|
||||
* (Sofort-Speichern nach der Umrechnung und `saveLayout`), T-BWO-02.
|
||||
*/
|
||||
export const useDashboardStore = create<DashboardState>()((set, get) => ({
|
||||
layouts: { lg: [], md: [], sm: [], xs: [], xxs: [] },
|
||||
@@ -108,15 +117,26 @@ export const useDashboardStore = create<DashboardState>()((set, get) => ({
|
||||
loadDashboard: async () => {
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
const [layouts, widgets] = await Promise.all([
|
||||
const [rawLayouts, widgets] = await Promise.all([
|
||||
api.fetchLayout(),
|
||||
api.fetchWidgets(),
|
||||
]);
|
||||
const { layouts: migratedLayouts, migrated } = migrateGridLayouts(rawLayouts);
|
||||
set({
|
||||
layouts: layouts as DashboardState['layouts'],
|
||||
layouts: migratedLayouts,
|
||||
widgets,
|
||||
isLoading: false,
|
||||
});
|
||||
// Umgerechnete Anordnung SOFORT mit Marker persistieren — nach dem set,
|
||||
// damit die Oberflaeche unabhaengig vom Speichern rendert; eigener
|
||||
// try/catch, damit ein Speicherfehler NICHT als Ladefehler erscheint.
|
||||
if (migrated) {
|
||||
try {
|
||||
await api.saveLayout(withGridVersion(migratedLayouts));
|
||||
} catch (err) {
|
||||
console.error('Failed to persist migrated layout:', err);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
set({
|
||||
error: 'Failed to load dashboard',
|
||||
@@ -127,7 +147,7 @@ export const useDashboardStore = create<DashboardState>()((set, get) => ({
|
||||
|
||||
saveLayout: async () => {
|
||||
try {
|
||||
await api.saveLayout(get().layouts);
|
||||
await api.saveLayout(withGridVersion(get().layouts));
|
||||
set({ isDirty: false });
|
||||
} catch (err) {
|
||||
console.error('Failed to save layout:', err);
|
||||
|
||||
Reference in New Issue
Block a user