feat(10.1-01): run registered widget actions through a cabana-owned route
- pact: AdminClientAssets, AdminAction, AdminActionInput, AdminActionResult,
HasAdminActions and AdminPartialData contracts
- fields.yaml type: widget with widget, action and fill keys; boot checks the
plugin tag prefix, the registered action and writable scalar fill fields
- POST .../widgets/{field} behind requireAjax, controller and action
permissions, scoped non-locking record read and a server-side fill filter
- typed OpenAPI operation, inventories and an acme conformance case
This commit is contained in:
@@ -23,6 +23,7 @@ var (
|
||||
formFieldTypes = map[string]struct{}{
|
||||
"text": {}, "textarea": {}, "number": {}, "checkbox": {},
|
||||
"switch": {}, "dropdown": {}, "relation": {}, "relation-manager": {},
|
||||
"widget": {},
|
||||
}
|
||||
formSpans = map[string]struct{}{
|
||||
"left": {}, "right": {}, "full": {}, "auto": {}, "row": {},
|
||||
@@ -34,7 +35,10 @@ var (
|
||||
"label": {}, "comment": {}, "span": {}, "type": {}, "required": {},
|
||||
"tab": {}, "context": {}, "attributes": {}, "size": {}, "default": {},
|
||||
"nameFrom": {}, "emptyOption": {}, "options": {}, "relation": {},
|
||||
"widget": {}, "action": {}, "fill": {},
|
||||
}
|
||||
// widgetKeys are valid only on `type: widget` (D-06).
|
||||
widgetKeys = []string{"widget", "action", "fill"}
|
||||
)
|
||||
|
||||
type formConfigDocument struct {
|
||||
@@ -108,6 +112,7 @@ func CompileForm(pluginID string, ctl pact.AdminController, fsys fs.FS) (*FormSc
|
||||
ModelClass: doc.ModelClass,
|
||||
Fields: fields,
|
||||
redirects: FormRedirects{Default: doc.DefaultRedirect},
|
||||
fieldsPath: fieldsPath,
|
||||
}
|
||||
if doc.Messages != nil {
|
||||
schema.messageKeys = *doc.Messages
|
||||
@@ -139,6 +144,8 @@ func (s *FormSchema) Localize(ctx context.Context, tr *phrasebook.Translator, pr
|
||||
field.Comment = translateKey(ctx, tr, src.Comment)
|
||||
field.Tab = translateKey(ctx, tr, src.Tab)
|
||||
field.EmptyOption = translateKey(ctx, tr, src.EmptyOption)
|
||||
field.ActionLabel = translateKey(ctx, tr, src.ActionLabel)
|
||||
field.Fill = append([]string(nil), src.Fill...)
|
||||
options, err := localizeOptions(ctx, tr, src, provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -412,6 +419,9 @@ func compileFieldNode(name string, node ast.Node) (FormField, error) {
|
||||
if typ == "relation-manager" && field.Relation == "" {
|
||||
return FormField{}, fmt.Errorf("relation is required")
|
||||
}
|
||||
if err := compileWidgetKeys(typ, values, &field); err != nil {
|
||||
return FormField{}, err
|
||||
}
|
||||
if node, ok := values["required"]; ok {
|
||||
field.Required, err = nodeBool(node)
|
||||
if err != nil {
|
||||
@@ -440,6 +450,54 @@ func compileFieldNode(name string, node ast.Node) (FormField, error) {
|
||||
return field, nil
|
||||
}
|
||||
|
||||
// compileWidgetKeys decodes the D-06 widget keys. They are refused on any
|
||||
// other type; a widget needs a custom-element tag and an action name, and may
|
||||
// list the fields its action writes back. The tag prefix, the registered
|
||||
// action and the fill fields are checked against the controller in
|
||||
// compileExtension.
|
||||
func compileWidgetKeys(typ string, values map[string]ast.Node, field *FormField) error {
|
||||
if typ != "widget" {
|
||||
for _, key := range widgetKeys {
|
||||
if _, ok := values[key]; ok {
|
||||
return fmt.Errorf("%s is only valid on type: widget", key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
tag, err := nodeString(values["widget"])
|
||||
if err != nil || strings.TrimSpace(tag) == "" {
|
||||
return fmt.Errorf("widget (the custom-element tag) is required on type: widget")
|
||||
}
|
||||
field.Widget = tag
|
||||
action, err := nodeString(values["action"])
|
||||
if err != nil || !identifier(action) {
|
||||
return fmt.Errorf("action %q is not an identifier (type: widget needs a registered action)", nodeText(values["action"]))
|
||||
}
|
||||
field.Action = action
|
||||
if node, ok := values["fill"]; ok {
|
||||
seq, ok := node.(*ast.SequenceNode)
|
||||
if !ok {
|
||||
return fmt.Errorf("fill must be a list of field names")
|
||||
}
|
||||
items := sequenceValues(seq)
|
||||
fill := make([]string, 0, len(items))
|
||||
seen := map[string]struct{}{}
|
||||
for _, item := range items {
|
||||
name, err := nodeString(unwrapNode(item))
|
||||
if err != nil || !identifier(name) {
|
||||
return fmt.Errorf("fill %q is not a field name", nodeText(item))
|
||||
}
|
||||
if _, dup := seen[name]; dup {
|
||||
return fmt.Errorf("fill: duplicate field %s", name)
|
||||
}
|
||||
seen[name] = struct{}{}
|
||||
fill = append(fill, name)
|
||||
}
|
||||
field.Fill = fill
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func compileContext(node ast.Node) (*fieldContext, error) {
|
||||
switch n := node.(type) {
|
||||
case *ast.StringNode:
|
||||
|
||||
Reference in New Issue
Block a user