From dd7dfe9c0be2cf135b6a2ef204bb19016b054384 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Sat, 25 Apr 2026 02:50:33 +0200 Subject: [PATCH] feat(portal): migrate option consumers to relational rich shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- apps/portal/auto-imports.d.ts | 43 +---- .../public-form/FieldCheckboxList.vue | 40 ++--- .../public-form/FieldMultiselect.vue | 38 ++-- .../src/components/public-form/FieldRadio.vue | 41 ++--- .../components/public-form/FieldSelect.vue | 38 ++-- .../src/composables/publicFormInjection.ts | 18 +- .../src/pages/register/[public_token].vue | 9 +- .../public-form/FieldOptionsLocale.spec.ts | 162 ++++++++++++++++++ packages/form-schema/src/types/formBuilder.ts | 24 ++- 9 files changed, 273 insertions(+), 140 deletions(-) create mode 100644 apps/portal/tests/components/public-form/FieldOptionsLocale.spec.ts diff --git a/apps/portal/auto-imports.d.ts b/apps/portal/auto-imports.d.ts index b0004a7e..692970ad 100644 --- a/apps/portal/auto-imports.d.ts +++ b/apps/portal/auto-imports.d.ts @@ -10,6 +10,7 @@ declare global { const COOKIE_MAX_AGE_1_YEAR: typeof import('./src/utils/constants')['COOKIE_MAX_AGE_1_YEAR'] const CreateUrl: typeof import('./src/@core/composable/CreateUrl')['CreateUrl'] const EffectScope: typeof import('vue')['EffectScope'] + const PUBLIC_FORM_LOCALE_KEY: typeof import('./src/composables/publicFormInjection')['PUBLIC_FORM_LOCALE_KEY'] const PUBLIC_FORM_TOKEN_KEY: typeof import('./src/composables/publicFormInjection')['PUBLIC_FORM_TOKEN_KEY'] const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] const alphaDashValidator: typeof import('./src/@core/utils/validators')['alphaDashValidator'] @@ -123,6 +124,7 @@ declare global { const prefixWithPlus: typeof import('./src/@core/utils/formatters')['prefixWithPlus'] const provide: typeof import('vue')['provide'] const provideLocal: typeof import('@vueuse/core')['provideLocal'] + const providePublicFormLocale: typeof import('./src/composables/publicFormInjection')['providePublicFormLocale'] const providePublicFormToken: typeof import('./src/composables/publicFormInjection')['providePublicFormToken'] const reactify: typeof import('@vueuse/core')['reactify'] const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] @@ -301,6 +303,7 @@ declare global { const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion'] const usePrevious: typeof import('@vueuse/core')['usePrevious'] const useProjection: typeof import('@vueuse/math')['useProjection'] + const usePublicFormLocale: typeof import('./src/composables/publicFormInjection')['usePublicFormLocale'] const usePublicFormToken: typeof import('./src/composables/publicFormInjection')['usePublicFormToken'] const useRafFn: typeof import('@vueuse/core')['useRafFn'] const useRefHistory: typeof import('@vueuse/core')['useRefHistory'] @@ -393,8 +396,8 @@ declare module 'vue' { interface GlobalComponents {} interface ComponentCustomProperties { readonly EffectScope: UnwrapRef + readonly PUBLIC_FORM_LOCALE_KEY: UnwrapRef readonly PUBLIC_FORM_TOKEN_KEY: UnwrapRef - readonly acceptHMRUpdate: UnwrapRef readonly alphaDashValidator: UnwrapRef readonly alphaValidator: UnwrapRef readonly asyncComputed: UnwrapRef @@ -411,11 +414,8 @@ declare module 'vue' { readonly controlledRef: UnwrapRef readonly createApp: UnwrapRef readonly createEventHook: UnwrapRef - readonly createGenericProjection: UnwrapRef readonly createGlobalState: UnwrapRef readonly createInjectionState: UnwrapRef - readonly createPinia: UnwrapRef - readonly createProjection: UnwrapRef readonly createReactiveFn: UnwrapRef readonly createReusableTemplate: UnwrapRef readonly createSharedComposable: UnwrapRef @@ -427,8 +427,6 @@ declare module 'vue' { readonly debouncedWatch: UnwrapRef readonly defineAsyncComponent: UnwrapRef readonly defineComponent: UnwrapRef - readonly definePage: UnwrapRef - readonly defineStore: UnwrapRef readonly draftIdempotencyKey: UnwrapRef readonly draftSubmitterStorageKey: UnwrapRef readonly eagerComputed: UnwrapRef @@ -440,7 +438,6 @@ declare module 'vue' { readonly formatDate: UnwrapRef readonly formatDateToMonthShort: UnwrapRef readonly generateDeviceFingerprint: UnwrapRef - readonly getActivePinia: UnwrapRef readonly getCurrentInstance: UnwrapRef readonly getCurrentScope: UnwrapRef readonly getDeviceName: UnwrapRef @@ -462,21 +459,11 @@ declare module 'vue' { readonly isToday: UnwrapRef readonly kFormatter: UnwrapRef readonly lengthValidator: UnwrapRef - readonly logicAnd: UnwrapRef - readonly logicNot: UnwrapRef - readonly logicOr: UnwrapRef readonly makeDestructurable: UnwrapRef - readonly mapActions: UnwrapRef - readonly mapGetters: UnwrapRef - readonly mapState: UnwrapRef - readonly mapStores: UnwrapRef - readonly mapWritableState: UnwrapRef readonly markRaw: UnwrapRef readonly nextTick: UnwrapRef readonly onActivated: UnwrapRef readonly onBeforeMount: UnwrapRef - readonly onBeforeRouteLeave: UnwrapRef - readonly onBeforeRouteUpdate: UnwrapRef readonly onBeforeUnmount: UnwrapRef readonly onBeforeUpdate: UnwrapRef readonly onClickOutside: UnwrapRef @@ -499,6 +486,7 @@ declare module 'vue' { readonly prefixWithPlus: UnwrapRef readonly provide: UnwrapRef readonly provideLocal: UnwrapRef + readonly providePublicFormLocale: UnwrapRef readonly providePublicFormToken: UnwrapRef readonly reactify: UnwrapRef readonly reactifyObject: UnwrapRef @@ -521,12 +509,9 @@ declare module 'vue' { readonly resolveUnref: UnwrapRef readonly resolveVuetifyTheme: UnwrapRef readonly rgbaToHex: UnwrapRef - readonly setActivePinia: UnwrapRef - readonly setMapStoreSuffix: UnwrapRef readonly shallowReactive: UnwrapRef readonly shallowReadonly: UnwrapRef readonly shallowRef: UnwrapRef - readonly storeToRefs: UnwrapRef readonly syncRef: UnwrapRef readonly syncRefs: UnwrapRef readonly templateRef: UnwrapRef @@ -547,7 +532,6 @@ declare module 'vue' { readonly unrefElement: UnwrapRef readonly until: UnwrapRef readonly urlValidator: UnwrapRef - readonly useAbs: UnwrapRef readonly useActiveElement: UnwrapRef readonly useAnimate: UnwrapRef readonly useArrayDifference: UnwrapRef @@ -565,7 +549,6 @@ declare module 'vue' { readonly useAsyncQueue: UnwrapRef readonly useAsyncState: UnwrapRef readonly useAttrs: UnwrapRef - readonly useAverage: UnwrapRef readonly useBase64: UnwrapRef readonly useBattery: UnwrapRef readonly useBluetooth: UnwrapRef @@ -573,8 +556,6 @@ declare module 'vue' { readonly useBroadcastChannel: UnwrapRef readonly useBrowserLocation: UnwrapRef readonly useCached: UnwrapRef - readonly useCeil: UnwrapRef - readonly useClamp: UnwrapRef readonly useClipboard: UnwrapRef readonly useClipboardItems: UnwrapRef readonly useCloned: UnwrapRef @@ -613,7 +594,6 @@ declare module 'vue' { readonly useFetch: UnwrapRef readonly useFileDialog: UnwrapRef readonly useFileSystemAccess: UnwrapRef - readonly useFloor: UnwrapRef readonly useFocus: UnwrapRef readonly useFocusWithin: UnwrapRef readonly useFormDraft: UnwrapRef @@ -622,7 +602,6 @@ declare module 'vue' { readonly useGamepad: UnwrapRef readonly useGenerateImageVariant: UnwrapRef readonly useGeolocation: UnwrapRef - readonly useI18n: UnwrapRef readonly useId: UnwrapRef readonly useIdle: UnwrapRef readonly useImage: UnwrapRef @@ -635,13 +614,10 @@ declare module 'vue' { readonly useLocalStorage: UnwrapRef readonly useMagicKeys: UnwrapRef readonly useManualRefHistory: UnwrapRef - readonly useMath: UnwrapRef - readonly useMax: UnwrapRef readonly useMediaControls: UnwrapRef readonly useMediaQuery: UnwrapRef readonly useMemoize: UnwrapRef readonly useMemory: UnwrapRef - readonly useMin: UnwrapRef readonly useModel: UnwrapRef readonly useMounted: UnwrapRef readonly useMouse: UnwrapRef @@ -662,22 +638,18 @@ declare module 'vue' { readonly usePointer: UnwrapRef readonly usePointerLock: UnwrapRef readonly usePointerSwipe: UnwrapRef - readonly usePrecision: UnwrapRef readonly usePreferredColorScheme: UnwrapRef readonly usePreferredContrast: UnwrapRef readonly usePreferredDark: UnwrapRef readonly usePreferredLanguages: UnwrapRef readonly usePreferredReducedMotion: UnwrapRef readonly usePrevious: UnwrapRef - readonly useProjection: UnwrapRef + readonly usePublicFormLocale: UnwrapRef readonly usePublicFormToken: UnwrapRef readonly useRafFn: UnwrapRef readonly useRefHistory: UnwrapRef readonly useResizeObserver: UnwrapRef readonly useResponsiveLeftSidebar: UnwrapRef - readonly useRound: UnwrapRef - readonly useRoute: UnwrapRef - readonly useRouter: UnwrapRef readonly useScreenOrientation: UnwrapRef readonly useScreenSafeArea: UnwrapRef readonly useScriptTag: UnwrapRef @@ -691,9 +663,9 @@ declare module 'vue' { readonly useSpeechRecognition: UnwrapRef readonly useSpeechSynthesis: UnwrapRef readonly useStepper: UnwrapRef + readonly useStorage: UnwrapRef readonly useStorageAsync: UnwrapRef readonly useStyleTag: UnwrapRef - readonly useSum: UnwrapRef readonly useSupported: UnwrapRef readonly useSwipe: UnwrapRef readonly useTemplateRef: UnwrapRef @@ -714,7 +686,6 @@ declare module 'vue' { readonly useToString: UnwrapRef readonly useToggle: UnwrapRef readonly useTransition: UnwrapRef - readonly useTrunc: UnwrapRef readonly useUrlSearchParams: UnwrapRef readonly useUserMedia: UnwrapRef readonly useVModel: UnwrapRef diff --git a/apps/portal/src/components/public-form/FieldCheckboxList.vue b/apps/portal/src/components/public-form/FieldCheckboxList.vue index 40d8314a..2c0ccea0 100644 --- a/apps/portal/src/components/public-form/FieldCheckboxList.vue +++ b/apps/portal/src/components/public-form/FieldCheckboxList.vue @@ -1,6 +1,8 @@