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:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user