feat(12.1-02): password and form-only fields, rules per operation and preset

- pact.FormVirtualFields lists form fields that are not model columns: never
  bound, filled or projected; their values reach the Form hooks through
  cabana.VirtualFieldsFromContext when the field's context allows the operation
- type: password is a masked field that must be listed as virtual
- pact.FormRules supplies the rule set per operation and replaces the model's
  Rules() for admin saves; a rule on a virtual field sees the submitted value
- preset on a text field follows another text field on the create form
- SPA: PasswordField, preset handling in FormView, empty password left out of
  an update
- README, docs, OpenAPI document, TS types and dist updated
This commit is contained in:
Jakub Zych
2026-10-05 10:35:08 +02:00
parent a65c670574
commit a1c6bb1ce6
42 changed files with 1284 additions and 49 deletions

View File

@@ -259,3 +259,35 @@ func TestRowStateValues(t *testing.T) {
}
}
}
type formSeams struct{}
func (formSeams) FormVirtualFields() []string { return []string{"password", "password_confirmation"} }
func (formSeams) FormRules(_ context.Context, op string) map[string]string {
if op == "create" {
return map[string]string{"password": "required|confirmed"}
}
return map[string]string{"password": "nullable|confirmed"}
}
func TestFormSeamsDiscoveredByTypeAssertion(t *testing.T) {
var ctl any = formSeams{}
virtual, ok := ctl.(FormVirtualFields)
if !ok || len(virtual.FormVirtualFields()) != 2 {
t.Fatalf("FormVirtualFields = %v ok=%v", virtual, ok)
}
rules, ok := ctl.(FormRules)
if !ok {
t.Fatal("FormRules is not implemented")
}
if got := rules.FormRules(context.Background(), "create")["password"]; got != "required|confirmed" {
t.Fatalf("create rules = %q", got)
}
if got := rules.FormRules(context.Background(), "update")["password"]; got != "nullable|confirmed" {
t.Fatalf("update rules = %q", got)
}
if _, ok := any(neither{}).(FormVirtualFields); ok {
t.Fatal("a plugin without the method implements FormVirtualFields")
}
}