feat(10-03): tabs, toggles, relation fields, form lifecycle and settings pages

- FormTabs groups fields by tab (untabbed fields in the default tab) as a
  segmented tablist; a tab holding invalid fields after a 422 shows a
  count badge and the form switches to the first invalid field
- switch, checkbox and relation join the renderer registry: toggle cards
  (role=switch, 20px checkbox) keep a numeric value numeric; relation
  fields are read-only labels from meta.labels, a searchable single
  select over fields/{field}/options with emptyOption first, or ordered
  removable chips with an appending search (300 ms debounce, 20 per
  page, more on scroll or the more action)
- FormView gets the back button, record title and update subtitle, a
  sticky footer (Usuń with deleteConfirm then DELETE, Anuluj, Zapisz i
  zamknij to the mapped redirectClose, Zapisz), and asks before leaving a
  dirty form on any route change plus a beforeunload guard
- Settings: the rail pins Ustawienia to the bottom when /settings is
  non-empty; /settings lists pages by category; /settings/:code renders
  the settings schema through FormGrid and the registry, PUTs the values
  and maps a 422 like the record form
- New backend::lang keys (form.more_options, tab_default, discard,
  settings.back); form and settings smoke tests; boardwalk/dist rebuilt
This commit is contained in:
Jakub Zych
2026-09-27 17:00:18 +02:00
parent 8b5f85603f
commit 8f32416f4f
30 changed files with 1898 additions and 58 deletions

View File

@@ -2,8 +2,20 @@
"data": {
"name": "Widget",
"fields": [
{ "name": "name", "label": "Name", "type": "text", "span": "left", "required": true },
{ "name": "code", "label": "Code", "type": "text", "span": "right", "comment": "Unique widget code" },
{
"name": "name",
"label": "Name",
"type": "text",
"span": "left",
"required": true
},
{
"name": "code",
"label": "Code",
"type": "text",
"span": "right",
"comment": "Unique widget code"
},
{
"name": "size",
"label": "Size",
@@ -11,28 +23,139 @@
"span": "left",
"emptyOption": "— none —",
"options": [
{ "value": "s", "label": "Small" },
{ "value": "m", "label": "Medium" },
{ "value": "l", "label": "Large" }
{
"value": "s",
"label": "Small"
},
{
"value": "m",
"label": "Medium"
},
{
"value": "l",
"label": "Large"
}
]
},
{ "name": "weight", "label": "Weight", "type": "number", "span": "right" },
{ "name": "notes", "label": "Notes", "type": "textarea", "span": "full", "size": "small" },
{ "name": "color", "label": "Colour", "type": "colorpicker", "span": "left" }
{
"name": "weight",
"label": "Weight",
"type": "number",
"span": "right"
},
{
"name": "notes",
"label": "Notes",
"type": "textarea",
"span": "full",
"size": "small"
},
{
"name": "color",
"label": "Colour",
"type": "colorpicker",
"span": "left"
},
{
"name": "active",
"label": "Active",
"type": "switch",
"span": "left",
"comment": "Shown in the catalogue"
},
{
"name": "featured",
"label": "Featured",
"type": "checkbox",
"span": "right",
"comment": "Pinned to the top"
},
{
"name": "maker",
"label": "Maker",
"type": "relation",
"span": "left",
"nameFrom": "name",
"emptyOption": "— none —"
},
{
"name": "tags",
"label": "Tags",
"type": "relation",
"span": "right",
"nameFrom": "name",
"multiple": true,
"required": true
},
{
"name": "owner",
"label": "Owner",
"type": "relation",
"span": "left",
"nameFrom": "email",
"emptyOption": "Current user",
"readOnly": true
},
{
"name": "serial",
"label": "Serial",
"type": "text",
"span": "left",
"tab": "Details",
"context": "update"
},
{
"name": "batch",
"label": "Batch",
"type": "text",
"span": "right",
"tab": "Details",
"context": [
"create"
]
},
{
"name": "parts",
"type": "relation-manager",
"relation": "parts",
"span": "full",
"tab": "Parts",
"context": "update"
}
],
"messages": {
"create": { "other": "New widget" },
"update": { "other": "Editing widget" },
"saved": { "other": "Widget „:name” saved" },
"deleteConfirm": { "other": "Delete this widget?" },
"deleted": { "other": "Widget deleted" }
"create": {
"other": "New widget"
},
"update": {
"other": "Editing widget"
},
"saved": {
"other": "Widget „:name” saved"
},
"deleteConfirm": {
"other": "Delete this widget?"
},
"deleted": {
"other": "Widget deleted"
}
},
"meta": {
"locale": "en"
},
"meta": { "locale": "en" },
"redirects": {
"create": { "redirect": "acme/demo/widgets/update/:id", "redirectClose": "acme/demo/widgets" },
"update": { "redirect": "acme/demo/widgets", "redirectClose": "acme/demo/widgets" },
"create": {
"redirect": "acme/demo/widgets/update/:id",
"redirectClose": "acme/demo/widgets"
},
"update": {
"redirect": "acme/demo/widgets",
"redirectClose": "acme/demo/widgets"
},
"default": "acme/demo/widgets"
}
},
"meta": { "locale": "en" }
"meta": {
"locale": "en"
}
}