- type: fileupload compiles the D-08 keys and binds to the model's attach.Relation at boot
- X-Session-Key (cabana.SessionKeyHeader) carries the form session key; RecordInput.SessionKey
- GET and POST .../{id}/files/{field}: list with pending uploads, multipart upload into attach.Store
- the create and update save attaches the session's pending files in its transaction
- swagger2openapi folds formData parameters into a multipart requestBody
- admin OpenAPI, TS types, conformance cases, README and forms docs
7.7 KiB
title, description, section, order
| title | description | section | order |
|---|---|---|---|
| Forms | Describe admin forms in config_form.yaml and fields.yaml, with the supported field types, spans, tabs, dropdown options and create or update contexts. | backend | 20 |
Forms
The Form behaviour's config_form.yaml and the model's fields.yaml keep their WinterCMS shape. cabana compiles them strictly at boot: an unknown key, field type, span or size stops the start-up with an error naming the file and the field, so a WinterCMS option that SummerCMS does not implement is never ignored silently.
config_form.yaml
The controller's form configuration names the fields file with a WinterCMS path, the model class, and where the SPA goes after a save:
name: acme.blog::lang.posts.form
form: ~/plugins/acme/blog/models/post/fields.yaml
modelClass: Post
defaultRedirect: acme/blog/posts
create:
redirect: acme/blog/posts/update/:id
redirectClose: acme/blog/posts
update:
redirect: acme/blog/posts
redirectClose: acme/blog/posts
modelClass must equal the controller's pact.AdminController.ModelName. The ~/plugins/<vendor>/<plugin>/ prefix points into the plugin's own embedded tree.
fields.yaml
fields:
title:
label: acme.blog::lang.posts.title_column
type: text
span: left
required: true
slug:
label: acme.blog::lang.posts.slug
type: text
span: right
context: update
comment: acme.blog::lang.posts.slug_comment
status:
label: acme.blog::lang.posts.status
type: dropdown
span: left
options:
draft: acme.blog::lang.posts.draft
published: acme.blog::lang.posts.published
published:
label: acme.blog::lang.posts.published
type: switch
span: right
content:
label: acme.blog::lang.posts.content
type: textarea
size: large
tab: acme.blog::lang.posts.tab_content
The compiled schema keeps the fields in file order, with their labels as translation keys until a request asks for them in its locale:
fsys := os.DirFS("testdata/docs")
form, err := cabana.CompileForm("acme.blog", PostsController{}, fsys)
if err != nil {
fmt.Println(err)
return
}
for _, f := range form.Fields {
fmt.Printf("%s %s span=%q tab=%q required=%v options=%d\n", f.Name, f.Type, f.Span, f.Tab, f.Required, len(f.Options))
}
// Output:
// title text span="left" tab="" required=true options=0
// slug text span="right" tab="" required=false options=0
// status dropdown span="left" tab="" required=false options=2
// published switch span="right" tab="" required=false options=0
// content textarea span="" tab="acme.blog::lang.posts.tab_content" required=false options=0
Field types
| Type | Renders |
|---|---|
text, textarea, number |
Text inputs. |
checkbox, switch |
Booleans. |
dropdown |
A select. Options are a map in the YAML, or the name of a method the controller answers through pact.DropdownOptionsProvider. |
relation |
A belongsTo or belongsToMany picker; see Relation manager. |
relation-manager |
An embedded list of related records; see Relation manager. |
widget |
A plugin custom element with a server action; see Partials and widgets. |
partial |
A server-rendered template; see Partials and widgets. |
fileupload |
Uploads for an attachOne or attachMany relation; see File uploads. |
The WinterCMS widgets that are not in this list (the rich editor, the media finder, the repeater and the others) are not provided. A field with one of those types stops the start-up.
Field options
A field takes label, comment, type, required, default, tab, span (left, right, full, auto, row), size (tiny, small, large, huge, giant), context, attributes (scalar HTML attributes for the input), options and emptyOption, plus nameFrom and relation on relation fields. WinterCMS keys outside this set, such as readOnly, disabled, trigger or dependsOn, are refused.
context: update shows a field only on the update form, and context: create only on the create form; a list of contexts is also accepted. The context is enforced on the server too: a field that is hidden on a form is never written by that form's save, whatever the request body holds.
File uploads
A type: fileupload field edits one attachment relation of the form's model. The model declares its relations with an AttachRelations method (attach.HasRelations) and implements attach.Owner; see Attachments. The field name must be one of the declared relation names, otherwise the start-up stops.
fields:
photos:
label: acme.blog::lang.posts.photos
type: fileupload
mode: image
fileTypes: jpg,png,webp
maxFilesize: 5
maxFiles: 10
thumbOptions:
mode: crop
The field takes the generic keys plus these WinterCMS keys:
| Key | Meaning |
|---|---|
mode |
image or file (the default). Image mode accepts only jpg, jpeg, png, gif and webp and checks that the bytes decode as such an image. |
fileTypes |
Allowed extensions, as a comma- or pipe-separated string or a list. |
mimeTypes |
Allowed MIME types (image/png, image/*) or extensions. |
maxFilesize |
The largest file in megabytes. It may not exceed http.body_limits.upload_bytes. |
maxFiles |
The most files an attachMany relation may hold. Refused on attachOne. |
imageWidth, imageHeight |
Preview size, 1 to 4096 pixels (240 by 240 when not set). |
thumbOptions |
A mapping with mode: auto, exact, crop (the default) or fit. |
useCaption |
Lets the administrator edit each file's title and description. |
prompt |
The upload button text, a translation key. |
options, emptyOption, nameFrom and every other key stop the start-up.
Uploads are deferred until the form is saved, on the create form and the update form alike, as WinterCMS's file upload widget does. The admin SPA makes a random session key when it opens a form and sends it in the X-Session-Key header with every upload and with the save. The upload stores the file unattached and records a pending binding for that key and the signed-in administrator; the record's create or update save attaches every pending file of the form's fileupload fields inside its own transaction. If the save fails with a 422, the uploads stay pending for the next attempt; if the form is left without saving, the daily deferred:purge removes them. A session key is only ever seen by the administrator who used it.
The limits are enforced on the server: the upload route caps the request body at the smaller of http.body_limits.upload_bytes and maxFilesize plus 64 KiB (413 payload_too_large past it), and a file that is too large, of a type the field does not allow, or not a valid image in image mode is a 422 on the field.
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). 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.