feat(router): wire portal/register pages, portal-context guard carve-out, lint cleanup

Routing wiring (Phase D of WS-3 PR-B1):

- apps/app/src/plugins/1.router/guards.ts: add a single early-return
  carve-out before the org-selection redirect — `if (to.meta.context
  === 'portal') return`. Per ARCH-CONSOLIDATION-2026-04 §4.3,
  meta.context is the canonical contract; PR-B2 evolves the guards
  from this key to full context-aware logic (post-login landing,
  context-switcher, role checks).
- apps/app/env.d.ts: extend RouteMeta with the new layout names
  ('OrganizerLayout' | 'PortalLayout' | 'PublicLayout'), context,
  requiresAuth, requiresToken, navMode, navTitle.
- apps/app/typed-router.d.ts: regenerated by unplugin-vue-router to
  pick up portal/* and register/* route names.
- Page meta finalisation: portal pages have layout: 'PortalLayout',
  context: 'portal', preserving original requiresAuth + nav fields;
  register pages have layout: 'PublicLayout' + public: true (the
  apps/app guard convention for public routes, since meta.public is
  what the existing guard recognises).

Form-types restructure (boundaries cleanup):

- apps/app/src/composables/forms/types/formBuilder.ts → src/types/forms/
- apps/app/src/composables/forms/utils/{formValidation,validators}.ts
  → src/utils/forms/
- All `@/composables/forms/{types,utils}/*` imports rewritten across
  pages, components, composables, tests.
- This avoids a `types → composables` boundaries violation at
  src/types/formSchema.ts which re-exports primitives from the
  inlined form-schema. types/formSchema.ts now imports from
  @/types/forms/formBuilder which is in the same boundaries zone.

Lint cleanup for moved portal sources (apps/portal had no
.eslintrc.cjs; the migrated code now has to pass apps/app's stricter
config):

- axios.isAxiosError → named import { isAxiosError }
  (ClaimenTab, RoosterTab, profiel.vue)
- void schemaQuery.refetch() → schemaQuery.refetch()
  (register/[public_token].vue)
- if-then-else collapsed to single boolean return (formatFieldValue)
- :delay-on-touch-only="true" → delay-on-touch-only shorthand
  (FieldSectionPriority)
- ml-2 class → ms-2 (FieldAvailabilityPicker)
- multi-statement-per-line splits in profiel.vue + spec files
- unused emailConfigured ref removed (profiel.vue)
- one-component-per-file disabled with TODO TECH-WS3-PORTAL-LINT-CLEANUP
  ref (FieldOptionsLocale.spec.ts — multi-Wrapper test pattern)
- restored `import Draggable from 'vuedraggable'` after lint:fix
  removed it (template-only usage; the import IS needed)
- camelcase param renamed in FieldOptionsLocale harness factory
- typecheck nudge: spec state.data typed via PublicFormSectionOption[] /
  PublicFormTimeSlot[] aliases instead of Record<string, unknown>
- PortalLayout.vue: explicit `import { useRoute, useRouter }` so the
  vitest mock can intercept (the trimmed AutoImport set doesn't pull
  vue-router's auto-imports)

Vitest: 23 / 162 passing. Lint: 0 errors / 0 new warnings (only the
pre-existing boundaries v5→v6 deprecation warnings remain). Typecheck:
clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-05 19:26:46 +02:00
parent e3452312d1
commit 5c689f42a0
74 changed files with 778 additions and 339 deletions

View File

@@ -3,6 +3,9 @@ import { mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { defineComponent, h, ref } from 'vue'
import { apiClient } from '@/lib/axios'
import { draftSubmitterStorageKey, useFormDraft } from '@/composables/useFormDraft'
vi.mock('@/lib/axios', () => {
const post = vi.fn()
const put = vi.fn()
@@ -11,9 +14,6 @@ vi.mock('@/lib/axios', () => {
return { apiClient: { post, put, get } }
})
import { apiClient } from '@/lib/axios'
import { draftSubmitterStorageKey, useFormDraft } from '@/composables/useFormDraft'
interface MockedApi {
post: ReturnType<typeof vi.fn>
put: ReturnType<typeof vi.fn>
@@ -34,6 +34,7 @@ function mountWithDraft(token: string, options?: Parameters<typeof useFormDraft>
})
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } })
const wrapper = mount(Host, {
global: { plugins: [[VueQueryPlugin, { queryClient }]] },
})
@@ -105,7 +106,9 @@ describe('useFormDraft - submitter details', () => {
await draft.saveDraftNow()
expect(mocked.put).toHaveBeenCalledTimes(1)
const [, body] = mocked.put.mock.calls[0]
expect(body.public_submitter_name).toBe('Test User')
wrapper.unmount()
@@ -125,7 +128,9 @@ describe('useFormDraft - submitter details', () => {
await draft.saveDraftNow()
expect(mocked.put).toHaveBeenCalledTimes(1)
const [, body] = mocked.put.mock.calls[0]
expect(body.public_submitter_email).toBe('user@example.nl')
wrapper.unmount()
@@ -146,11 +151,14 @@ describe('useFormDraft - submitter details', () => {
draft.setSubmitterEmail('alice@example.nl')
const result = await draft.submitForm()
await flush()
expect(result).toBeTruthy()
expect(mocked.post).toHaveBeenCalledTimes(2)
const [, submitBody] = mocked.post.mock.calls[1]
expect(submitBody.public_submitter_name).toBe('Alice')
expect(submitBody.public_submitter_email).toBe('alice@example.nl')
@@ -174,9 +182,12 @@ describe('useFormDraft - submitter details', () => {
const result = await draft.submitForm()
expect(result).toBeNull()
const err = draft.submitError.value as { code?: string } | null
expect(err).toBeTruthy()
expect(err?.code).toBe('MISSING_SUBMITTER')
// Submit endpoint was NOT called — only the start POST is in the log.
expect(mocked.post).toHaveBeenCalledTimes(1)
@@ -193,11 +204,15 @@ describe('useFormDraft - submitter details', () => {
await flush()
draft.setSubmitterName('Bob')
// email intentionally left empty
const result = await draft.submitForm()
expect(result).toBeNull()
const err = draft.submitError.value as { code?: string } | null
expect(err?.code).toBe('MISSING_SUBMITTER')
expect(mocked.post).toHaveBeenCalledTimes(1)
@@ -221,6 +236,7 @@ describe('useFormDraft - submitter details', () => {
expect(draft.submitterEmail.value).toBe('resumed@example.nl')
const [, body] = mocked.post.mock.calls[0]
expect(body.public_submitter_name).toBe('Resumed')
expect(body.public_submitter_email).toBe('resumed@example.nl')
@@ -235,8 +251,11 @@ describe('useFormDraft - submitter details', () => {
draft.setSubmitterEmail('persist@example.nl')
const raw = sessionStorage.getItem(draftSubmitterStorageKey(TOKEN))
expect(raw).toBeTruthy()
const parsed = JSON.parse(raw as string)
expect(parsed).toEqual({ name: 'Persist Me', email: 'persist@example.nl' })
wrapper.unmount()