Files
summercms/modules/cabana/field_trigger.go
Jakub Zych 7091d09699 feat(19.6.1-02): field trigger and admin build
- fields.yaml trigger (action disable|hide, field, condition checked|unchecked) compiled to FormField.Trigger
- boot errors for malformed triggers, controlling fields that are not a switch or checkbox, and triggers in repeater rows, settings and relation forms
- Administrators permissions overrides are disabled while Superuser is on (D-06)
- SPA: trigger.ts (triggerState, applyTriggers), FORM_TRIGGER_NOTES, the trigger note in FormField, FormView tabs/banner/payload after triggers, PreviewView hide only
- regenerated admin OpenAPI and schema.d.ts; rebuilt modules/boardwalk/dist; docs and README
2026-10-22 11:43:58 +02:00

102 lines
3.4 KiB
Go

package cabana
import (
"fmt"
"github.com/goccy/go-yaml/ast"
)
// FieldTrigger is a field's fields.yaml `trigger` (D-21): the admin disables
// or hides the field while a switch or checkbox of the same form is checked
// or unchecked. Action is disable or hide, Field the controlling field's
// name, Condition checked or unchecked. It is a display rule only: the
// server never trusts it, and the admin leaves a disabled or hidden field
// out of the save body, so the stored value is kept.
type FieldTrigger struct {
Action string `json:"action"`
Field string `json:"field"`
Condition string `json:"condition"`
}
// triggerKeys are the sub-keys a trigger mapping must hold, each exactly once.
var triggerKeys = []string{"action", "field", "condition"}
// compileTrigger decodes a field's `trigger`: a mapping with exactly action
// (disable or hide), field (an identifier) and condition (checked or
// unchecked). The controlling field is checked against the form in
// checkTriggers.
func compileTrigger(values map[string]ast.Node, field *FormField) error {
node, ok := values["trigger"]
if !ok {
return nil
}
mapping, ok := node.(*ast.MappingNode)
if !ok {
return fmt.Errorf("trigger must be a mapping with action, field and condition")
}
got := map[string]string{}
for _, entry := range mapping.Values {
key, err := nodeString(unwrapNode(entry.Key))
if err != nil {
return fmt.Errorf("trigger: %w", err)
}
switch key {
case "action", "field", "condition":
default:
return fmt.Errorf("trigger: unknown key %s (want action, field and condition)", key)
}
if _, dup := got[key]; dup {
return fmt.Errorf("trigger: duplicate key %s", key)
}
value, err := nodeString(unwrapNode(entry.Value))
if err != nil {
return fmt.Errorf("trigger: %s: %w", key, err)
}
got[key] = value
}
for _, key := range triggerKeys {
if _, ok := got[key]; !ok {
return fmt.Errorf("trigger: %s is required", key)
}
}
trigger := FieldTrigger{Action: got["action"], Field: got["field"], Condition: got["condition"]}
if trigger.Action != "disable" && trigger.Action != "hide" {
return fmt.Errorf("trigger: action %s is not supported (want disable or hide)", trigger.Action)
}
if trigger.Condition != "checked" && trigger.Condition != "unchecked" {
return fmt.Errorf("trigger: condition %s is not supported (want checked or unchecked)", trigger.Condition)
}
if !identifier(trigger.Field) {
return fmt.Errorf("trigger: field %q is not a field name", trigger.Field)
}
field.Trigger = &trigger
return nil
}
// checkTriggers checks every trigger of a controller form against its
// fields: the controlling field must be another field of the same form, of
// type switch or checkbox (a virtual switch counts).
func checkTriggers(fields []FormField) error {
types := make(map[string]string, len(fields))
for _, field := range fields {
types[field.Name] = field.Type
}
for _, field := range fields {
if field.Trigger == nil {
continue
}
name := field.Trigger.Field
if name == field.Name {
return fmt.Errorf("field %s: trigger field names the field itself", field.Name)
}
typ, ok := types[name]
if !ok {
return fmt.Errorf("field %s: trigger field %s is not a field of this form", field.Name, name)
}
if typ != "switch" && typ != "checkbox" {
return fmt.Errorf("field %s: trigger field %s must be a switch or checkbox, not type %s", field.Name, name, typ)
}
}
return nil
}