feat(19.3-04): pact.ReorderAfterSave hook inside cabana's reorder transaction

- optional ReorderAfterSave(ctx, partition) runs after the new order is
  written; an error rolls the reorder back through lifecycleFailure
- reorderPost runs in lagoon.Transaction so lagoon.AfterCommit work a hook
  registers (a site-cache purge) runs only after the commit
- pact and cabana READMEs, admin hook table, tree reorder docs
This commit is contained in:
Jakub Zych
2026-10-15 18:27:07 +02:00
parent 0bc390f39f
commit af9dad634c
7 changed files with 28 additions and 6 deletions

View File

@@ -215,6 +215,7 @@ Behaviour overrides such as `formBeforeCreate` or `listExtendQuery` become optio
| `pact.FormBeforeCreate`, `pact.FormAfterCreate` | Around a create, inside its transaction. |
| `pact.FormBeforeUpdate`, `pact.FormAfterUpdate` | Around an update, inside its transaction. |
| `pact.FormBeforeDelete`, `pact.FormAfterDelete` | Around a delete, inside its transaction. |
| `pact.ReorderAfterSave` | After a reorder save wrote the new order, inside its transaction, with the request's partition (nil without one). An error rolls the reorder back. See [Tree reorder](lists-and-filters.md#tree-reorder). |
| `pact.DropdownOptionsProvider` | Supplies the options of a `dropdown` field that names a method. |
A hook or scope that has to read the database during a write should use the transaction the write runs in, `cabana.TxFromContext(ctx)`, rather than the application pool: it then sees the write's own snapshot and takes no second connection while the transaction holds row locks. The handle is valid only until the hook returns, and the list route, which runs no transaction, reports none.

View File

@@ -223,6 +223,8 @@ A save posts `{partition, nodes: [{id, parent_id, sort_order}]}` to `POST .../re
- A duplicate id, a parent outside the set, a cycle and a node deeper than `maxDepth` are 422 `validation_failed` on `nodes`; a partitioned page without `partition` is a 422 on `partition`.
- Otherwise the parent and sort columns of every node are written, without running the model's save hooks, and the answer is the new tree.
The model's save hooks do not run on a reorder, and neither do the controller's `pact.FormAfterUpdate` hooks. A controller that has to react to a new order implements `pact.ReorderAfterSave`: `ReorderAfterSave(ctx, partition)` runs after every node was written, inside the same transaction (`cabana.TxFromContext` returns it), with the posted partition, or nil when the reorder has none. A returned error rolls the whole reorder back. The transaction is a `lagoon.Transaction`, so work registered with `lagoon.AfterCommit` runs only once the new order is committed; purging a site's page cache after a menu reorder is the typical case (`cooler.PurgeSiteCacheAfterCommit`, see [Site cache purge](../services/caching.md#site-cache-purge)).
Both routes need the controller's permissions; the save is a write route (CSRF header, and a personal token needs a scope that allows POST).
In the admin, a list whose schema carries `reorder` shows a "Reorder records" button before "New record". It opens the reorder page, and when the list is filtered on the partition column it opens that partition. The page shows the tree with a drag handle and move up, move down, outdent and indent buttons on every row. A row can also be dragged before, after or into another row (never into its own subtree), and the arrow keys on a focused handle move it the same way as the buttons. There is no Save button: 400 ms after the last move the page posts the whole set, shows "Order saved", and sends a pending save at once when the administrator leaves. Only one save is in flight at a time: moves made while it runs are sent after it settles, with the tree as it is then, so the stored order never ends older than the screen. A refused save puts the last saved order back, unless a later save is waiting (it carries the whole tree); a 409 reloads the tree. A save that settles after the page switched partition does not touch the new partition's tree. A partitioned page has a selector for the partition.

View File

@@ -156,7 +156,7 @@ func (c articlesController) FormAfterUpdate(ctx context.Context, model any) erro
}
```
A nil purger (no purge configured) does nothing, so the hooks need no configuration check.
A nil purger (no purge configured) does nothing, so the hooks need no configuration check. A tree or sortable list's reorder save runs `pact.ReorderAfterSave` the same way; see [Tree reorder](../backend/lists-and-filters.md#tree-reorder).
### The site side