Skip to content

Commit 7613b34

Browse files
feat(spa): toolbar polish — one toolbar per page, icon-only History, leading icons on Edit/Delete (#610)
Closes #608. Patch — UX polish, no API change.
1 parent 518ba88 commit 7613b34

3 files changed

Lines changed: 68 additions & 59 deletions

File tree

frontend/apps/web/src/pages/DetailPage.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@
1010
// Edit/Delete are gated by the `permissions` block the API returns.
1111

1212
import { useCallback, useEffect, useRef, useState } from 'react';
13-
import { ChevronDown, Clock, ExternalLink, RefreshCw } from 'lucide-react';
13+
import {
14+
ChevronDown,
15+
Clock,
16+
ExternalLink,
17+
Pencil,
18+
RefreshCw,
19+
Trash2,
20+
} from 'lucide-react';
1421
import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom';
1522

1623
import {
@@ -255,9 +262,15 @@ export function DetailPage({
255262
<button
256263
type="button"
257264
onClick={() => setHistoryOpen(true)}
258-
className="inline-flex items-center gap-1.5 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
265+
aria-label="History"
266+
title="History"
267+
// Icon-only (#608) — the clock icon already speaks for
268+
// itself alongside the other small icon-only buttons
269+
// (Refresh, the per-object actions); the "History"
270+
// label was redundant chrome on a crowded header.
271+
className="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-2 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
259272
>
260-
<Clock className="h-4 w-4" aria-hidden /> History
273+
<Clock className="h-4 w-4" aria-hidden />
261274
</button>
262275
{data.view_on_site_url && (
263276
<a
@@ -307,7 +320,9 @@ export function DetailPage({
307320
/>
308321
{canChange && (
309322
<Button variant="primary" onClick={() => setEditing(true)}>
310-
Edit
323+
<span className="inline-flex items-center gap-1.5">
324+
<Pencil className="h-4 w-4" aria-hidden /> Edit
325+
</span>
311326
</Button>
312327
)}
313328
{canDelete && (
@@ -859,7 +874,9 @@ function DeleteButton({ label, loadPreview, onConfirm }: DeleteButtonProps) {
859874
return (
860875
<>
861876
<Button variant="danger" onClick={() => setOpen(true)}>
862-
Delete
877+
<span className="inline-flex items-center gap-1.5">
878+
<Trash2 className="h-4 w-4" aria-hidden /> Delete
879+
</span>
863880
</Button>
864881
{open && (
865882
<Modal

frontend/apps/web/src/pages/ListPage.tsx

Lines changed: 45 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -519,18 +519,53 @@ export function ListPage() {
519519
/>
520520
<h1 className="text-2xl font-semibold">{listTitle}</h1>
521521
</div>
522-
{/* Header right-side: only the +Add primary action lives here.
523-
Customize moved to the FilterBar trailing slot in #554 so the
524-
filter row is one self-contained unit (… filter chips |
525-
Clear all | Customize) — no second toolbar row, no dangling
526-
chrome. */}
527-
<div className="flex shrink-0 items-center gap-2">
522+
{/* Header right-side toolbar (#608): one row holds every page-
523+
level affordance — Clear all (when filters are active),
524+
Refresh, Customize, then the primary `+ <Entity>` button.
525+
Previously these were split between the page header (Add)
526+
and the FilterBar trailing slot (Clear all / Refresh /
527+
Customize), which read as two toolbars; consolidating into
528+
one row matches the detail-page header layout (#572) and
529+
removes the second row of chrome.
530+
The `+ Add` label dropped the word "Add" — the leading `+`
531+
already signals "create", so "Add" was redundant (#608). */}
532+
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2">
533+
{activeFilterCount > 0 ? (
534+
<ResetButton
535+
isDirty
536+
onReset={() =>
537+
patchParams((next) => filters.forEach((f) => next.delete(f.name)))
538+
}
539+
label="Clear all"
540+
icon={<X className="h-4 w-4" aria-hidden />}
541+
title="Clear all filters"
542+
/>
543+
) : null}
544+
<RefreshButton
545+
onRefresh={refresh}
546+
tooltip="Refresh"
547+
icon={<RefreshCw className="h-4 w-4" aria-hidden />}
548+
/>
549+
<button
550+
type="button"
551+
onClick={() => setColsOpen(true)}
552+
aria-haspopup="dialog"
553+
aria-label="Customize columns"
554+
title={
555+
hiddenCols.size > 0
556+
? `Customize columns (${hiddenCols.size} hidden)`
557+
: 'Customize columns'
558+
}
559+
className="inline-flex shrink-0 items-center justify-center rounded-md border border-gray-300 px-2 py-1.5 text-sm hover:bg-gray-100"
560+
>
561+
<Settings2 className="h-4 w-4" aria-hidden />
562+
</button>
528563
{data.permissions.add && (
529564
<Link
530565
to={withPreservedFilters(`/${appLabel}/${modelName}/add`, searchParams.toString())}
531566
className="rounded-md border border-primary bg-primary px-3 py-2 text-sm font-medium text-white hover:opacity-90"
532567
>
533-
+ Add {data.verbose_name ? capitalize(data.verbose_name) : modelName}
568+
+ {data.verbose_name ? capitalize(data.verbose_name) : modelName}
534569
</Link>
535570
)}
536571
</div>
@@ -597,52 +632,9 @@ export function ListPage() {
597632
</Popover>
598633
) : null
599634
}
600-
trailing={
601-
// Filter-row trailing slot (#554): "Clear all" + Refresh +
602-
// Customize, in that order, as the last three buttons on
603-
// the row. "Clear all" hides entirely when no filters apply
604-
// (owner directive, v1.3.3) — the filter pills themselves
605-
// signal there's nothing to clear, so a disabled button
606-
// would be redundant chrome (CLAUDE.md §7). The Customize
607-
// affordance is icon-only (the cog speaks for itself
608-
// alongside Refresh — text + count chip were noise on a
609-
// row that's already crowded).
610-
<>
611-
{activeFilterCount > 0 ? (
612-
<ResetButton
613-
isDirty
614-
onReset={() =>
615-
patchParams((next) => filters.forEach((f) => next.delete(f.name)))
616-
}
617-
label="Clear all"
618-
icon={<X className="h-4 w-4" aria-hidden />}
619-
title="Clear all filters"
620-
/>
621-
) : null}
622-
{/* Refresh (#592): refetch the changelist + filter counts
623-
with the current filter / search / ordering / page
624-
state preserved. Between Clear all and Customize. */}
625-
<RefreshButton
626-
onRefresh={refresh}
627-
tooltip="Refresh"
628-
icon={<RefreshCw className="h-4 w-4" aria-hidden />}
629-
/>
630-
<button
631-
type="button"
632-
onClick={() => setColsOpen(true)}
633-
aria-haspopup="dialog"
634-
aria-label="Customize columns"
635-
title={
636-
hiddenCols.size > 0
637-
? `Customize columns (${hiddenCols.size} hidden)`
638-
: 'Customize columns'
639-
}
640-
className="inline-flex shrink-0 items-center justify-center rounded-md border border-gray-300 px-2 py-1.5 text-sm hover:bg-gray-100"
641-
>
642-
<Settings2 className="h-4 w-4" aria-hidden />
643-
</button>
644-
</>
645-
}
635+
// FilterBar's `trailing` slot retired in v1.4.5 / #608 — Clear
636+
// all / Refresh / Customize live in the page header now so the
637+
// SPA shows one toolbar per page, not two.
646638
/>
647639

648640
{editCount > 0 && (

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "django-admin-react"
3-
version = "1.4.4"
3+
version = "1.4.5"
44
description = "A drop-in React single-page admin for Django, driven entirely by ModelAdmin."
55
authors = ["django-admin-react contributors"]
66
license = "MIT"

0 commit comments

Comments
 (0)