feat(quick-260923-ad9): Reiterleiste in der Oberflaeche - Task 3

dashboard-api.ts: fuenf neue Abrufe fuer die Reiter-Wege, die vier
bestehenden Aufrufe (Layout/Widgets lesen/schreiben) tragen jetzt die
Reiter-Kennung (Abfrageparameter bzw. Rumpf).

dashboard-store.ts: Zustand um dashboards/activeDashboardId/
isSwitchingDashboard erweitert. loadDashboard() holt zuerst die Reiter,
macht den ersten aktiv, laedt erst danach dessen Inhalt; ein modul-globales
Versprechen schuetzt gegen doppeltes Laden der Reiterliste bei doppeltem
Einhaengen. selectDashboard() schreibt eine ungespeicherte Anordnung ZUERST
fuer den alten Reiter (Kennung vor dem Wechsel gelesen) und ersetzt danach
Kacheln/Anordnung vollstaendig. createDashboard/renameDashboard/
deleteDashboard pflegen Reiterliste und aktiven Reiter; Loeschen des
aktiven Reiters macht den dann ersten Reiter aktiv. Die Marker-Umrechnung
(quick-260916-bwo) laeuft unveraendert je Reiter mit, auch beim Wechsel.

dashboard-tabs.tsx (neu): Reiterleiste, Klick wechselt immer; im
Bearbeitungsmodus zusaetzlich Anlegen, Umbenennen (an Ort und Stelle,
Enter/Escape) und Loeschen (mit Rueckfrage) - Loeschen-Knopf fehlt beim
letzten verbleibenden Reiter (D-10). Fokus beim Umbenennen ueber einen Ref
statt autoFocus (lint/a11y/noAutofocus).

(portal)/page.tsx: Leiste ueber dem Raster, kurze Ladezeile waehrend eines
Reiterwechsels statt des Rasters - die Leiste bleibt stehen. DashboardGrid
selbst unveraendert (D-06).

Uebersetzungen: neue Schluessel unter widgets.tabs.* in de.json/en.json,
Dialog-Knoepfe nutzen die vorhandenen common.cancel/common.delete.

Deviations (Rule 3 - blockierende Nachwirkung dieses Tasks, ausserhalb der
files_modified-Liste, aber direkt durch die dashboardId-Pflicht verursacht):
- settings/dashboard/page.tsx: fetchWidgets() verlangt jetzt eine
  Reiter-Kennung; die Seite ist nicht reiterbewusst (ausserhalb des
  Umfangs) und zeigt jetzt die Kacheln des ERSTEN Reiters - deckungsgleich
  mit dem bisherigen Verhalten fuer den haeufigen Fall genau eines Reiters.
- (portal)/page.test.tsx: mockStore brauchte die neuen Reiter-Felder/
  -Methoden, sonst waere DashboardTabs auf `dashboards.map` von undefined
  gescheitert.

