feat(17-01): repeater field type and the dependent dropdown save recheck
- cabana: type repeater with nested form.fields (text, textarea, number, checkbox, switch, dropdown, richeditor), prompt, minItems, maxItems, titleFrom; groups, style, nested repeaters and other types fail boot - rows decoded before projection: only nested keys, coerced scalars, per-row required (field.index.nested), row bounds; ProjectWritableFields exempts repeater lists only, nestedValue unchanged - nested richeditor values sanitized with notices under the row path - dependent dropdowns rechecked on save, top level and per row; null when the sibling takes no choice, 422 form.dependent_invalid otherwise - dependent-options accepts repeater.field; schema carries RepeaterForm - backend lang repeater.max_items, repeater.min_items, form.dependent_invalid; TestCompanionsRepeaterSmoke; docs
This commit is contained in:
@@ -27,7 +27,7 @@ var (
|
||||
"widget": {}, "partial": {}, "fileupload": {}, "datepicker": {},
|
||||
"password": {}, "permissioneditor": {},
|
||||
"markdown": {}, "mltext": {}, "mltextarea": {}, "mlmarkdown": {},
|
||||
"richeditor": {},
|
||||
"richeditor": {}, "repeater": {},
|
||||
}
|
||||
formSpans = map[string]struct{}{
|
||||
"left": {}, "right": {}, "full": {}, "auto": {}, "row": {},
|
||||
@@ -46,6 +46,8 @@ var (
|
||||
"format": {}, "minDate": {}, "maxDate": {}, "yearRange": {},
|
||||
"firstDay": {}, "twelveHour": {}, "ignoreTimezone": {},
|
||||
"preset": {}, "dependsOn": {},
|
||||
"form": {}, "maxItems": {}, "minItems": {}, "titleFrom": {},
|
||||
"groups": {}, "style": {},
|
||||
}
|
||||
// widgetKeys are valid only on `type: widget` (D-06).
|
||||
widgetKeys = []string{"widget", "action", "fill"}
|
||||
@@ -264,26 +266,10 @@ func (s *FormSchema) Localize(ctx context.Context, tr *phrasebook.Translator, pr
|
||||
ctx = towel.WithLocale(ctx, locale)
|
||||
fields := make([]FormField, len(s.Fields))
|
||||
for i, src := range s.Fields {
|
||||
field := src
|
||||
field.Label = translateKey(ctx, tr, src.Label)
|
||||
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.Prompt = translateKey(ctx, tr, src.Prompt)
|
||||
field.Fill = append([]string(nil), src.Fill...)
|
||||
if src.Preset != nil {
|
||||
preset := *src.Preset
|
||||
field.Preset = &preset
|
||||
}
|
||||
field.FileTypes = append([]string(nil), src.FileTypes...)
|
||||
field.MimeTypes = append([]string(nil), src.MimeTypes...)
|
||||
field.YearRange = append([]int(nil), src.YearRange...)
|
||||
options, err := localizeOptions(ctx, tr, src, provider)
|
||||
field, err := localizeField(ctx, tr, src, provider, src.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
field.Options = options
|
||||
fields[i] = field
|
||||
}
|
||||
if fields == nil {
|
||||
@@ -329,6 +315,45 @@ func (s *FormSchema) CanonicalJSON() ([]byte, error) {
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
// localizeField copies one field and resolves its display strings and
|
||||
// options for a request. path is the name the dropdown provider is asked
|
||||
// with: the field name, or repeater.nested for a field of a repeater row.
|
||||
func localizeField(ctx context.Context, tr *phrasebook.Translator, src FormField, provider pact.DropdownOptionsProvider, path string) (FormField, error) {
|
||||
field := src
|
||||
field.Label = translateKey(ctx, tr, src.Label)
|
||||
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.Prompt = translateKey(ctx, tr, src.Prompt)
|
||||
field.Fill = append([]string(nil), src.Fill...)
|
||||
if src.Preset != nil {
|
||||
preset := *src.Preset
|
||||
field.Preset = &preset
|
||||
}
|
||||
field.FileTypes = append([]string(nil), src.FileTypes...)
|
||||
field.MimeTypes = append([]string(nil), src.MimeTypes...)
|
||||
field.YearRange = append([]int(nil), src.YearRange...)
|
||||
named := src
|
||||
named.Name = path
|
||||
options, err := localizeOptions(ctx, tr, named, provider)
|
||||
if err != nil {
|
||||
return FormField{}, err
|
||||
}
|
||||
field.Options = options
|
||||
if src.Form != nil {
|
||||
nested := make([]FormField, len(src.Form.Fields))
|
||||
for i, inner := range src.Form.Fields {
|
||||
nested[i], err = localizeField(ctx, tr, inner, provider, src.Name+"."+inner.Name)
|
||||
if err != nil {
|
||||
return FormField{}, err
|
||||
}
|
||||
}
|
||||
field.Form = &RepeaterForm{Fields: nested}
|
||||
}
|
||||
return field, nil
|
||||
}
|
||||
|
||||
func requireDropdownProvider(ctl pact.AdminController, fields []FormField) error {
|
||||
var method string
|
||||
for _, field := range fields {
|
||||
@@ -336,6 +361,16 @@ func requireDropdownProvider(ctl pact.AdminController, fields []FormField) error
|
||||
method = field.optionsMethod
|
||||
break
|
||||
}
|
||||
if field.Form != nil {
|
||||
for _, inner := range field.Form.Fields {
|
||||
if inner.optionsMethod != "" && method == "" {
|
||||
method = inner.optionsMethod
|
||||
}
|
||||
}
|
||||
if method != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if method == "" || dropdownProvider(ctl) != nil {
|
||||
return nil
|
||||
@@ -602,6 +637,9 @@ func compileFieldNode(name string, node ast.Node, types *fieldTypeRegistry) (For
|
||||
if err := compileDependsOn(typ, values, &field); err != nil {
|
||||
return FormField{}, err
|
||||
}
|
||||
if err := compileRepeater(name, typ, values, &field); err != nil {
|
||||
return FormField{}, err
|
||||
}
|
||||
if node, ok := values["required"]; ok {
|
||||
field.Required, err = nodeBool(node)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user