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.
- **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.
- **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`.
- **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.