Tests: dashboard-store.test.ts 15 (6 alte angepasste Signaturen + 9 neue),
dashboard-tabs.test.tsx 12 (neu). web gesamt 682 Tests in 82 Dateien,
dashboard-grid.test.tsx unveraendert bei 12. type-check 4/4, lint 5/5 mit
weiterhin genau 53 Warnungen in web.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 08:10:45 +02:00
parent df7a5e7e8e
commit d34f682c28
10 changed files with 999 additions and 40 deletions
+10
View File
@@ -20,6 +20,12 @@ vi.mock('next-intl', () => ({
}));
const mockStore = {
// quick-260923-ad9: Reiter — leere Liste rendert die Reiterleiste ohne
// Reiter-Knoepfe, damit diese Datei bei den bestehenden Zusammensetzungs-
// Pruefungen bleibt, statt den Reiter-Speicher selbst nachzubauen.
dashboards: [] as Array<{ id: string; name: string; position: number }>,
activeDashboardId: null as string | null,
isSwitchingDashboard: false,
layouts: { lg: [], md: [], sm: [], xs: [], xxs: [] },
widgets: [] as Array<{ id: string; widgetType: string; config: Record<string, unknown> }>,
isEditMode: false,
@@ -32,6 +38,10 @@ const mockStore = {
removeWidget: vi.fn(),
loadDashboard: vi.fn(),
saveLayout: vi.fn(),
selectDashboard: vi.fn(),
createDashboard: vi.fn(),
renameDashboard: vi.fn(),
deleteDashboard: vi.fn(),
};
vi.mock('@/lib/stores/dashboard-store', () => ({
+33 -6
View File
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useState } from 'react';
import { useTranslations } from 'next-intl';
import { DashboardGrid } from '@/components/dashboard/dashboard-grid';
import { DashboardTabs } from '@/components/dashboard/dashboard-tabs';
import { EditModeToggle } from '@/components/dashboard/edit-mode-toggle';
import { WidgetCatalogModal } from '@/components/dashboard/widget-catalog-modal';
import { registerWidget } from '@/components/dashboard/widget-registry';
@@ -48,6 +49,9 @@ export default function DashboardPage() {
const [accessibleModuleSlugs, setAccessibleModuleSlugs] = useState<string[] | null>(null);
const {
dashboards,
activeDashboardId,
isSwitchingDashboard,
layouts,
widgets,
isEditMode,
@@ -58,6 +62,10 @@ export default function DashboardPage() {
addWidget,
removeWidget,
loadDashboard,
selectDashboard,
createDashboard,
renameDashboard,
deleteDashboard,
} = useDashboardStore();
// Load dashboard data on mount
@@ -105,15 +113,34 @@ export default function DashboardPage() {
return (
<div className="relative p-2">
{/* Dashboard grid — direkt im Container, ohne Abstands-Wrapper. */}
<DashboardGrid
layouts={layouts}
widgets={widgets}
{/* Reiterleiste (quick-260923-ad9) — bleibt waehrend eines
Reiterwechsels stehen, nur das Raster darunter zeigt eine kurze
Ladezeile. */}
<DashboardTabs
dashboards={dashboards}
activeDashboardId={activeDashboardId}
isEditMode={isEditMode}
onLayoutChange={updateLayouts}
onRemoveWidget={removeWidget}
onSelect={selectDashboard}
onCreate={createDashboard}
onRename={renameDashboard}
onDelete={deleteDashboard}
/>
{isSwitchingDashboard ? (
<div className="flex min-h-[40vh] items-center justify-center">
<p className="text-sm text-muted-foreground">{t('tabs.switching')}</p>
</div>
) : (
/* Dashboard grid — direkt im Container, ohne Abstands-Wrapper. */
<DashboardGrid
layouts={layouts}
widgets={widgets}
isEditMode={isEditMode}
onLayoutChange={updateLayouts}
onRemoveWidget={removeWidget}
/>
)}
{/* Feste Aktionsleiste unten rechts.
quick-260916-dyv: Umschalter unten rechts statt oben rechts, damit das
Grid direkt unter der Kopfzeile beginnt (12 + 8 + 8 = 28 px statt 60 px).
@@ -3,11 +3,20 @@
import { useEffect, useState } from 'react';
import { useTranslations } from 'next-intl';
import { WidgetSettingsPanel } from '@/components/settings/widget-settings-panel';
import { fetchWidgets } from '@/lib/dashboard-api';
import { fetchDashboards, fetchWidgets } from '@/lib/dashboard-api';
/**
* Settings > Dashboard > Widgets page (D-03).
* Fetches user's widget instances and renders per-instance config forms.
* Fetches the widget instances of the user's FIRST dashboard tab (the
* standard tab, quick-260923-ad9) and renders per-instance config forms.
*
* quick-260923-ad9 (Task 3, deviation Rule 3 — blocking compile issue):
* `fetchWidgets` now requires a `dashboardId` since widgets are scoped per
* tab. This page is not itself tab-aware (out of this plan's scope — see
* "Nicht im Umfang") and previously showed every widget the user had, back
* when there was exactly one dashboard per user; it now shows the first
* tab's widgets, which is the same set for the (overwhelmingly common)
* case of a user who has not yet created a second tab.
*/
export default function WidgetSettingsPage() {
const t = useTranslations('settings');
@@ -18,12 +27,17 @@ export default function WidgetSettingsPage() {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetchWidgets()
.then(setWidgets)
.catch(() => {
(async () => {
try {
const dashboards = await fetchDashboards();
const first = dashboards[0];
setWidgets(first ? await fetchWidgets(first.id) : []);
} catch {
// Silent fail — empty widget list shown
})
.finally(() => setIsLoading(false));
} finally {
setIsLoading(false);
}
})();
}, []);
return (