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

@@ -65,3 +65,56 @@ func VirtualFieldsFromContext(ctx context.Context) (map[string]any, bool) {
}
return out, true
}
type fieldWritesKey struct{}
// FieldWrite is one value a save wrote for a plugin field type
// (pact.HasFormFieldTypes): Locale is the content locale of a translatable
// type's value and empty otherwise, Default is true when the value went to
// the host column, and Value is what the type's Decode returned.
type FieldWrite struct {
Locale string
Default bool
Value any
}
// withFieldWrites returns ctx carrying the plugin field values a save writes.
func withFieldWrites(ctx context.Context, writes map[string][]FieldWrite) context.Context {
if writes == nil {
writes = map[string][]FieldWrite{}
}
return context.WithValue(ctx, fieldWritesKey{}, writes)
}
// WithFieldWrites returns ctx carrying writes as a save would, so a test can
// call a Form hook that reads FieldWritesFromContext. The map is copied.
func WithFieldWrites(ctx context.Context, writes map[string][]FieldWrite) context.Context {
return withFieldWrites(ctx, copyFieldWrites(writes))
}
// FieldWritesFromContext returns the values a create or update save
// actually writes for the form's plugin field types, keyed by field name, for
// the context handed to a Form hook (pact.FormBeforeCreate,
// pact.FormAfterCreate, pact.FormBeforeUpdate, pact.FormAfterUpdate). A field
// or locale whose submitted value equals the value the form was served is
// not written and not listed; a translatable type lists one entry per
// written locale. The map is a copy. The second result is false outside a
// create or update save.
func FieldWritesFromContext(ctx context.Context) (map[string][]FieldWrite, bool) {
if ctx == nil {
return nil, false
}
writes, ok := ctx.Value(fieldWritesKey{}).(map[string][]FieldWrite)
if !ok {
return nil, false
}
return copyFieldWrites(writes), true
}
func copyFieldWrites(in map[string][]FieldWrite) map[string][]FieldWrite {
out := make(map[string][]FieldWrite, len(in))
for field, list := range in {
out[field] = append([]FieldWrite(nil), list...)
}
return out
}