feat(17-01): tree reorder page and singleton admin controllers

- cabana: config_reorder.yaml (title, nameFrom, modelClass, parentColumn,
  sortColumn, maxDepth, partition) with fail-loud boot checks and the
  built-in reorder toolbar button; list schema carries reorder
- GET/POST {controller}/reorder: scoped set (ListExtendQuery plus the
  partition) locked FOR UPDATE, 409 reorder_stale on any set difference,
  422 for duplicates, foreign parents, cycles and maxDepth, both columns
  written without model hooks
- pact.AdminSingleton: GET {controller}/singleton (first or create, form
  scope checked), form schema singleton, list/create/bulk/delete 404,
  no config_list.yaml needed
- swag, inventories, token classes, CSRF walk, conformance cases, backend
  lang reorder.stale and load_failed, reorder and singleton smoke tests, docs
This commit is contained in:
2026-10-12 15:35:43 +02:00
parent 46835fd064
commit 23f33c3911
31 changed files with 1905 additions and 15 deletions

View File

@@ -446,6 +446,15 @@ The admin SPA shows the declared actions in a "Bulk actions" menu next to the se
Bulk actions have their own namespace next to the toolbar and widget actions: a name is unique among the controller's bulk actions, and `create` and `delete` stay reserved. `bulkActions` needs `showCheckboxes: true`. A name the controller does not register, a duplicate, or an action without a label stops the start-up. The built-in bulk delete is not a declared action and keeps its own route and toolbar button.
## Singleton controllers
A controller that edits exactly one record, such as a site-wide definition, implements `pact.AdminSingleton`. Its `SingletonRecord(ctx, db)` returns that record and creates it first when it does not exist (first or create), with `db` the request's transaction. Make the creation safe against two first requests at once, for example with a unique key column and GORM's `FirstOrCreate`.
- `GET <prefix>/api/v1/{vendor}/{plugin}/{controller}/singleton` answers the record's id (`cabana.AdminSingletonResult`), after checking that the form scope (`pact.FormExtendQuery`) can load it. The admin then opens the record's form.
- The form schema carries `singleton: true`. Show and update work as on any controller.
- The controller needs a `config_form.yaml` and no `config_list.yaml`. Its list, create, bulk and delete routes answer 404 `not_found`, and the singleton route answers 404 on any other controller.
- A settings entry whose `Controller` names the singleton controller links to it; see [Settings](settings.md).
## Record actions
A record action runs on one record, as a button on its screen: activating an account, lifting a ban. The controller registers its record actions through `pact.HasAdminRecordActions` (`PeopleController` above registers two), and `recordActions` in `config_form.yaml` lists the ones the form offers, in display order: