- 41 unit and component suites under admin/tests/{app,state,shell,list,form,relation,views,ui}
covering states and a11y roles; every src module is imported by a test
- typed fixture helper assigns each JSON fixture to its generated OpenAPI type
- fix: iconFor ignores inherited object members such as "constructor"
- fix: field controls import ./control instead of the registry (import cycle
left a renderer unregistered depending on module load order)
- fix: dropdown shows the placeholder for an unknown stored value next to an emptyOption
- fix: list announces a failed schema load even when the rows arrive after it
- tailwind no longer scans admin/tests; boardwalk/dist rebuilt
70 lines
2.4 KiB
TypeScript
70 lines
2.4 KiB
TypeScript
// Shared props and helpers of the field controls (D-05). Kept apart from the
|
|
// registry, which imports every control: a control importing the registry
|
|
// would close an import cycle.
|
|
import type { ControllerParams, FormField, RelationOption } from '../../api/types'
|
|
|
|
/** Props every field control receives from FieldRenderer. */
|
|
export interface FieldControlProps {
|
|
field: FormField
|
|
modelValue: unknown
|
|
/** id of the focusable control; the label points at it. */
|
|
controlId: string
|
|
invalid?: boolean
|
|
/** Space-separated ids of the comment and error lines. */
|
|
describedBy?: string
|
|
/** Display labels of the field's current relation values (meta.labels). */
|
|
labels?: RelationOption[]
|
|
/** Controller whose option endpoints serve this form; null for settings. */
|
|
source?: ControllerParams | null
|
|
/** Id of the record being edited; null on create and on settings pages. */
|
|
recordId?: number | null
|
|
}
|
|
|
|
// Winter `attributes:` are HTML attributes on the control. Only these are
|
|
// passed through; anything else (event handlers included) is dropped.
|
|
const allowedAttributes = new Set([
|
|
'readonly',
|
|
'placeholder',
|
|
'maxlength',
|
|
'minlength',
|
|
'min',
|
|
'max',
|
|
'step',
|
|
'autocomplete',
|
|
'spellcheck',
|
|
'pattern',
|
|
])
|
|
|
|
export function controlAttributes(field: FormField): Record<string, string | number | boolean> {
|
|
const out: Record<string, string | number | boolean> = {}
|
|
for (const [name, value] of Object.entries(field.attributes ?? {})) {
|
|
const key = name.toLowerCase()
|
|
if (!allowedAttributes.has(key) || value === null || value === false) {
|
|
continue
|
|
}
|
|
out[key] = value
|
|
}
|
|
return out
|
|
}
|
|
|
|
/** Shared control styling: 44px, radius 10, danger border when invalid. */
|
|
export function controlClass(invalid: boolean | undefined): string {
|
|
return [
|
|
'w-full rounded-control border bg-surface px-3.5 text-text transition-colors duration-150 ease-out',
|
|
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
|
|
}
|