feat(12.2-03): add hasMany relation contracts, relation forms and child create

- RelationContract gains Kind (empty is belongsToMany) and ForeignKey, with kind-aware boot checks
- manage.form, view.form and pivot.form compile against the related or pivot model; $/ paths resolve inside the plugin
- view toolbarButtons accept create|update|delete|link|unlink, each the capability of its routes
- POST .../relations/{name}/records creates a child through the manage form; the server sets the hasMany key
- relation schema carries kind, deferrable and the localized forms; 17 new relation message keys in en and pl
This commit is contained in:
Jakub Zych
2026-10-02 18:37:13 +02:00
parent 0b4ef9c311
commit 48a5b8045a
23 changed files with 1621 additions and 100 deletions

View File

@@ -304,34 +304,47 @@ func decodeFields(raw []byte) ([]FormField, error) {
}
func (m *fieldMap) UnmarshalYAML(node ast.Node) error {
items, err := decodeFieldMapping(node, nil)
if err != nil {
return err
}
m.items = items
return nil
}
// decodeFieldMapping compiles a fields mapping in source order. rename, when
// set, maps a raw field name (such as WinterCMS's pivot[role]) to the name
// that must be an identifier.
func decodeFieldMapping(node ast.Node, rename func(string) string) ([]FormField, error) {
node = unwrapNode(node)
if _, ok := node.(*ast.NullNode); ok || node == nil {
m.items = []FormField{}
return nil
return []FormField{}, nil
}
mapping, ok := node.(*ast.MappingNode)
if !ok {
return fmt.Errorf("fields must be a mapping")
return nil, fmt.Errorf("fields must be a mapping")
}
items := make([]FormField, 0, len(mapping.Values))
seen := map[string]struct{}{}
for _, entry := range mapping.Values {
name, err := nodeString(unwrapNode(entry.Key))
if err == nil && rename != nil {
name = rename(name)
}
if err != nil || !identifier(name) {
return fmt.Errorf("field name %q is not an identifier", nodeText(entry.Key))
return nil, fmt.Errorf("field name %q is not an identifier", nodeText(entry.Key))
}
if _, dup := seen[name]; dup {
return fmt.Errorf("duplicate field %s", name)
return nil, fmt.Errorf("duplicate field %s", name)
}
seen[name] = struct{}{}
field, err := compileFieldNode(name, unwrapNode(entry.Value))
if err != nil {
return fmt.Errorf("field %s: %w", name, err)
return nil, fmt.Errorf("field %s: %w", name, err)
}
items = append(items, field)
}
m.items = items
return nil
return items, nil
}
func compileFieldNode(name string, node ast.Node) (FormField, error) {