feat(14.2.1-03): document ML fields and regenerate admin artifacts

- Document markdown, mltext, and mlmarkdown plus TranslationWriter save semantics
- Extend the SPA registry tests and rebuild committed boardwalk dist

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-10-06 13:29:00 +02:00
parent a04116d41f
commit e3b3475b1c
9 changed files with 82 additions and 16 deletions

View File

@@ -202,7 +202,7 @@ Every controller gets the same routes, relative to `<prefix>/api/v1/{vendor}/{pl
The cabana README lists the full route table, including relation, options, widget, toolbar and partial routes. Every response uses one JSON envelope (`cabana.Envelope`), and a validation failure is a 422 `validation_failed` error with messages per field.
Request parameters never reach SQL directly: search, sort and filters apply only to declared columns, and a write passes only the form's writable fields, filled through `lagoon.Fill` and validated through `lagoon.Validate` in a transaction.
Request parameters never reach SQL directly: search, sort and filters apply only to declared columns, and a write passes only the form's writable fields, filled through `lagoon.Fill` and validated through `lagoon.Validate` in a transaction. A `mltext` or `mlmarkdown` field arrives as a nested locale map: cabana lifts it before `cabana.ProjectWritableFields` drops nested values, writes the default locale as the host scalar, and calls a published `cabana.TranslationWriter` for the other locales inside the same permissioned save transaction. There is no separate translate-write route; see [Markdown and multilingual fields](forms.md#markdown-and-multilingual-fields).
## Hooks

View File

@@ -97,8 +97,11 @@ for _, f := range form.Fields {
| `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). |
| `markdown` | A source editor for a host text column, with a sanitized preview; see [Markdown and multilingual fields](#markdown-and-multilingual-fields). |
| `mltext` | A text field with one editor per locale; see [Markdown and multilingual fields](#markdown-and-multilingual-fields). |
| `mlmarkdown` | A markdown field with one editor per locale; see [Markdown and multilingual fields](#markdown-and-multilingual-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.
The WinterCMS widgets that are not in this list (the rich 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.
### Field options
@@ -318,8 +321,34 @@ func (MembersController) AdminSetPermissionValues(_ context.Context, field strin
- 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.
## Markdown and multilingual fields
`type: markdown` edits markdown source on a host text column. The admin SPA shows a source editor and may preview HTML from `cabana.RenderMarkdown`, which uses the pinned goldmark engine without unsafe HTML. Output that still contains a script or iframe tag, an event handler, or a javascript, vbscript or data URL is refused, so translated raw HTML cannot become executable preview content.
`type: mltext` and `type: mlmarkdown` reuse the ordinary text and markdown editors with a locale selector. `mlmarkdown` composes the markdown control rather than a second parser. Each ML field shows its own selector; changing one selector changes every ML control on the form. Copy-from-locale fills the active locale from another code already in the value.
The save body sends every locale as a JSON object of code to string. Extra YAML keys are not accepted: the types reuse `label`, `comment`, `span`, `size`, `required`, `tab` and `context`. An unknown type such as `mlunknown` stops the start-up.
```yaml
fields:
title:
label: acme.blog::lang.posts.title
type: mltext
span: full
excerpt:
label: acme.blog::lang.posts.excerpt
type: markdown
body:
label: acme.blog::lang.posts.body
type: mlmarkdown
size: huge
tab: acme.blog::lang.posts.tab_content
```
A save of `{ "title": { "en": "Hello", "pl": "Witaj" } }` lifts the nested map before `cabana.ProjectWritableFields` drops nested values. The default locale fills the host column; other locales are written through a published `cabana.TranslationWriter` after the host row has a primary key, still inside the controller's permissioned save transaction. The default locale is not duplicated into the translation store. A non-string value, an undeclared locale, a missing default locale, or a nested map on a field that is not declared multilingual is a 422 on that field. Generic nested objects on other field types are still dropped. Cabana never imports a translate plugin and never exposes a public translate-write route: if no writer is published, a nested ML map is a 422.
## 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.
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)). Declared `mltext` and `mlmarkdown` locale maps are the exception: they are lifted first, as [Markdown and multilingual fields](#markdown-and-multilingual-fields). 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.
[Form-only fields](#form-only-fields) are never written by cabana. Their submitted values are validated, when a rule names them, and handed to the controller's hooks; what is stored from them is the hook's decision.