test(10-05): bring every SPA module, composable and component under Vitest
- 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
This commit is contained in:
@@ -5,7 +5,6 @@
|
||||
// relation through its own endpoints and renders only on an existing record.
|
||||
// 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'
|
||||
@@ -16,22 +15,11 @@ import TextField from './fields/TextField.vue'
|
||||
import TextareaField from './fields/TextareaField.vue'
|
||||
import UnsupportedField from './fields/UnsupportedField.vue'
|
||||
|
||||
/** 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
|
||||
}
|
||||
// The control helpers live in ./control so the field components never import
|
||||
// this module: registry -> field -> registry would be an import cycle whose
|
||||
// evaluation order decides whether a renderer is registered at all.
|
||||
export { controlAttributes, controlClass, toggleOn, toggleValue } from './control'
|
||||
export type { FieldControlProps } from './control'
|
||||
|
||||
/** The relation manager's field type (design screen 5). */
|
||||
export const RELATION_MANAGER = 'relation-manager'
|
||||
@@ -73,51 +61,3 @@ export function needsRecord(type: string): boolean {
|
||||
export function ownsLabel(type: string): boolean {
|
||||
return selfLabelled.has(type)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user