fix(dashboard): Rasterversion schuetzen, Kalender-Mindestbreite, Breiten je Breakpoint, Hintergrund

- Layout mit neuerer Rasterversion wird nie gespeichert, Bearbeiten gesperrt mit Hinweis
- Kalender minW 11 (~260 px bei lg), Breiten je Breakpoint auf Spaltenzahl begrenzt
- optimistische Ruecksetzung nur, wenn noch der gesetzte Wert steht
- Titel-Schalter mit fester Beschriftung + aria-pressed
- Hintergrund-Dialog: Fokus rein/zurueck, Tab bleibt im Dialog
- Loeschen eines Bildes setzt eine darauf zeigende Hintergrund-Wahl zurueck
- Uebersetzungen und CHANGELOG

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 03:20:16 +02:00
parent 071082983b
commit 12214a948e
21 changed files with 1194 additions and 205 deletions
+59 -9
View File
@@ -36,7 +36,15 @@ describe('grid-layout-migration (quick-260916-bwo)', () => {
const { layouts, migrated } = migrateGridLayouts(altLayouts());
expect(layouts.lg[0]).toEqual({
i: 'a', x: 4, y: 4, w: 8, h: 6, minW: 8, 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: 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 });
@@ -53,7 +61,15 @@ describe('grid-layout-migration (quick-260916-bwo)', () => {
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,
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 });
@@ -131,6 +147,7 @@ describe('grid-layout-migration (quick-260916-bwo)', () => {
const future = migrateGridLayouts({ ...alt, __gridVersion: 4 });
expect(future.layouts).toEqual(alt);
expect(future.migrated).toBe(false);
expect(future.newer).toBe(true);
const stringMarker = migrateGridLayouts({ ...alt, __gridVersion: '2' });
expect(stringMarker.migrated).toBe(true);
@@ -138,24 +155,57 @@ describe('grid-layout-migration (quick-260916-bwo)', () => {
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 }],
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: 8, 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);
});
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(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 });
expect(migrateGridLayouts(undefined)).toEqual({ layouts: {}, migrated: false });
expect(migrateGridLayouts(42)).toEqual({ layouts: {}, migrated: false });
expect(migrateGridLayouts([1, 2])).toEqual({ layouts: {}, migrated: false });
expect(migrateGridLayouts(null)).toEqual({ layouts: {}, migrated: false, newer: false });
expect(migrateGridLayouts(undefined)).toEqual({ layouts: {}, migrated: false, newer: false });
expect(migrateGridLayouts(42)).toEqual({ layouts: {}, migrated: false, newer: false });
expect(migrateGridLayouts([1, 2])).toEqual({ layouts: {}, migrated: false, newer: false });
});
it('Test 8 (quick-260930): nur eine HOEHERE Marke meldet newer — v1/v2/v3 nicht', () => {
const alt = altLayouts();
expect(migrateGridLayouts(alt).newer).toBe(false);
expect(migrateGridLayouts({ ...alt, [GRID_VERSION_KEY]: 2 }).newer).toBe(false);
expect(migrateGridLayouts({ ...alt, [GRID_VERSION_KEY]: GRID_VERSION }).newer).toBe(false);
const future = migrateGridLayouts({ ...alt, [GRID_VERSION_KEY]: GRID_VERSION + 1 });
expect(future.newer).toBe(true);
// Unveraendert uebernommen, nichts skaliert.
expect(future.layouts).toEqual(alt);
});
});
+16 -3
View File
@@ -34,6 +34,13 @@
* Idempotenz: `migrateGridLayouts(withGridVersion(migrateGridLayouts(alt).layouts))`
* liefert dasselbe Ergebnis wie `migrateGridLayouts(alt)` mit `migrated: false`.
*
* Neuere Marke (quick-260930): Traegt das JSON eine HOEHERE Marke als
* `GRID_VERSION`, hat es ein neuerer Programmstand geschrieben (etwa ein
* frisch geladener Tab neben einem alten). Umgerechnet wird dann nichts, aber
* `newer: true` gemeldet — der Store darf diese Anordnung NIE speichern: er
* wuerde sie mit der alten Marke zurueckschreiben, und der neue Stand wuerde
* sie beim naechsten Laden ein zweites Mal skalieren.
*
* 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
@@ -72,14 +79,20 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
* 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 } {
export function migrateGridLayouts(raw: unknown): {
layouts: GridLayouts;
migrated: boolean;
/** Marke hoeher als `GRID_VERSION` — Anordnung stammt von einem neueren Programmstand. */
newer: boolean;
} {
if (!isPlainObject(raw)) {
return { layouts: {}, migrated: false };
return { layouts: {}, migrated: false, newer: 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 newer = version > GRID_VERSION;
const steps = MIGRATION_STEPS.filter((step) => version <= step.from);
const needsScaling = steps.length > 0;
@@ -108,7 +121,7 @@ export function migrateGridLayouts(raw: unknown): { layouts: GridLayouts; migrat
});
}
return { layouts, migrated };
return { layouts, migrated, newer };
}
/**
+142 -8
View File
@@ -53,6 +53,7 @@ beforeEach(() => {
isDirty: false,
isLoading: false,
error: null,
layoutFromNewerVersion: false,
});
vi.clearAllMocks();
vi.mocked(api.fetchDashboards).mockResolvedValue([DASH_1]);
@@ -68,7 +69,11 @@ afterEach(() => {
describe('dashboard-store — einmalige Umrechnung mit Marker (quick-260916-bwo)', () => {
it('Test 1: alte Anordnung wird beim Laden umgerechnet und SOFORT fuer den ersten Reiter mit Marker gespeichert', async () => {
vi.mocked(api.fetchLayout).mockResolvedValue({
lg: [{ i: 'a', x: 1, y: 1, w: 2, h: 2 }], md: [], sm: [], xs: [], xxs: [],
lg: [{ i: 'a', x: 1, y: 1, w: 2, h: 2 }],
md: [],
sm: [],
xs: [],
xxs: [],
});
await useDashboardStore.getState().loadDashboard();
@@ -88,7 +93,12 @@ 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: 3,
lg: [{ i: 'a', x: 1, y: 1, w: 2, h: 2 }],
md: [],
sm: [],
xs: [],
xxs: [],
__gridVersion: 3,
});
await useDashboardStore.getState().loadDashboard();
@@ -129,7 +139,11 @@ describe('dashboard-store — einmalige Umrechnung mit Marker (quick-260916-bwo)
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: [],
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(() => {});
@@ -197,7 +211,9 @@ describe('dashboard-store — Reiterwechsel (quick-260923-ad9, Task 3)', () => {
it('Test 9: ein Wechsel mit ungespeicherter Anordnung schreibt die Anordnung ZUERST fuer den ALTEN Reiter', async () => {
vi.mocked(api.fetchDashboards).mockResolvedValue([DASH_1, DASH_2]);
await useDashboardStore.getState().loadDashboard();
useDashboardStore.getState().updateLayouts({ lg: [{ i: 'x', x: 0, y: 0, w: 1, h: 1 }], md: [], sm: [], xs: [], xxs: [] });
useDashboardStore
.getState()
.updateLayouts({ lg: [{ i: 'x', x: 0, y: 0, w: 1, h: 1 }], md: [], sm: [], xs: [], xxs: [] });
vi.mocked(api.saveLayout).mockClear();
vi.mocked(api.fetchLayout).mockResolvedValue({ ...EMPTY });
vi.mocked(api.fetchWidgets).mockResolvedValue([]);
@@ -213,9 +229,16 @@ 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: 3,
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: {} },
]);
await useDashboardStore.getState().selectDashboard('dash-2');
@@ -253,7 +276,11 @@ describe('dashboard-store — Reiter anlegen/umbenennen/loeschen (quick-260923-a
it('Test 13: renameDashboard aktualisiert den Namen in der Reiterliste', async () => {
await useDashboardStore.getState().loadDashboard();
vi.mocked(api.renameDashboardTab).mockResolvedValue({ id: 'dash-1', name: 'Finanzen', position: 0 });
vi.mocked(api.renameDashboardTab).mockResolvedValue({
id: 'dash-1',
name: 'Finanzen',
position: 0,
});
await useDashboardStore.getState().renameDashboard('dash-1', 'Finanzen');
@@ -265,7 +292,9 @@ describe('dashboard-store — Reiter anlegen/umbenennen/loeschen (quick-260923-a
await useDashboardStore.getState().loadDashboard();
vi.mocked(api.deleteDashboardTab).mockResolvedValue(undefined);
vi.mocked(api.fetchLayout).mockResolvedValue({ ...EMPTY });
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: {} },
]);
await useDashboardStore.getState().deleteDashboard('dash-1');
@@ -320,3 +349,108 @@ describe('dashboard-store — Reiter per Ziehen umsortieren (quick-260923-ad9, T
expect(errorSpy).toHaveBeenCalledTimes(1);
});
});
describe('dashboard-store — Anordnung eines neueren Programmstands (quick-260930)', () => {
const FUTURE = {
lg: [{ i: 'a', x: 2, y: 0, w: 8, h: 4 }],
md: [],
sm: [],
xs: [],
xxs: [],
__gridVersion: 99,
};
it('Test 18: hoehere Marke -> Merker gesetzt, nichts skaliert, nie gespeichert, Bearbeiten gesperrt', async () => {
vi.mocked(api.fetchLayout).mockResolvedValue({ ...FUTURE });
await useDashboardStore.getState().loadDashboard();
const state = useDashboardStore.getState();
expect(state.layoutFromNewerVersion).toBe(true);
expect(state.layouts.lg[0]).toEqual({ i: 'a', x: 2, y: 0, w: 8, h: 4 });
expect(api.saveLayout).not.toHaveBeenCalled();
// Bearbeiten bleibt aus.
useDashboardStore.getState().setEditMode(true);
expect(useDashboardStore.getState().isEditMode).toBe(false);
// Auch ein ausdrueckliches Speichern (etwa nach einer Aenderung) schreibt nichts.
useDashboardStore.getState().updateLayouts({ lg: [{ i: 'a', x: 0, y: 0, w: 4, h: 4 }] });
await useDashboardStore.getState().saveLayout();
expect(api.saveLayout).not.toHaveBeenCalled();
});
it('Test 19: Wechsel von einem Reiter mit neuerer Marke speichert ihn nicht; ein normaler Reiter hebt den Merker wieder auf', async () => {
vi.mocked(api.fetchDashboards).mockResolvedValue([DASH_1, DASH_2]);
vi.mocked(api.fetchLayout).mockResolvedValueOnce({ ...FUTURE });
await useDashboardStore.getState().loadDashboard();
useDashboardStore.setState({ isDirty: true });
vi.mocked(api.fetchLayout).mockResolvedValueOnce({ ...EMPTY, __gridVersion: 3 });
await useDashboardStore.getState().selectDashboard('dash-2');
expect(api.saveLayout).not.toHaveBeenCalled();
expect(useDashboardStore.getState().layoutFromNewerVersion).toBe(false);
useDashboardStore.getState().setEditMode(true);
expect(useDashboardStore.getState().isEditMode).toBe(true);
});
it('Test 20: normale Anordnung -> Merker aus, Speichern wie gewohnt', async () => {
vi.mocked(api.fetchLayout).mockResolvedValue({ ...EMPTY, __gridVersion: 3 });
await useDashboardStore.getState().loadDashboard();
expect(useDashboardStore.getState().layoutFromNewerVersion).toBe(false);
await useDashboardStore.getState().saveLayout();
expect(api.saveLayout).toHaveBeenCalledTimes(1);
});
});
describe('dashboard-store — Breiten je Breakpoint (quick-260930)', () => {
it('Test 21: neue Kachel ist in keinem Breakpoint breiter als dessen Spaltenzahl', async () => {
await useDashboardStore.getState().loadDashboard();
useDashboardStore.setState({ layouts: { lg: [], md: [], sm: [], xs: [], xxs: [] } });
vi.mocked(api.addWidget).mockResolvedValue({ id: 's1', widgetType: 'search', config: {} });
await useDashboardStore.getState().addWidget('search');
const { layouts } = useDashboardStore.getState();
expect(layouts.lg[0].w).toBe(24);
expect(layouts.xs[0].w).toBe(16);
expect(layouts.xxs[0].w).toBe(4);
});
});
describe('dashboard-store — optimistische Kachel-Konfiguration (quick-260930)', () => {
it('Test 22: scheitert der erste von zwei schnellen Klicks, bleibt die zweite Wahl stehen', async () => {
useDashboardStore.setState({ widgets: [{ id: 'w1', widgetType: 'clock', config: {} }] });
let rejectFirst: (e: Error) => void = () => {};
vi.mocked(api.updateWidgetConfig)
.mockImplementationOnce(
() =>
new Promise((_resolve, reject) => {
rejectFirst = reject;
}),
)
.mockResolvedValueOnce(undefined as never);
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const first = useDashboardStore.getState().setWidgetConfig('w1', { hideTitle: true });
await useDashboardStore.getState().setWidgetConfig('w1', { hideTitle: false });
rejectFirst(new Error('PATCH failed'));
await first;
expect(useDashboardStore.getState().widgets[0].config).toEqual({ hideTitle: false });
expect(errorSpy).toHaveBeenCalledTimes(1);
});
it('Test 23: scheitert der einzige Klick, kommt der vorige Wert zurueck', async () => {
useDashboardStore.setState({
widgets: [{ id: 'w1', widgetType: 'clock', config: { hideTitle: false } }],
});
vi.mocked(api.updateWidgetConfig).mockRejectedValue(new Error('PATCH failed'));
vi.spyOn(console, 'error').mockImplementation(() => {});
await useDashboardStore.getState().setWidgetConfig('w1', { hideTitle: true });
expect(useDashboardStore.getState().widgets[0].config).toEqual({ hideTitle: false });
});
});
+42 -13
View File
@@ -1,8 +1,8 @@
import { create } from 'zustand';
import * as api from '@/lib/dashboard-api';
import type { DashboardTab } from '@/lib/dashboard-api';
import type { WidgetType } from '@/components/dashboard/widget-registry';
import { WIDGET_CONSTRAINTS } from '@/components/dashboard/widget-registry';
import { clampWidthToCols, WIDGET_CONSTRAINTS } from '@/components/dashboard/widget-registry';
import type { DashboardTab } from '@/lib/dashboard-api';
import * as api from '@/lib/dashboard-api';
import { migrateGridLayouts, withGridVersion } from '@/lib/grid-layout-migration';
export interface WidgetInstance {
@@ -21,6 +21,13 @@ interface DashboardState {
isDirty: boolean;
isLoading: boolean;
error: string | null;
/**
* Die geladene Anordnung traegt eine hoehere Rastermarke, als dieser
* Programmstand kennt (quick-260930) — ein neuerer Stand hat sie
* geschrieben. Solange das gilt, wird nie gespeichert und der
* Bearbeitungsmodus bleibt gesperrt; die Oberflaeche bittet ums Neuladen.
*/
layoutFromNewerVersion: boolean;
setEditMode: (mode: boolean) => void;
updateLayouts: (layouts: Record<string, unknown>) => void;
@@ -79,6 +86,12 @@ function loadDashboardsOnce(): Promise<DashboardTab[]> {
* Marker-Umrechnung oben gilt weiterhin je Reiter: `selectDashboard`
* durchlaeuft dieselbe Umrechnung-plus-Sofort-Speichern-Logik wie
* `loadDashboard`.
*
* quick-260930: Traegt die geladene Anordnung eine HOEHERE Marke als
* `GRID_VERSION` (ein neuerer Programmstand hat sie geschrieben, dieser Tab
* ist alt), setzt der Store `layoutFromNewerVersion`: `saveLayout` speichert
* dann nie, `setEditMode(true)` wird ignoriert, die Oberflaeche bittet ums
* Neuladen. Der Merker gilt je geladenem Reiter.
*/
export const useDashboardStore = create<DashboardState>()((set, get) => ({
dashboards: [],
@@ -90,8 +103,11 @@ export const useDashboardStore = create<DashboardState>()((set, get) => ({
isDirty: false,
isLoading: false,
error: null,
layoutFromNewerVersion: false,
setEditMode: (mode: boolean) => {
// quick-260930: Anordnung eines neueren Programmstands — nicht bearbeiten.
if (mode && get().layoutFromNewerVersion) return;
const prev = get().isEditMode;
set({ isEditMode: mode });
// On exit edit mode, auto-save if dirty (D-01)
@@ -125,7 +141,8 @@ export const useDashboardStore = create<DashboardState>()((set, get) => ({
i: newWidget.id,
x: 0,
y: maxY,
w: constraints.defaultW,
// quick-260930: nie breiter als das Raster dieses Breakpoints.
w: clampWidthToCols(constraints.defaultW, bp),
h: constraints.defaultH,
});
newLayouts[bp] = arr;
@@ -168,12 +185,18 @@ export const useDashboardStore = create<DashboardState>()((set, get) => ({
set((state) => ({
widgets: state.widgets.map((w) => (w.id === id ? { ...w, config } : w)),
}));
patch({ ...before, ...partial });
const optimistic = { ...before, ...partial };
patch(optimistic);
try {
await api.updateWidgetConfig(id, partial);
} catch (err) {
console.error('Failed to update widget config:', err);
patch(before);
// quick-260930: nur zuruecksetzen, wenn noch GENAU dieser optimistische
// Stand angezeigt wird. Hat inzwischen ein zweiter Klick etwas anderes
// gewaehlt, wuerde der Fehler des ersten sonst die zweite Wahl
// ueberschreiben.
const current = get().widgets.find((w) => w.id === id)?.config;
if (current === optimistic) patch(before);
}
},
@@ -191,13 +214,15 @@ export const useDashboardStore = create<DashboardState>()((set, get) => ({
api.fetchLayout(first.id),
api.fetchWidgets(first.id),
]);
const { layouts: migratedLayouts, migrated } = migrateGridLayouts(rawLayouts);
const { layouts: migratedLayouts, migrated, newer } = migrateGridLayouts(rawLayouts);
set({
dashboards,
activeDashboardId: first.id,
layouts: migratedLayouts,
widgets,
isLoading: false,
layoutFromNewerVersion: newer,
...(newer ? { isEditMode: false, isDirty: false } : {}),
});
// Umgerechnete Anordnung SOFORT mit Marker persistieren — nach dem set,
// damit die Oberflaeche unabhaengig vom Speichern rendert; eigener
@@ -220,6 +245,9 @@ export const useDashboardStore = create<DashboardState>()((set, get) => ({
saveLayout: async () => {
const dashboardId = get().activeDashboardId;
if (!dashboardId) return;
// quick-260930: NIE eine Anordnung eines neueren Programmstands speichern —
// sie ginge mit der alten Marke zurueck und wuerde doppelt skaliert.
if (get().layoutFromNewerVersion) return;
try {
await api.saveLayout(dashboardId, withGridVersion(get().layouts));
set({ isDirty: false });
@@ -246,17 +274,16 @@ export const useDashboardStore = create<DashboardState>()((set, get) => ({
set({ isSwitchingDashboard: true, error: null });
try {
const [rawLayouts, widgets] = await Promise.all([
api.fetchLayout(id),
api.fetchWidgets(id),
]);
const { layouts: migratedLayouts, migrated } = migrateGridLayouts(rawLayouts);
const [rawLayouts, widgets] = await Promise.all([api.fetchLayout(id), api.fetchWidgets(id)]);
const { layouts: migratedLayouts, migrated, newer } = migrateGridLayouts(rawLayouts);
set({
activeDashboardId: id,
layouts: migratedLayouts,
widgets,
isDirty: false,
isSwitchingDashboard: false,
layoutFromNewerVersion: newer,
...(newer ? { isEditMode: false } : {}),
});
if (migrated) {
try {
@@ -332,7 +359,9 @@ export const useDashboardStore = create<DashboardState>()((set, get) => ({
reorderDashboards: async (ids: string[]) => {
const previous = get().dashboards;
const byId = new Map(previous.map((d) => [d.id, d]));
const optimistic = ids.map((id) => byId.get(id)).filter((d): d is DashboardTab => d !== undefined);
const optimistic = ids
.map((id) => byId.get(id))
.filter((d): d is DashboardTab => d !== undefined);
set({ dashboards: optimistic });
try {