From 57e1426bb08e47e14087fc8b04e8fd09dea538a3 Mon Sep 17 00:00:00 2001 From: Martin Castro Laminrs Date: Mon, 1 Jun 2026 20:15:48 +0200 Subject: [PATCH] fix(spa): detail-page header in 3 stacked full-width rows (#658) + 1.8.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous header laid out breadcrumb + title + action toolbar on a single horizontal flex row, with breadcrumb + title on the left (``flex-1 min-w-0``) and the toolbar on the right (``shrink-0``, wrapping right-justified). Two failure modes followed: 1. **Long single-token titles collapsed to one-word-per-line at full H1 size.** A filename / slug / UUID like ``Bk stmt docs-pp0_4-bank_statement-2024_12_09-first_bank_and_trust.pdf`` in a narrow title column wrapped on every hyphen at full H1 size, filling the entire viewport vertically before any content showed. 2. **Many actions overflowed and pushed the title off-screen.** A ModelAdmin with 8+ ``@admin.action``s + Edit + Delete made the toolbar wider than the available row, eating the title column entirely. The user saw a wall of buttons but no record name. ## Fix: three stacked full-width rows row 1 — breadcrumb (full width) row 2 — H1 (full width, ``break-words`` so long tokens wrap inside the container instead of forcing each segment to its own line; ``text-balance`` for shorter multi-word titles) row 3 — toolbar (full width, ``flex-wrap`` so 8+ actions flow onto subsequent lines; primary actions Edit/Delete right-aligned via ``ml-auto`` on a cluster div so the destructive button stays at the trailing edge) Each concern now owns its own row — they never share horizontal space, so neither can crowd the other off-screen. Matches what classic Django admin and modern SPA admin frameworks (Refine, React-Admin, Retool) do; scales cleanly from 1 action to 20+ with no breakpoints. The primary-actions cluster (Refresh + Edit + Delete) is wrapped in its own ``ml-auto`` div with its own ``flex-wrap`` — so even when the leading @admin.action cluster wraps to multiple rows, Edit / Delete stay adjacent and at the trailing edge. The destructive Delete is never orphaned from the constructive Edit on a narrow viewport. ## Verification - ``pnpm test`` — 222 / 222 ✓ (no regressions; header changes are layout-only and not covered by existing component tests) - ``pnpm -r typecheck`` ✓ - ``pnpm lint`` ✓ Visual smoke-test pending CodeRabbit + the release pilot loop. Closes #658. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/apps/web/src/pages/DetailPage.tsx | 116 ++++++++++++--------- pyproject.toml | 2 +- 2 files changed, 68 insertions(+), 50 deletions(-) diff --git a/frontend/apps/web/src/pages/DetailPage.tsx b/frontend/apps/web/src/pages/DetailPage.tsx index c9680d0..ebc97a6 100644 --- a/frontend/apps/web/src/pages/DetailPage.tsx +++ b/frontend/apps/web/src/pages/DetailPage.tsx @@ -237,30 +237,40 @@ export function DetailPage({ return (
- {/* Header (#572): the title is the page's most important element - and gets as much horizontal space as it needs (`flex-1 - min-w-0`); the toolbar is `shrink-0` and only pushes the title - when it genuinely can't fit on its row. `justify-end` on the - toolbar's flex-wrap keeps wrapped button rows flush right to - the page padding, instead of left-aligned within their column. */} -
-
- ( - - {label} - - )} - /> -

{data.label}

-
+ {/* Header (#572 / #658): three stacked full-width rows so the + title never shares horizontal space with the toolbar — the + old side-by-side layout collapsed long single-token titles + (filenames, slugs, UUIDs) into one-word-per-line at full H1 + size, AND let an 8+ action toolbar push the title clean off + the viewport. Each concern now owns its own row: + + row 1: breadcrumb (full width, truncates on tight viewports) + row 2: H1 (full width, `overflow-wrap: anywhere` so a + single long token wraps inside the container) + row 3: toolbar (full width, `flex-wrap` so 8+ actions flow + to new lines; primary actions Edit/Delete + sit at the trailing edge via `ml-auto`) */} +
+ ( + + {label} + + )} + /> + {/* `break-words` (Tailwind's overflow-wrap: anywhere) keeps a + very long single-token title wrapping inside the container + at H1 size, instead of forcing each hyphen segment onto its + own line. `text-balance` rebalances the wrap for shorter + multi-word titles too. */} +

{data.label}

{!editing && ( -
+
- )} - {canDelete && ( - fetchDeletePreview({ client, appLabel, modelName, pk })} - onConfirm={async () => { - await deleteObject({ client, appLabel, modelName, pk }); - toast.success(`Deleted “${data.label}”.`); - navigate(listPath); - }} + {/* Primary-actions cluster (Refresh + Edit + Delete) is + grouped with ``ml-auto`` so it floats to the trailing + edge of the toolbar row even when the leading + ``@admin.action`` cluster wraps onto multiple lines. + ``flex-wrap`` on the cluster itself keeps Edit / Delete + together if the row is too narrow for the whole group + — destructive Delete stays adjacent to the constructive + Edit, never orphaned. */} +
+ {/* Refresh (#592): refetch the object + inlines + history + with no full page reload. */} + } /> - )} + {canChange && ( + + )} + {canDelete && ( + fetchDeletePreview({ client, appLabel, modelName, pk })} + onConfirm={async () => { + await deleteObject({ client, appLabel, modelName, pk }); + toast.success(`Deleted “${data.label}”.`); + navigate(listPath); + }} + /> + )} +
)}
diff --git a/pyproject.toml b/pyproject.toml index 30dc194..c0a8c1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "django-admin-react" -version = "1.8.0" +version = "1.8.1" description = "A drop-in React single-page admin for Django, driven entirely by ModelAdmin." authors = ["django-admin-react contributors"] license = "MIT"