chore(drawer): close consolidation gaps after three-consumer rollout

Loose ends surfaced by comparing the three drawer consumers side by side:

- Core i18n was missing `Open full page`, `Loading`, `Failed to load` —
  admin/users showed English strings in the German UI because those labels
  resolve from core, not module catalogs.
- Helpdesk tickets page config did not pass drawer labels; the JS fell back
  to its hard-coded English defaults. Labels now wired via `t(...)`.
- CLAUDE.md notes the `fetchUrl` single-expression convention (required so
  the architecture test can statically locate the fragment path) and
  documents the `onContentLoaded` hook for module-specific runtime
  (helpdesk's async communication feed uses it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 17:25:07 +02:00
parent 22966fdacf
commit f793f5d493
4 changed files with 14 additions and 1 deletions

View File

@@ -178,7 +178,8 @@ docker/ # Dockerfiles, Nginx configs, PHP configs
- **Detail drawer**: Row-level detail views that slide in from the right use the core primitive — no per-module drawer implementation. - **Detail drawer**: Row-level detail views that slide in from the right use the core primitive — no per-module drawer implementation.
- **Server:** Action `<route>/<name>-fragment($id).php` + view `<route>/<name>-fragment(none).phtml`. The `(none)` template renders without layout/topbar/breadcrumbs. Reuse the same partial that the full-page view uses (extract a `modules/<id>/templates/<name>-profile.phtml` if needed). Must enforce auth + scope exactly like the full-page view. - **Server:** Action `<route>/<name>-fragment($id).php` + view `<route>/<name>-fragment(none).phtml`. The `(none)` template renders without layout/topbar/breadcrumbs. Reuse the same partial that the full-page view uses (extract a `modules/<id>/templates/<name>-profile.phtml` if needed). Must enforce auth + scope exactly like the full-page view.
- **Route:** Declare `['path' => '<route>/<name>-fragment', 'target' => '<route>/<name>-fragment']` in the module manifest. - **Route:** Declare `['path' => '<route>/<name>-fragment', 'target' => '<route>/<name>-fragment']` in the module manifest.
- **Client:** `initDetailDrawer({ gridConfig, fetchUrl: (uuid) => new URL('<route>/<name>-fragment/' + uuid, appBase).toString(), fullUrl, hashPrefix, rowProvider? })` from `/js/components/app-detail-drawer.js`. The drawer handles focus-trap, body-scroll-lock, session-expiry reload, and auto-initializes tabs/tooltips/fslightbox in the fragment via `initFragmentContent()`. Consumers do NOT need to call `initTabs`, `refreshFsLightbox`, etc. manually. - **Client:** `initDetailDrawer({ gridConfig, fetchUrl: (uuid) => new URL(`<route>/<name>-fragment/${uuid}`, appBase).toString(), fullUrl, hashPrefix, rowProvider?, onContentLoaded? })` from `/js/components/app-detail-drawer.js`. The drawer handles focus-trap, body-scroll-lock, session-expiry reload, and auto-initializes tabs/tooltips/fslightbox in the fragment via `initFragmentContent()`. Consumers do NOT need to call `initTabs`, `refreshFsLightbox`, etc. manually. For module-specific runtime (e.g. async communication feeds) use `onContentLoaded(contentEl, uuid)`.
- **`fetchUrl` signature:** write it as a single-expression arrow (not a block body) so the architecture test can statically locate the fragment path. Pattern: ``(uuid) => new URL(`path/${uuid}...`, appBase).toString()``.
- **Architecture test** `DetailDrawerFragmentContractTest` enforces that every `initDetailDrawer({ fetchUrl })` has matching `*-fragment($id).php` + `*-fragment(none).phtml`. - **Architecture test** `DetailDrawerFragmentContractTest` enforces that every `initDetailDrawer({ fetchUrl })` has matching `*-fragment($id).php` + `*-fragment(none).phtml`.
- **Filename rule:** URL parameters go only into the `.php` action filename (`*-fragment($id).php`). The `(none).phtml` view filename must NOT contain `($id)` — MintyPHP's router parses paren groups and breaks silently on nested ones. - **Filename rule:** URL parameters go only into the `.php` action filename (`*-fragment($id).php`). The `(none).phtml` view filename must NOT contain `($id)` — MintyPHP's router parses paren groups and breaks silently on nested ones.

View File

@@ -2,6 +2,9 @@
"Login": "Login", "Login": "Login",
"Login credentials": "Mit Zugangsdaten oder SSO anmelden", "Login credentials": "Mit Zugangsdaten oder SSO anmelden",
"Login provider": "Login-Anbieter", "Login provider": "Login-Anbieter",
"Open full page": "Vollseite öffnen",
"Loading": "Lädt",
"Failed to load": "Laden fehlgeschlagen",
"Register": "Registrieren", "Register": "Registrieren",
"Password": "Passwort", "Password": "Passwort",
"Or": "Oder", "Or": "Oder",

View File

@@ -2,6 +2,9 @@
"Login": "Login", "Login": "Login",
"Login credentials": "Login credentials", "Login credentials": "Login credentials",
"Login provider": "Login provider", "Login provider": "Login provider",
"Open full page": "Open full page",
"Loading": "Loading",
"Failed to load": "Failed to load",
"Register": "Register", "Register": "Register",
"Password": "Password", "Password": "Password",
"Or": "Or", "Or": "Or",

View File

@@ -351,6 +351,12 @@ $searchConfig = is_array($searchConfig ?? null) ? $searchConfig : null;
'support' => t('Support'), 'support' => t('Support'),
'created' => t('Created'), 'created' => t('Created'),
'lastActivity' => t('Last activity'), 'lastActivity' => t('Last activity'),
'drawerClose' => t('Close'),
'drawerPrev' => t('Previous'),
'drawerNext' => t('Next'),
'drawerOpenFull' => t('Open full page'),
'drawerLoading' => t('Loading'),
'drawerError' => t('Failed to load'),
], ],
'gridSearch' => $searchConfig, 'gridSearch' => $searchConfig,
'filterSchema' => $clientFilterSchema, 'filterSchema' => $clientFilterSchema,