feat(portal): migrate option consumers to relational rich shape
Aligns the portal form renderer with the post-WS-5d snapshot + resource
contract. FieldRadio, FieldSelect, FieldMultiselect, and FieldCheckboxList
now consume options as arrays of {value, label, sort_order, translations?}
objects instead of the legacy string | {label, description, value?} union.
Locale resolution: option-level translations[locale] is preferred over
the default label when the active form locale is non-default. Pages
provide the locale via providePublicFormLocale (new helper in
publicFormInjection, mirrors providePublicFormToken). Field components
inject via usePublicFormLocale, which falls back to 'nl' when no
provider is on the tree — keeps standalone component tests light.
[public_token].vue now provides schemaQuery.data.locale ?? 'nl' to all
option-bearing renderers.
TypeScript types updated: PublicFormField.options is now OptionSpec[] |
null in @form-schema/types/formBuilder. The legacy `FieldOption` union
type is gone — passing strings or {label, description} would now fail
type-check. resolveOptionLabel(option, locale) helper exported from the
same module is the single source of truth for label resolution.
The legacy per-option `description` field is dropped as part of the
type narrowing — ARCH §5.1's option-bearing field types
(RADIO/SELECT/MULTISELECT/CHECKBOX_LIST) don't model descriptions; the
parallel RegistrationFieldTemplate domain in apps/app keeps its own
description support which is orthogonal and out of WS-5d scope. The 4
migrated components no longer render the description subtitle/paragraph
(both Vuetify item slots and the radio/checkbox custom #label slots
removed).
apps/app is NOT touched in this commit — its only options-reading
components (RegistrationField*.vue) consume the legacy
registration_field_templates / registration_form_fields domain and are
out of WS-5d scope. The commit-3 secondary filter-registry scan
returned zero portal+app consumers as predicted, so commit 4 stays
portal-only.
Vitest: 102 → 111 passed (+9 new tests in FieldOptionsLocale.spec.ts
covering preference of translations[locale] over label, fallback on
missing translation, and default-locale-no-provider fall-through, for
each of the four migrated components plus a no-provider sanity test).
The 2 pre-existing failures in FieldSectionPriority.spec.ts (stale
post-WS-5b max_priorities → max_selected references) are out of WS-5d
scope; the failure baseline is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -72,7 +72,19 @@ export interface FormFieldValidationRules {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type FieldOption = string | { label: string; description?: string | null; value?: string | number | null }
|
||||
// Post-WS-5d rich-shape option contract. Per ARCH-FORM-BUILDER §17.6,
|
||||
// every RADIO/SELECT/MULTISELECT/CHECKBOX_LIST option carries
|
||||
// value+label+sort_order on the row, and per-locale translations as a
|
||||
// JSON bag indexed by BCP-47 locale (e.g. "nl", "en_GB"). The legacy
|
||||
// `description` field is gone — ARCH §5.1's option-bearing field types
|
||||
// don't model descriptions; that lives on the parallel
|
||||
// RegistrationFieldTemplate domain in apps/app and is out of scope.
|
||||
export interface OptionSpec {
|
||||
value: string
|
||||
label: string
|
||||
sort_order: number
|
||||
translations?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface AvailableTag {
|
||||
id: string
|
||||
@@ -86,7 +98,7 @@ export interface PublicFormField {
|
||||
field_type: FormFieldType
|
||||
label: string
|
||||
help_text: string | null
|
||||
options: FieldOption[] | null
|
||||
options: OptionSpec[] | null
|
||||
available_tags: AvailableTag[] | null
|
||||
validation_rules: FormFieldValidationRules | null
|
||||
is_required: boolean
|
||||
@@ -96,6 +108,14 @@ export interface PublicFormField {
|
||||
form_schema_section_id: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an option's display label for a given locale. Falls back to
|
||||
* the default label when no translation exists for that locale.
|
||||
*/
|
||||
export function resolveOptionLabel(option: OptionSpec, locale: string): string {
|
||||
return option.translations?.[locale] ?? option.label
|
||||
}
|
||||
|
||||
export interface PublicFormSection {
|
||||
id: string
|
||||
slug: string
|
||||
|
||||
Reference in New Issue
Block a user