feat(12.1-02): read-only preview screen with a status hint and record actions

- config_form.yaml preview block (optional headerPartial), reported in the form schema as preview
- fields with context: preview show only on the preview screen and are never written
- form messages preview and edit; recordActions without a preview block stops boot
- SPA route {id}/preview, PreviewView and PreviewField, record actions in the footer
- mapWinterUrl maps preview/:id; the update form returns to the preview
- summer-callout partial style classes for status hints
- README, docs, OpenAPI document, TS types and the embedded build updated
This commit is contained in:
Jakub Zych
2026-10-05 00:10:48 +02:00
parent 1c99de5013
commit a65c670574
50 changed files with 1441 additions and 66 deletions

View File

@@ -13,9 +13,10 @@ WinterCMS controllers extend their screens with partials, `addJs` and `addCss`,
Two places accept a partial:
- `headerPartial: <name>` in `config_list.yaml`, a strip above the list;
- a `type: partial` field with `path: <name>` in `fields.yaml`.
- a `type: partial` field with `path: <name>` in `fields.yaml`;
- `preview.headerPartial: <name>` in `config_form.yaml`, a status hint above the fields of the [preview screen](forms.md#preview-screen).
Both render `{ConfigDir}/_<name>.htm` with Go's `html/template`. WinterCMS `$/` and `~/` partial paths are not supported. The template's data is `.Data`, the value the controller's `pact.AdminPartialData` returns for that partial name; for a form partial on an existing record, cabana passes the record it loaded through the controller's `pact.FormExtendQuery` scope. `trans "<key>"` translates a phrase key in the request locale.
All three render `{ConfigDir}/_<name>.htm` with Go's `html/template`. WinterCMS `$/` and `~/` partial paths are not supported. The template's data is `.Data`, the value the controller's `pact.AdminPartialData` returns for that partial name; for a form partial on an existing record, cabana passes the record it loaded through the controller's `pact.FormExtendQuery` scope. `trans "<key>"` translates a phrase key in the request locale.
A statistics strip above a list, using the SPA's partial style classes:
@@ -31,6 +32,21 @@ The view model must be a struct built for the template. cabana refuses a view mo
The rendered HTML is parsed and walked through an allowlist before it reaches the SPA: script, style, iframe, form and similar elements are removed with their content, unknown elements are unwrapped, `id`, `style` and event handler attributes are dropped, and links and images must be same-origin paths. Output is capped at 64 KiB, 2000 nodes and a depth of 32. The cabana README lists the allowed elements, attributes and style classes.
### Status hints
The preview screen's header partial always belongs to one record, so cabana renders it like a form partial: with the record it loaded through the controller's `pact.FormExtendQuery` scope. A typical hint tells the administrator why a record needs attention, and the record action that resolves it is a button in the screen's footer. The SPA's callout classes give it the native look:
```html src=modules/cabana/testdata/roster/controllers/people/_status.htm
{{- if .Data.Title -}}
<div class="summer-callout summer-callout--{{ .Data.Tone }}" role="status">
<p class="summer-callout__title">{{ trans .Data.Title }}</p>
<p class="summer-callout__text">{{ trans .Data.Text }}</p>
</div>
{{- end -}}
```
`summer-callout` is the block, `summer-callout--warning` and `summer-callout--danger` set its tone, and `summer-callout__title` and `summer-callout__text` are its two lines. `role="status"` is on the attribute allowlist and makes a hint that appears after an action polite to screen readers. A callout holds no icon, link or button. A template that renders nothing (here: when the view model has no title) leaves no gap on the screen, and the hint is fetched again after every record action.
## Plugin JavaScript and CSS
A controller that implements `pact.AdminClientAssets` names `.js`, `.mjs` and `.css` files under its plugin's `assets/` directory, the Go form of `addJs` and `addCss`. They are read from the embedded tree at start-up (a missing file stops it) and served from `<prefix>/assets/{vendor}/{plugin}/...` with a content hash in the URL, the admin Content-Security-Policy (`script-src 'self'`) and `nosniff`. Only declared files are reachable; the YAML and templates never are.