feat(12.1-02): permissioneditor field in radio or checkbox mode
- type: permissioneditor with mode radio (1, -1) or checkbox (1); the controller serves the options per request through cabana.PermissionEditorProvider and reads and stores the values - a save answers 422 for a non-object, an unknown code or a value outside the mode's set and 403 for a changed locked code; stored codes that are not offered are kept - record responses carry the stored permissions as an object - SPA: PermissionEditorField with sections by tab, locked rows and a read-only mode for the preview - README, docs, OpenAPI document, TS types and dist updated
This commit is contained in:
@@ -95,6 +95,7 @@ for _, f := range form.Fields {
|
||||
| `partial` | A server-rendered template; see [Partials and widgets](partials-and-widgets.md). |
|
||||
| `fileupload` | Uploads for an attachOne or attachMany relation; see [File uploads](#file-uploads). |
|
||||
| `datepicker` | A date, date and time, or time of day; see [Date pickers](#date-pickers). |
|
||||
| `permissioneditor` | A list of permissions to allow, deny or leave inherited; see [Permission editor](#permission-editor). |
|
||||
| `password` | A masked input with a show and hide button. It is a form-only field: the value is sent with a save and never returned; see [Form-only fields](#form-only-fields). |
|
||||
|
||||
The WinterCMS widgets that are not in this list (the rich editor, the markdown editor, the code editor, the color picker, the media finder, the repeater, the tag list and the others) are not provided. A field with one of those types stops the start-up.
|
||||
@@ -248,6 +249,74 @@ func (MembersController) FormVirtualFields() []string {
|
||||
- `type: password` is always a form-only field: a password field the controller does not list stops the start-up. The admin SPA shows it empty on every load, leaves an empty password out of an update (empty means unchanged), and clears it after a save. A confirmation is a second `password` field, compared by the `confirmed` rule on the server.
|
||||
- Form-only fields are not available on settings forms or relation forms.
|
||||
|
||||
## Permission editor
|
||||
|
||||
`type: permissioneditor` edits a set of permissions on a record: which codes are allowed, and in radio mode which are denied. The field needs a `mode`:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
label: acme.roster::lang.people.permissions
|
||||
type: permissioneditor
|
||||
mode: radio
|
||||
tab: acme.roster::lang.people.tab_permissions
|
||||
context: update
|
||||
```
|
||||
|
||||
| Mode | Control per permission | Values |
|
||||
|------|------------------------|--------|
|
||||
| `radio` | Allow, Inherit, Deny | `1` allows, `-1` denies; an inherited permission has no value. |
|
||||
| `checkbox` | One checkbox | `1` allows; an unchecked permission has no value. |
|
||||
|
||||
The permissions themselves are not in the YAML. The controller implements `cabana.PermissionEditorProvider` and answers per request, so the list may depend on the signed-in administrator:
|
||||
|
||||
```go src=modules/cabana/example_form_seams_test.go#MembersController.AdminPermissionOptions
|
||||
// AdminPermissionOptions lists the permissions the `type: permissioneditor`
|
||||
// field offers, in display order. The principal on ctx decides what is locked.
|
||||
func (MembersController) AdminPermissionOptions(ctx context.Context, field string) ([]cabana.PermissionOption, error) {
|
||||
principal, _ := bouncer.User(ctx)
|
||||
mayExport := cabana.Allows(principal, []string{"acme.roster.manage"})
|
||||
return []cabana.PermissionOption{
|
||||
{Code: "posts.edit", Label: "acme.roster::lang.permissions.posts_edit", Tab: "acme.roster::lang.permissions.tab_content"},
|
||||
{Code: "posts.publish", Label: "acme.roster::lang.permissions.posts_publish", Tab: "acme.roster::lang.permissions.tab_content"},
|
||||
{Code: "reports.export", Label: "acme.roster::lang.permissions.reports_export", Tab: "acme.roster::lang.permissions.tab_reports", Locked: !mayExport},
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
```go src=modules/cabana/example_form_seams_test.go#MembersController.AdminPermissionValues
|
||||
// AdminPermissionValues reads the permissions stored on the record.
|
||||
func (MembersController) AdminPermissionValues(_ context.Context, field string, record any) (map[string]int, error) {
|
||||
values := map[string]int{}
|
||||
if raw := record.(*Member).Permissions; raw != "" {
|
||||
if err := json.Unmarshal([]byte(raw), &values); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
```
|
||||
|
||||
```go src=modules/cabana/example_form_seams_test.go#MembersController.AdminSetPermissionValues
|
||||
// AdminSetPermissionValues stores the checked set on the model. The save
|
||||
// writes the row afterwards, in the same transaction.
|
||||
func (MembersController) AdminSetPermissionValues(_ context.Context, field string, record any, values map[string]int) error {
|
||||
raw, err := json.Marshal(values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
record.(*Member).Permissions = string(raw)
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
- An option (`cabana.PermissionOption`) has a `Code`, a `Label`, and optionally a `Tab` and a `Comment`; the three texts are translation keys or text. Options with the same `Tab` are shown as one section of the list, in the order the controller returns them, and options without a `Tab` form a last section. The form schema carries them on the field as `permissionOptions`.
|
||||
- The value travels as a JSON object of code to integer, in a record response and in a save body: `{"posts.edit": 1, "posts.publish": -1}`. A record response always carries the object, empty when nothing is stored.
|
||||
- A save checks the submitted object inside its transaction, after the Form before-hooks and before the row is written. A value that is not an object of integers, a code the controller does not offer, or a value outside the mode's set is a 422 on the field; a `0` means "no value". The checked set is then handed to `AdminSetPermissionValues`, which decides how it is stored.
|
||||
- A stored code that is not among the options is kept as it is: an offered code that is left out loses its value, a code that is not offered is never touched and can never be submitted.
|
||||
- An option with `Locked` set is shown with a disabled control. The server enforces it: a save in which a locked code's value differs from the stored one is answered 403 `forbidden` with a message on the field, and nothing is written.
|
||||
- A save that does not send the field leaves the stored permissions alone, and the field's `context` applies as for any field.
|
||||
- The keys `options`, `default`, `nameFrom`, `emptyOption`, `relation` and `preset` are refused on the type. A field without `mode`, or on a controller that does not implement the provider, stops the start-up. The type is not available on settings forms or relation forms.
|
||||
|
||||
## What a save may write
|
||||
|
||||
The form's writable fields are bound to model columns at boot. A save passes only those fields that are also in the model's `Fillable` list, drops unknown keys, case variants and nested objects, and fills the model with `lagoon.Fill` (see [Models](../database/models.md)). The model's validation rules (a `Rules` method returning `lagoon.Validate` rule strings) and the form's `required` flags are checked in the save's transaction, and a failure is a 422 with messages per field. A value that does not fit its column is also a 422 on that field. A controller implementing `pact.FormRules` supplies its own rule set per operation, which replaces the model's rules for admin saves.
|
||||
|
||||
Reference in New Issue
Block a user