feat(12.2-02): add the fileupload field with deferred uploads committed on save

- 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
This commit is contained in:
Jakub Zych
2026-10-02 18:04:34 +02:00
parent edda803dc1
commit 044e0450ef
22 changed files with 2183 additions and 21 deletions

View File

@@ -23,7 +23,7 @@ var (
formFieldTypes = map[string]struct{}{
"text": {}, "textarea": {}, "number": {}, "checkbox": {},
"switch": {}, "dropdown": {}, "relation": {}, "relation-manager": {},
"widget": {}, "partial": {},
"widget": {}, "partial": {}, "fileupload": {},
}
formSpans = map[string]struct{}{
"left": {}, "right": {}, "full": {}, "auto": {}, "row": {},
@@ -36,6 +36,9 @@ var (
"tab": {}, "context": {}, "attributes": {}, "size": {}, "default": {},
"nameFrom": {}, "emptyOption": {}, "options": {}, "relation": {},
"widget": {}, "action": {}, "fill": {}, "path": {},
"mode": {}, "fileTypes": {}, "mimeTypes": {}, "maxFilesize": {},
"maxFiles": {}, "imageWidth": {}, "imageHeight": {},
"thumbOptions": {}, "useCaption": {}, "prompt": {},
}
// widgetKeys are valid only on `type: widget` (D-06).
widgetKeys = []string{"widget", "action", "fill"}
@@ -145,7 +148,10 @@ func (s *FormSchema) Localize(ctx context.Context, tr *phrasebook.Translator, pr
field.Tab = translateKey(ctx, tr, src.Tab)
field.EmptyOption = translateKey(ctx, tr, src.EmptyOption)
field.ActionLabel = translateKey(ctx, tr, src.ActionLabel)
field.Prompt = translateKey(ctx, tr, src.Prompt)
field.Fill = append([]string(nil), src.Fill...)
field.FileTypes = append([]string(nil), src.FileTypes...)
field.MimeTypes = append([]string(nil), src.MimeTypes...)
options, err := localizeOptions(ctx, tr, src, provider)
if err != nil {
return nil, err
@@ -422,6 +428,9 @@ func compileFieldNode(name string, node ast.Node) (FormField, error) {
if err := compilePartialPath(typ, values, &field); err != nil {
return FormField{}, err
}
if err := compileFileuploadKeys(typ, values, &field); err != nil {
return FormField{}, err
}
if node, ok := values["required"]; ok {
field.Required, err = nodeBool(node)
if err != nil {