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

@@ -128,7 +128,20 @@ describe('edit a record', () => {
const [put] = requestsTo(calls, 'PUT', RECORD)
expect(put).toBeDefined()
expect(put!.headers.get('X-Requested-With')).toBe('XMLHttpRequest')
expect(await put!.json()).toEqual({ name: 'Red widget', code: 'W-01', size: 'l', weight: null, notes: 'Round' })
// Every editable, context-allowed field; not the unsupported colorpicker,
// the read-only owner or the relation manager.
expect(await put!.json()).toEqual({
name: 'Red widget',
code: 'W-01',
size: 'l',
weight: null,
notes: 'Round',
active: true,
featured: false,
maker: 2,
tags: [3, 1],
serial: 'SN-1',
})
const toast = wrapper.find('[role="status"]')
expect(toast.text()).toContain('Widget „Red widget” saved')
expect(toast.find('button').attributes('aria-label')).toBe('Zamknij')