From 4d1bd13a3b5737b0fe7d742628e9ba87ffc9354b Mon Sep 17 00:00:00 2001 From: fs Date: Sat, 25 Apr 2026 19:06:06 +0200 Subject: [PATCH] refactor(ui): move detail drawer chrome to a server-rendered partial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The drawer chrome used to be built in JS via a 50-line innerHTML string and needed every caller to plumb through a labels object sourced from per-page PHP config. That diverged from the codebase convention — the confirm dialog, search dialog and session warning are all PHP partials mounted once in templates/default.phtml. The drawer now follows the same pattern: templates/partials/app-detail-drawer.phtml renders the markup with t() labels, default.phtml mounts it inside the logged-in shell, and the JS only attaches behavior via the [data-detail-drawer] selector. Knock-on cleanup: ensureDrawerElement and resolveLabels disappear from the JS, all three initDetailDrawer callers (admin/users, address book, helpdesk debitor) drop their labels parameter, and the matching dead 'drawerClose'/'drawerPrev'/… entries leave the per-page PHP grid configs. The drawer also gains a clean fallback when the partial is missing (console warn + null return), so logged-out edges can't crash. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../pages/address-book/index(default).phtml | 6 -- .../web/js/pages/address-book-index.js | 8 -- .../pages/helpdesk/debitor(default).phtml | 6 -- modules/helpdesk/web/js/helpdesk-detail.js | 8 -- pages/admin/users/index(default).phtml | 6 -- templates/default.phtml | 1 + templates/partials/app-detail-drawer.phtml | 57 ++++++++++++++ web/js/components/app-detail-drawer.js | 76 +++---------------- web/js/pages/admin-users-index.js | 8 -- 9 files changed, 69 insertions(+), 107 deletions(-) create mode 100644 templates/partials/app-detail-drawer.phtml diff --git a/modules/addressbook/pages/address-book/index(default).phtml b/modules/addressbook/pages/address-book/index(default).phtml index a5499f7..248313e 100644 --- a/modules/addressbook/pages/address-book/index(default).phtml +++ b/modules/addressbook/pages/address-book/index(default).phtml @@ -255,12 +255,6 @@ $pageConfig = [ 'sendEmail' => t('Send email'), 'call' => t('Call'), 'extraAssignments' => t('further assignments'), - 'drawerClose' => t('Close'), - 'drawerPrev' => t('Previous'), - 'drawerNext' => t('Next'), - 'drawerOpenFull' => t('Open full page'), - 'drawerLoading' => t('Loading'), - 'drawerError' => t('Failed to load'), ], ]; ?> diff --git a/modules/addressbook/web/js/pages/address-book-index.js b/modules/addressbook/web/js/pages/address-book-index.js index 64741fe..092be60 100644 --- a/modules/addressbook/web/js/pages/address-book-index.js +++ b/modules/addressbook/web/js/pages/address-book-index.js @@ -205,14 +205,6 @@ createListPageModule({ fetchUrl: (uuid) => new URL(`address-book/view-fragment/${uuid}`, appBase).toString(), fullUrl: (uuid) => withCurrentListReturn(new URL(`address-book/view/${uuid}`, appBase).toString()), hashPrefix: 'user', - labels: { - close: labels.drawerClose || 'Close', - previous: labels.drawerPrev || 'Previous', - next: labels.drawerNext || 'Next', - openFull: labels.drawerOpenFull || 'Open full page', - loading: labels.drawerLoading || 'Loading', - error: labels.drawerError || 'Failed to load', - }, }); return gridConfig; diff --git a/modules/helpdesk/pages/helpdesk/debitor(default).phtml b/modules/helpdesk/pages/helpdesk/debitor(default).phtml index 34c6d06..25b2109 100644 --- a/modules/helpdesk/pages/helpdesk/debitor(default).phtml +++ b/modules/helpdesk/pages/helpdesk/debitor(default).phtml @@ -351,12 +351,6 @@ $searchConfig = is_array($searchConfig ?? null) ? $searchConfig : null; 'support' => t('Support'), 'created' => t('Created'), '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, 'filterSchema' => $clientFilterSchema, diff --git a/modules/helpdesk/web/js/helpdesk-detail.js b/modules/helpdesk/web/js/helpdesk-detail.js index 5f398e8..3314c28 100644 --- a/modules/helpdesk/web/js/helpdesk-detail.js +++ b/modules/helpdesk/web/js/helpdesk-detail.js @@ -848,14 +848,6 @@ function init(container) { onContentLoaded: (contentEl) => { try { initHelpdeskTicket(contentEl); } catch (err) { console.warn('[helpdesk-detail] initHelpdeskTicket failed', err); } }, - labels: { - close: labels.drawerClose || 'Close', - previous: labels.drawerPrev || 'Previous', - next: labels.drawerNext || 'Next', - openFull: labels.drawerOpenFull || 'Open full page', - loading: labels.drawerLoading || 'Loading', - error: labels.drawerError || 'Failed to load', - }, }); } finally { ticketsGridInitializing = false; diff --git a/pages/admin/users/index(default).phtml b/pages/admin/users/index(default).phtml index 38045cb..a27ab97 100644 --- a/pages/admin/users/index(default).phtml +++ b/pages/admin/users/index(default).phtml @@ -131,12 +131,6 @@ $pageConfig = [ 'bulkSendAccessConfirm' => t('Send access emails to selected users?'), 'bulkAccessPdfConfirm' => t('Generate access PDFs for selected users?'), 'primaryTenant' => t('Primary tenant'), - 'drawerClose' => t('Close'), - 'drawerPrev' => t('Previous'), - 'drawerNext' => t('Next'), - 'drawerOpenFull' => t('Open full page'), - 'drawerLoading' => t('Loading'), - 'drawerError' => t('Failed to load'), 'bulkMessages' => [ 'activate' => [ 'success' => t('%d users activated'), diff --git a/templates/default.phtml b/templates/default.phtml index 6652a0e..b176158 100644 --- a/templates/default.phtml +++ b/templates/default.phtml @@ -340,6 +340,7 @@ $componentPageConfig['moduleRuntimeComponents'] = $moduleRuntimeComponents; + + diff --git a/web/js/components/app-detail-drawer.js b/web/js/components/app-detail-drawer.js index 3dda705..394e457 100644 --- a/web/js/components/app-detail-drawer.js +++ b/web/js/components/app-detail-drawer.js @@ -2,6 +2,10 @@ * Reusable detail drawer — slides in from the right, loads a fragment URL, * supports deep-link via URL hash, and can step through rows on the current * grid page. + * + * The drawer chrome is rendered server-side by templates/partials/app-detail-drawer.phtml + * (mounted once in the logged-in shell). This module only attaches behavior + * via the [data-detail-drawer] selector and never injects markup. */ import { getHtml, SessionExpiredError } from '../core/app-http.js'; @@ -10,66 +14,6 @@ import { createFocusTrap, createScrollLock } from '../core/app-focus-trap.js'; const STATE_KEY = Symbol('detail-drawer-state'); -function resolveLabels(labels = {}) { - return { - close: labels.close || 'Close', - previous: labels.previous || 'Previous', - next: labels.next || 'Next', - openFull: labels.openFull || 'Open full page', - loading: labels.loading || 'Loading', - error: labels.error || 'Failed to load', - }; -} - -function ensureDrawerElement(labels) { - let root = document.querySelector('[data-detail-drawer]'); - if (root instanceof HTMLElement) { - return root; - } - root = document.createElement('div'); - root.className = 'app-detail-drawer'; - root.setAttribute('data-detail-drawer', ''); - root.setAttribute('role', 'dialog'); - root.setAttribute('aria-modal', 'true'); - root.setAttribute('aria-hidden', 'true'); - root.hidden = true; - root.innerHTML = ` -
- - `; - document.body.appendChild(root); - return root; -} - /** Default row-uuid discovery: scrapes the visible Grid.js rows on the page. */ function makeGridRowProvider(gridConfig, rowUuidAttr) { if (!gridConfig) { return null; } @@ -96,7 +40,6 @@ export function initDetailDrawer(options = {}) { fullUrl, hashPrefix = 'detail', onContentLoaded = null, - labels: labelOverrides = {}, } = options; if (typeof fetchUrl !== 'function') { @@ -104,15 +47,18 @@ export function initDetailDrawer(options = {}) { return null; } + const root = document.querySelector('[data-detail-drawer]'); + if (!(root instanceof HTMLElement)) { + console.warn('[detail-drawer] [data-detail-drawer] not in DOM — partial not mounted?'); + return null; + } + if (root[STATE_KEY]) { return root[STATE_KEY]; } + const effectiveRowProvider = typeof rowProvider === 'function' ? rowProvider : makeGridRowProvider(gridConfig, rowUuidAttr); const stepperEnabled = typeof effectiveRowProvider === 'function'; - const labels = resolveLabels(labelOverrides); - const root = ensureDrawerElement(labels); - if (root[STATE_KEY]) { return root[STATE_KEY]; } - const panel = root.querySelector('[data-detail-drawer-panel]'); const backdrop = root.querySelector('[data-detail-drawer-backdrop]'); const closeButton = root.querySelector('[data-detail-drawer-close]'); diff --git a/web/js/pages/admin-users-index.js b/web/js/pages/admin-users-index.js index 50eab94..89b35bf 100644 --- a/web/js/pages/admin-users-index.js +++ b/web/js/pages/admin-users-index.js @@ -199,14 +199,6 @@ createListPageModule({ fetchUrl: (uuid) => new URL(`admin/users/view-fragment/${uuid}`, appBase).toString(), fullUrl: (uuid) => withCurrentListReturn(new URL(`admin/users/edit/${uuid}`, appBase).toString()), hashPrefix: 'user', - labels: { - close: labels.drawerClose || 'Close', - previous: labels.drawerPrev || 'Previous', - next: labels.drawerNext || 'Next', - openFull: labels.drawerOpenFull || 'Open full page', - loading: labels.drawerLoading || 'Loading', - error: labels.drawerError || 'Failed to load', - }, }); return usersGridConfig;