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:
@@ -1,11 +1,15 @@
|
||||
// Field renderer registry (D-05). A form field's `type` selects the control
|
||||
// component; any type without a renderer gets UnsupportedField, the design's
|
||||
// dashed box, so an unknown type never breaks the form. Phase 10.1 turns this
|
||||
// seam into the plugin extension point.
|
||||
// dashed box, so an unknown type never breaks the form. The relation manager
|
||||
// arrives with its own component in a later plan. Phase 10.1 turns this seam
|
||||
// into the plugin extension point.
|
||||
import type { Component } from 'vue'
|
||||
import type { ControllerParams, FormField, RelationOption } from '../../api/types'
|
||||
import CheckboxField from './fields/CheckboxField.vue'
|
||||
import DropdownField from './fields/DropdownField.vue'
|
||||
import NumberField from './fields/NumberField.vue'
|
||||
import RelationField from './fields/RelationField.vue'
|
||||
import SwitchField from './fields/SwitchField.vue'
|
||||
import TextField from './fields/TextField.vue'
|
||||
import TextareaField from './fields/TextareaField.vue'
|
||||
import UnsupportedField from './fields/UnsupportedField.vue'
|
||||
@@ -30,10 +34,13 @@ const renderers = new Map<string, Component>([
|
||||
['textarea', TextareaField],
|
||||
['number', NumberField],
|
||||
['dropdown', DropdownField],
|
||||
['switch', SwitchField],
|
||||
['checkbox', CheckboxField],
|
||||
['relation', RelationField],
|
||||
])
|
||||
|
||||
/** Types whose control shows the label itself (toggle cards). */
|
||||
const selfLabelled = new Set<string>()
|
||||
const selfLabelled = new Set<string>(['switch', 'checkbox'])
|
||||
|
||||
export function rendererFor(type: string): Component {
|
||||
return renderers.get(type) ?? UnsupportedField
|
||||
@@ -82,3 +89,16 @@ export function controlClass(invalid: boolean | undefined): string {
|
||||
invalid ? 'border-danger' : 'border-border-strong',
|
||||
].join(' ')
|
||||
}
|
||||
|
||||
/** Truthiness of a stored toggle value (true, 1, "1", "true"). */
|
||||
export function toggleOn(value: unknown): boolean {
|
||||
if (typeof value === 'string') {
|
||||
return value !== '' && value !== '0' && value.toLowerCase() !== 'false'
|
||||
}
|
||||
return Boolean(value)
|
||||
}
|
||||
|
||||
/** A toggle's next value in the stored value's own type (1/0 or boolean). */
|
||||
export function toggleValue(current: unknown, on: boolean): boolean | number {
|
||||
return typeof current === 'number' ? (on ? 1 : 0) : on
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user