docs(quick-260922-m1h): Hinweis bei gesperrter Kachel, Changelog und Entwicklerdoku
- eine Kachel ohne Bauteil (entfernter Typ oder gesperrtes Modul) zeigt statt des rohen Typnamens den Satz `widgets.unavailable`, zentriert und grau; Schluessel in de.json und en.json - Entwicklerdoku: neuer Abschnitt "Eine Kachel zum Modul" im Modul-Walkthrough — die drei verbliebenen Stellen und was eine Kachel mit moduleSlug automatisch tut - Changelog unter "Unveroeffentlicht - Geaendert" Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T
|
||||
### Geändert
|
||||
|
||||
- Bilderrahmen: hochgeladene Bilder liegen jetzt im Dateibereich des Servers statt in der Datenbank — die Datenbanksicherung bleibt dadurch klein; vorhandene Bilder ziehen beim ersten Start automatisch um
|
||||
- Dashboard: Kacheln, die zu einem Modul gehören, erscheinen nur noch für Benutzer, die dieses Modul nutzen dürfen; eine nicht mehr freigegebene Kachel erklärt das jetzt, statt leer zu bleiben
|
||||
|
||||
## 1.3.0 – 2026-09-22
|
||||
|
||||
|
||||
@@ -3,15 +3,21 @@ import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// quick-260916-iex: Link-Widget entfernt — unbekannte Widget-Typen (z. B.
|
||||
// eine alte Link-Kachel vor dem Einspielen der Migration) muessen weiterhin
|
||||
// ohne Absturz als grauer Text gerendert werden.
|
||||
// ohne Absturz gerendert werden.
|
||||
// quick-260922-m1h: Statt des rohen Typnamens steht dort jetzt ein Satz, der
|
||||
// den Fall erklaert — derselbe Fall tritt kuenftig auf, wenn eine Kachel zu
|
||||
// einem Modul gehoert, das dem Benutzer nicht freigegeben ist.
|
||||
vi.mock('next-intl', () => ({
|
||||
useTranslations: () => (key: string) => key,
|
||||
useTranslations: () => (key: string) =>
|
||||
key === 'unavailable'
|
||||
? 'Diese Kachel steht nicht zur Verfügung — das zugehörige Modul ist nicht freigegeben.'
|
||||
: key,
|
||||
}));
|
||||
|
||||
import { WidgetWrapper } from './widget-wrapper';
|
||||
|
||||
describe('WidgetWrapper', () => {
|
||||
it('unbekannter Widget-Typ (z. B. eine alte Link-Kachel vor der Migration) rendert als grauer Text ohne Absturz', () => {
|
||||
it('unbekannter/gesperrter Widget-Typ erklaert sich mit einem Hinweistext statt leer oder als roher Typname zu rendern', () => {
|
||||
render(
|
||||
<WidgetWrapper
|
||||
widget={{ id: 'w-alt', widgetType: 'link', config: {} }}
|
||||
@@ -23,7 +29,29 @@ describe('WidgetWrapper', () => {
|
||||
const article = screen.getByRole('article');
|
||||
expect(article).toHaveAttribute('aria-label', 'link');
|
||||
|
||||
const fallback = screen.getByText('link');
|
||||
expect(fallback.className).toContain('text-muted-foreground');
|
||||
const hint = screen.getByText(
|
||||
'Diese Kachel steht nicht zur Verfügung — das zugehörige Modul ist nicht freigegeben.',
|
||||
);
|
||||
expect(hint.className).toContain('text-muted-foreground');
|
||||
expect(hint.className).toContain('text-center');
|
||||
|
||||
// Der rohe Typname steht nicht mehr im Rumpf der Kachel.
|
||||
expect(screen.queryByText('link')).toBeNull();
|
||||
});
|
||||
|
||||
it('eine bekannte Kachel rendert weiterhin ihre Komponente, nicht den Hinweis', () => {
|
||||
render(
|
||||
<WidgetWrapper
|
||||
widget={{ id: 'w-uhr', widgetType: 'clock', config: {} }}
|
||||
isEditMode={false}
|
||||
onRemove={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByText(
|
||||
'Diese Kachel steht nicht zur Verfügung — das zugehörige Modul ist nicht freigegeben.',
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,8 +111,14 @@ export function WidgetWrapper({ widget, isEditMode, onRemove }: WidgetWrapperPro
|
||||
isEditMode={isEditMode}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
{widget.widgetType}
|
||||
/* quick-260922-m1h: Kein Bauteil zu diesem Typ — entweder eine alte
|
||||
Kachel eines entfernten Typs oder (ab der ersten Modul-Kachel) eine
|
||||
Kachel, deren Modul dem Benutzer nicht freigegeben ist. Vorher
|
||||
stand hier der rohe Typname, der dem Anwender nichts sagte. */
|
||||
<div className="flex h-full items-center justify-center p-3">
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
{t('unavailable')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -213,6 +213,7 @@
|
||||
"saveChanges": "Änderungen speichern",
|
||||
"layoutLoadError": "Dashboard konnte nicht geladen werden. Bitte laden Sie die Seite neu.",
|
||||
"widgetSaveError": "Änderungen konnten nicht gespeichert werden. Bitte versuchen Sie es erneut.",
|
||||
"unavailable": "Diese Kachel steht nicht zur Verfügung — das zugehörige Modul ist nicht freigegeben.",
|
||||
"clock": {
|
||||
"name": "Uhr",
|
||||
"description": "Zeigt die aktuelle Uhrzeit an",
|
||||
|
||||
@@ -213,6 +213,7 @@
|
||||
"saveChanges": "Save changes",
|
||||
"layoutLoadError": "Could not load dashboard. Please reload the page.",
|
||||
"widgetSaveError": "Could not save changes. Please try again.",
|
||||
"unavailable": "This tile is not available — the module it belongs to is not enabled for you.",
|
||||
"clock": {
|
||||
"name": "Clock",
|
||||
"description": "Shows the current time",
|
||||
|
||||
@@ -400,6 +400,40 @@ Für ein Modul mit Unterrouten (Einstellungsseite, Verwaltungsansicht) orientier
|
||||
`dkv-fleet` oder `tender-radar` — beide haben zusätzliche `settings/page.tsx` bzw. weitere
|
||||
Unterverzeichnisse, die vom selben `layout.tsx` mitgedeckt werden.
|
||||
|
||||
### Eine Kachel zum Modul
|
||||
|
||||
Ein Modul kann zusätzlich als Kachel auf dem Dashboard erscheinen. Seit
|
||||
`quick-260922-m1h` sind dafür **drei** Stellen nötig (vorher waren es sieben):
|
||||
|
||||
1. **Die Kachel-Komponente schreiben** — `apps/web/src/components/dashboard/widgets/<name>-widget.tsx`,
|
||||
nimmt die `WidgetProps` aus `widget-registry.tsx` (`instanceId`, `config`, `isEditMode`) entgegen.
|
||||
2. **Den Typ eintragen** — in `WIDGET_TYPES` in `packages/shared/src/index.ts`. Gehört die Kachel zu
|
||||
einem Modul, zusätzlich `WIDGET_MODULE_SLUGS['<typ>'] = '<modul-slug>'` in derselben Datei. Das ist
|
||||
die einzige Liste: die API validiert `POST /dashboard/widgets` per `@IsIn` gegen genau sie, und
|
||||
das Frontend leitet Registry und Katalog davon ab.
|
||||
3. **Anmelden** — `registerWidget('<typ>', <Name>Widget)` in `apps/web/src/app/(portal)/page.tsx`,
|
||||
neben den übrigen Aufrufen. Der Aufruf steht dort und nicht in der Registry, weil die Kachel über
|
||||
den Wrapper wieder die Registry importiert — ein Import aus der Registry heraus wäre ein
|
||||
Zirkelimport.
|
||||
|
||||
Dazu kommen wie bei jeder Oberfläche die **Übersetzungsschlüssel** (`<typ>.name` und
|
||||
`<typ>.description` unter `widgets` in `de.json` **und** `en.json`), ein **Symbol** als Inline-SVG
|
||||
und die **Größenvorgaben** in `WIDGET_CONSTRAINTS` (`minW`/`minH` = kleinste noch bedienbare Kachel,
|
||||
`defaultW`/`defaultH` = Startgröße) — beides in `widget-registry.tsx`. Ein Test in
|
||||
`widget-registry.test.tsx` prüft, dass `WIDGET_TYPES`, Registry und Constraints deckungsgleich sind;
|
||||
vergisst man eine Stelle, schlägt er fehl.
|
||||
|
||||
**Was eine Kachel mit `moduleSlug` automatisch tut:** Sie verschwindet für Benutzer, die das Modul
|
||||
nicht nutzen dürfen — aus dem Katalog („Widget hinzufügen", `visibleWidgetTypes`) und aus dem
|
||||
Dashboard selbst. Ist die Modulliste unbekannt, weil ihr Abruf fehlschlug, bleibt die Kachel
|
||||
ebenfalls verborgen (fail-closed). Eine bereits angelegte Kachel eines gesperrten Moduls rendert
|
||||
nicht mehr leer, sondern zeigt den Hinweis `widgets.unavailable`.
|
||||
|
||||
**Wie beim Modul-Gate gilt auch hier:** Der Katalogfilter ist Komfort, nicht Zugriffskontrolle. Die
|
||||
verbindliche Prüfung sitzt serverseitig in `DashboardService.getWidgets()` (filtert Kacheln
|
||||
gesperrter Module fail-closed aus `GET /dashboard/widgets`) — und die Daten, die eine Modul-Kachel
|
||||
anzeigt, holt sie über die Endpunkte ihres Moduls, die `@UseModule('<slug>')` tragen müssen.
|
||||
|
||||
## Mandantentrennung
|
||||
|
||||
Der tatsächliche Mechanismus ist `TenantGuard` (`apps/api/src/tenant/tenant.guard.ts`), global als
|
||||
|
||||
Reference in New Issue
Block a user