Split the model admin into a list and a page per model

/admin/models rendered a full edit form for every model. With eight that
was merely long; with a hundred it was unusable, which is the report.

The list is now compact rows only -- avatar, name, badges, position,
reorder buttons, Edit link -- with search across id and display name,
filter tabs (All / Enabled / Disabled / Pinned / Restricted, each with a
count), a connection filter, and pagination at 40. Filters are links, so
a filtered view is a real URL you can keep. Editing moved to
/admin/models/{id}/edit, one model per page, with Previous/Next links so
a freshly imported connection can be tidied without returning to the
list each time.

Measured with 128 models: the list is 73 KB showing 40 rows over 4
pages, and a detail page is 17 KB. The old page would have rendered all
128 forms into one response.

Reordering needed rethinking at that size too. Up/down is fine for
nudging a model one place but hopeless for moving it sixty, so the
detail page has a position field you type into; the value is clamped and
a non-numeric one is ignored rather than throwing. The move buttons take
a `back` field so they return to whatever filtered, paginated view they
were pressed on instead of dumping you at page 1.

Also adds a select-all checkbox for the bulk bar, scoped to a container
selector rather than the page so a future list can carry more than one.

212 tests, ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-07-21 13:43:00 +02:00
parent 7b67568f2c
commit 085dca5ec4
8 changed files with 709 additions and 163 deletions
+12
View File
@@ -232,6 +232,18 @@
if (event.target.matches("[data-autosize]")) autosize(event.target);
});
/* A "select all" box driving every checkbox inside a container. Scoped to a
selector rather than the whole page, so a list can carry more than one. */
document.addEventListener("change", function (event) {
var master = event.target.closest("[data-select-all]");
if (!master) return;
var scope = document.querySelector(master.dataset.selectAll);
if (!scope) return;
scope.querySelectorAll('input[type="checkbox"]').forEach(function (box) {
box.checked = master.checked;
});
});
/* Enter sends, Shift+Enter inserts a newline -- the convention every chat
application uses. Left alone on touch devices, where there is no easy
Shift and Enter should mean "new line". */