feat(18-01): money list column, danger record actions, preview Edit rule and table kit (task 2)
- columns.yaml type: money with currency or currencyFrom (exactly one), never searchable, not sortable by default; every list row carries the currencyFrom column's value under its own key; the schema serves currency and currencyFrom on the column - pact.AdminRecordAction.Danger, served as danger on cabana.RecordAction (absent when false); RecordActions.vue passes it to the confirm dialog - CellValue money kind through formatMoney with the row's code, the em dash when empty and the amount alone without a code; DataTable aligns money headers and cells to the right - PreviewView renders Edit only when a registered value field allows update (F6) - main.css .summer-table* and .summer-stat__note kit classes (F7) over --c-* variables; cabana README kit rows - fixture record actions void (danger) and resend; TestMoneyListCompile, TestMoneyListSmoke and TestRecordActionDangerSmoke; regenerated OpenAPI and TS types; rebuilt dist; lists-and-filters and admin-controllers docs
This commit is contained in:
@@ -480,11 +480,12 @@ messages:
|
||||
edit: acme.roster::lang.people.edit
|
||||
```
|
||||
|
||||
Each `pact.AdminRecordAction` has a `Name`, a `Label`, an optional `Confirm` text, its own `Permissions`, an optional `Applies` function and `Run`:
|
||||
Each `pact.AdminRecordAction` has a `Name`, a `Label`, an optional `Confirm` text, its own `Permissions`, an optional `Danger` flag, an optional `Applies` function and `Run`:
|
||||
|
||||
- `Applies` reports whether the action fits the record's current state; without one the action always applies. It must only read, because it runs in two places: when a record is shown, to decide which actions to offer, and again inside the action's transaction, right before `Run`.
|
||||
- The show response (`GET /{controller}/{id}`) lists the offered actions in `meta.actions` as `cabana.RecordAction` entries: only those the administrator may run and that apply to the record. The key is absent when none is offered, and create and update responses never carry it.
|
||||
- `POST /{controller}/{id}/actions/{action}` takes an empty `{}` body. cabana loads the record through `pact.FormExtendQuery` with a row lock in one transaction and hands it to `Run` in `pact.AdminRecordActionInput`. A missing record and a record outside the scope are the same 404; an action whose `Applies` reports false answers 409 `conflict`; an administrator without the action's permissions gets 403.
|
||||
- `Run` writes through `cabana.TxFromContext(ctx)` and returns a `pact.AdminRecordActionResult` with an optional `Message`. Any error rolls the transaction back.
|
||||
- `Danger: true` marks an action that moves money out or cannot be undone, such as a refund. The record response serializes it as `danger` on the `cabana.RecordAction` entry (the key is absent for an ordinary action, so existing payloads are unchanged), and the admin's confirm dialog then shows its confirm button in the danger variant; the footer button itself stays the same. The flag changes nothing on the server: permissions and `Applies` are checked as for any action.
|
||||
|
||||
Record actions have their own namespace: a record action and a bulk action may share a name, such as `activate` here. `create` and `delete` are reserved. A name in `recordActions` that the controller does not register, a duplicate, or an action without a label stops the start-up.
|
||||
|
||||
@@ -59,7 +59,7 @@ columns:
|
||||
type: datetime
|
||||
```
|
||||
|
||||
A column takes `label`, `type` (`text`, `datetime`, `date`, `time` or `switch`), `searchable` (default false) and `sortable` (default true, as in WinterCMS), and, for a column read through a relation, `relation` and `select`. Other WinterCMS column types and options are refused at boot, except the alias of a [plugin field type](field-types.md#list-columns), whose column shows the type's list text. When `type` is omitted, a `time.Time` column is `datetime`, a `lagoon.Date` column is `date` and a `lagoon.TimeOfDay` column is `time`; every other column stays untyped (the admin treats that as text). An explicit `type` always wins. A searchable relation column matches a record when any related record matches: a `categories` column with `relation: categories` and `select: name` on a blog post finds the posts with at least one matching category, for belongsTo, hasOne, hasMany and belongsToMany relations alike, each through a subquery, so a post is listed and counted once. A sortable hasMany or belongsToMany column sorts by its comma-joined values, the text the list shows. The compiled list keeps the columns in file order:
|
||||
A column takes `label`, `type` (`text`, `datetime`, `date`, `time`, `switch` or `money`), `searchable` (default false) and `sortable` (default true, as in WinterCMS; false for a money column), and, for a column read through a relation, `relation` and `select`. Other WinterCMS column types and options are refused at boot, except the alias of a [plugin field type](field-types.md#list-columns), whose column shows the type's list text. When `type` is omitted, a `time.Time` column is `datetime`, a `lagoon.Date` column is `date` and a `lagoon.TimeOfDay` column is `time`; every other column stays untyped (the admin treats that as text). An explicit `type` always wins. A searchable relation column matches a record when any related record matches: a `categories` column with `relation: categories` and `select: name` on a blog post finds the posts with at least one matching category, for belongsTo, hasOne, hasMany and belongsToMany relations alike, each through a subquery, so a post is listed and counted once. A sortable hasMany or belongsToMany column sorts by its comma-joined values, the text the list shows. The compiled list keeps the columns in file order:
|
||||
|
||||
```go src=modules/cabana/example_test.go#ExampleCompileList
|
||||
// The plugin embeds its controllers/ and models/ trees; the example reads
|
||||
@@ -88,6 +88,26 @@ for _, f := range list.Filters {
|
||||
|
||||
`type: date` and `type: time` are for `lagoon.Date` and `lagoon.TimeOfDay` columns (a `DATE` and a `TIME` column): the admin shows the stored `2026-10-02` or `14:30:00` as it is, with no time zone conversion, where `type: datetime` would read a date as midnight UTC and could show the previous day. A struct that stores itself in one column (it implements `sql.Scanner` or `driver.Valuer`, as both types do) is a plain column, never a relation.
|
||||
|
||||
### Money columns
|
||||
|
||||
`type: money` shows an exact decimal amount column (the value of a [money field](forms.md#money)) formatted by the administrator's locale with its ISO code: `PLN 39.84` in English, `39,84 PLN` in Polish, with at least two and at most four decimals, so a stored `85.4193` is shown in full. The column takes the same currency keys as the field, exactly one of them:
|
||||
|
||||
```yaml
|
||||
columns:
|
||||
total:
|
||||
label: acme.shop::lang.orders.total
|
||||
type: money
|
||||
currencyFrom: currency
|
||||
fee:
|
||||
label: acme.shop::lang.orders.fee
|
||||
type: money
|
||||
currency: EUR
|
||||
```
|
||||
|
||||
- `currencyFrom` names a string column of the model. Every list row then carries that column's value under its own key, even when the file does not declare it as a column, so the admin formats each row with that row's code; a row whose code is empty shows the amount alone.
|
||||
- A money column is not sortable unless the file says `sortable: true`, and is never searchable: `searchable: true` stops the start-up, as do two currency sources, none, a code that is not three upper-case letters, a `currencyFrom` that is not a string column, a `relation`, or a model column that is neither a string nor a `sql.Scanner` and `driver.Valuer` type.
|
||||
- The admin aligns the header and the cells of a money column to the right in tabular numerals and never wraps an amount; an empty value shows a muted dash. The schema reports `currency` or `currencyFrom` on the column.
|
||||
|
||||
`invisible: true` keeps a column out of the table while it stays part of the list: the search covers it when it is `searchable`, it can still be sorted by, and the schema reports it with `invisible`. The admin SPA renders no header and no cell for it, and the rows of a list response do not carry its value. Use it for a column administrators search by but do not need to read, such as an e-mail address next to a name.
|
||||
|
||||
Search runs over the searchable columns only; sort accepts only sortable columns. A request that names any other column is refused, never passed to SQL.
|
||||
|
||||
Reference in New Issue
Block a user