feat(16.2-01): translatable plugin field types, untouched skip, writes, list values and uploads

- translatable types lift, store and hydrate through the ML path
  (isMLField); Decode and Load run per locale and notices carry the locale
- on update a value equal to the served value (Load of the stored host
  value or exact translation) is neither decoded nor written
- FieldWrite, FieldWritesFromContext and WithFieldWrites expose what a save
  writes to the Form hooks
- list columns may use a plugin alias and show the type's ListValue
- POST .../{id}/fields/{field}/upload hands the file to the type's Upload
  (guard, X-Requested-With, permissions, field context, record scope, body
  cap); OpenAPI, route inventories, README and docs updated
This commit is contained in:
2026-10-10 11:19:24 +02:00
parent 83fa2f7b2c
commit 8fa92db96d
21 changed files with 1281 additions and 67 deletions

View File

@@ -355,6 +355,10 @@ func (s *service) mount(r pact.Router) {
constrainFileID(g)
g.Get("/{vendor}/{plugin}/{controller}/{id}/files/{field}/{file}/thumb", s.fileThumb)
constrainFileID(g)
// Uploads of plugin field type elements (D-18): cabana owns the route,
// the type's Upload only stores the file. {id} 0 is the create form.
g.Post("/{vendor}/{plugin}/{controller}/{id}/fields/{field}/upload", requireAjax(s.fieldUpload))
constrainFile(g)
})
// The SPA shell: public, no guard. ServeMux prefers every API pattern
// above over the {path...} wildcard.
@@ -1074,6 +1078,13 @@ func (s *service) operationDeclared(w http.ResponseWriter, r *http.Request, cc *
}
func projectRow(row any, controller pact.AdminController, cols []ListColumn) map[string]any {
return projectRowCtx(context.Background(), row, controller, cols)
}
// projectRowCtx is one list row: the id and every visible column. A column
// whose type is a plugin field type with ListValue shows the type's text for
// the stored value.
func projectRowCtx(ctx context.Context, row any, controller pact.AdminController, cols []ListColumn) map[string]any {
v := reflect.ValueOf(row)
for v.Kind() == reflect.Pointer {
if v.IsNil() {
@@ -1100,6 +1111,10 @@ func projectRow(row any, controller pact.AdminController, cols []ListColumn) map
if !field.IsValid() || !field.CanInterface() {
continue
}
if ft := col.fieldType; ft != nil && ft.def.ListValue != nil {
out[col.Key] = ft.def.ListValue(ctx, derefValue(field.Interface()))
continue
}
out[col.Key] = field.Interface()
}
return out