From 43915501401003cd14a2030bec25b9a00ef5df53 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Mon, 11 May 2026 01:12:06 +0200 Subject: [PATCH] feat(layouts): rewrite layout shells with PrimeVue Drawer + Menubar + Avatar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Layout-shell rewrite per RFC AD-3, B7-option-B. R-10 isolation invariant honored — this single commit is revertible to roll back the layout change without losing B1–B6 progress. New component (PrimeVue-only, no Vuetify imports per F3 hard constraint): - apps/app/src/layouts/components/AppShell.vue (~210 lines) - Desktop sidebar (Tailwind grid, lg+ breakpoint) renders nav items as PrimeVue Buttons + Icons. Mobile ( for semantic structure F3 functional regressions (intentional — F4 sub-packages reintroduce each item through PrimeVue): - NavSearchBar (Vuetify-heavy combobox/overlay) — absent from top bar - ContextSwitcher (Vuetify VBtn + VMenu) — absent - NavbarThemeSwitcher (Vuetify IconBtn) — absent; dark mode driven by PrimeVue's darkModeSelector: '.dark' continues to work via the existing @core skin classes until F6 cleanup - NavbarShortcuts (Vuetify-heavy) — absent - NavBarNotifications (Vuetify-heavy) — absent - UserProfile from @/layouts/components/ (Vuetify-heavy menu) — replaced with the minimal Avatar + Menu dropdown described above; rich profile panel returns in F4 - ImpersonationBanner — absent; super-admin impersonation UX is F4 work - PortalLayout event-mode vs platform-mode topbar (route.meta.navMode driven) — absent; F4 reintroduces via AppShell prop or slot - Suspense + AppLoadingIndicator wrapping pages — dropped; pages handle their own loading via PrimeVue ProgressSpinner VApp at App.vue level still wraps everything, so Vuetify components inside still-Vuetify pages continue to render correctly during the parallel-mode window. Test updates (no Vuetify in layout structure to assert against anymore): - OrganizerLayout.spec.ts — mocks AppShell instead of the deleted DefaultLayoutWithVerticalNav reference; provides Pinia. - PortalLayout.spec.ts — same mock pattern; new structural assertions go through AppShell stub; the new third test verifies PortalLayout forwards portal nav items + title to AppShell. - PublicLayout.vue — uses
for semantics; PublicLayout.spec.ts still passes unchanged. Auto-generated component/auto-import dts files refreshed for the new AppShell component (committed for stable dev workflow). Verification: - pnpm typecheck — clean. - pnpm test — 402 tests pass (test count unchanged after spec rewrites). - pnpm build — succeeds in 14.05s; AppShell chunk is ~57 KB raw. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/app/auto-imports.d.ts | 4 + apps/app/components.d.ts | 2 + apps/app/src/layouts/OrganizerLayout.vue | 51 +++- apps/app/src/layouts/PortalLayout.vue | 255 ++---------------- apps/app/src/layouts/PublicLayout.vue | 19 +- .../layouts/__tests__/OrganizerLayout.spec.ts | 32 ++- .../layouts/__tests__/PortalLayout.spec.ts | 105 +++----- apps/app/src/layouts/blank.vue | 52 +--- apps/app/src/layouts/components/AppShell.vue | 214 +++++++++++++++ apps/app/src/layouts/default.vue | 88 +++--- 10 files changed, 415 insertions(+), 407 deletions(-) create mode 100644 apps/app/src/layouts/components/AppShell.vue diff --git a/apps/app/auto-imports.d.ts b/apps/app/auto-imports.d.ts index 2d95ae79..2bf7c39b 100644 --- a/apps/app/auto-imports.d.ts +++ b/apps/app/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 FORM_API_ERRORS_KEY: typeof import('./src/composables/useFormError')['FORM_API_ERRORS_KEY'] 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'] @@ -245,6 +246,7 @@ declare global { const useFocus: typeof import('@vueuse/core')['useFocus'] const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin'] const useFormDraft: typeof import('./src/composables/useFormDraft')['useFormDraft'] + const useFormError: typeof import('./src/composables/useFormError')['useFormError'] const useFps: typeof import('@vueuse/core')['useFps'] const useFullscreen: typeof import('@vueuse/core')['useFullscreen'] const useGamepad: typeof import('@vueuse/core')['useGamepad'] @@ -394,6 +396,7 @@ declare module 'vue' { interface ComponentCustomProperties { readonly COOKIE_MAX_AGE_1_YEAR: UnwrapRef readonly EffectScope: UnwrapRef + readonly FORM_API_ERRORS_KEY: UnwrapRef readonly PUBLIC_FORM_LOCALE_KEY: UnwrapRef readonly PUBLIC_FORM_TOKEN_KEY: UnwrapRef readonly acceptHMRUpdate: UnwrapRef @@ -621,6 +624,7 @@ declare module 'vue' { readonly useFocus: UnwrapRef readonly useFocusWithin: UnwrapRef readonly useFormDraft: UnwrapRef + readonly useFormError: UnwrapRef readonly useFps: UnwrapRef readonly useFullscreen: UnwrapRef readonly useGamepad: UnwrapRef diff --git a/apps/app/components.d.ts b/apps/app/components.d.ts index 74de8c1d..1651ab32 100644 --- a/apps/app/components.d.ts +++ b/apps/app/components.d.ts @@ -96,9 +96,11 @@ declare module 'vue' { FormErrorState: typeof import('./src/components/shared/public-form/FormErrorState.vue')['default'] FormFailureDetail: typeof import('./src/components/form-failures/FormFailureDetail.vue')['default'] FormFailuresTable: typeof import('./src/components/form-failures/FormFailuresTable.vue')['default'] + FormField: typeof import('./src/components/forms/FormField.vue')['default'] FormStepper: typeof import('./src/components/shared/public-form/FormStepper.vue')['default'] GridBg: typeof import('./src/components/timetable/GridBg.vue')['default'] I18n: typeof import('./src/@core/components/I18n.vue')['default'] + Icon: typeof import('./src/components/Icon.vue')['default'] IdentityMatchBanner: typeof import('./src/components/shared/public-form/IdentityMatchBanner.vue')['default'] ImageUploadField: typeof import('./src/components/common/ImageUploadField.vue')['default'] ImpersonateDialog: typeof import('./src/components/platform/ImpersonateDialog.vue')['default'] diff --git a/apps/app/src/layouts/OrganizerLayout.vue b/apps/app/src/layouts/OrganizerLayout.vue index a576535c..838126e7 100644 --- a/apps/app/src/layouts/OrganizerLayout.vue +++ b/apps/app/src/layouts/OrganizerLayout.vue @@ -1,9 +1,54 @@ diff --git a/apps/app/src/layouts/PortalLayout.vue b/apps/app/src/layouts/PortalLayout.vue index 151a29ba..99a0963f 100644 --- a/apps/app/src/layouts/PortalLayout.vue +++ b/apps/app/src/layouts/PortalLayout.vue @@ -1,234 +1,35 @@ diff --git a/apps/app/src/layouts/PublicLayout.vue b/apps/app/src/layouts/PublicLayout.vue index c86af6c6..e18b76d1 100644 --- a/apps/app/src/layouts/PublicLayout.vue +++ b/apps/app/src/layouts/PublicLayout.vue @@ -1,16 +1,13 @@ diff --git a/apps/app/src/layouts/__tests__/OrganizerLayout.spec.ts b/apps/app/src/layouts/__tests__/OrganizerLayout.spec.ts index fcf2f112..32baf1df 100644 --- a/apps/app/src/layouts/__tests__/OrganizerLayout.spec.ts +++ b/apps/app/src/layouts/__tests__/OrganizerLayout.spec.ts @@ -1,13 +1,34 @@ import { describe, expect, it, vi } from 'vitest' import { mount } from '@vue/test-utils' +import { createPinia, setActivePinia } from 'pinia' -// Mock the import path so OrganizerLayout's `import DefaultLayoutWithVerticalNav` -// statement doesn't pull in the real component (which transitively imports -// Vuetify .css files that the trimmed-down vitest config can't transform). -vi.mock('@/layouts/components/DefaultLayoutWithVerticalNav.vue', () => ({ - default: { template: '
' }, +// Mock AppShell so the OrganizerLayout test isolates the layout +// wrapper's job (compute nav items, pass them to AppShell, expose +// RouterView via default slot). AppShell's own behavior is exercised +// by its own tests / Playwright CT in F4. +vi.mock('@/layouts/components/AppShell.vue', () => ({ + default: { + template: '
', + props: ['navItems', 'title'], + }, })) +vi.mock('@/stores/useAuthStore', () => ({ + useAuthStore: () => ({ + organisations: [], + currentOrganisation: null, + isSuperAdmin: false, + }), +})) + +vi.mock('@/stores/useImpersonationStore', () => ({ + useImpersonationStore: () => ({ + isImpersonating: false, + }), +})) + +setActivePinia(createPinia()) + const OrganizerLayout = (await import('../OrganizerLayout.vue')).default describe('OrganizerLayout', () => { @@ -19,6 +40,7 @@ describe('OrganizerLayout', () => { }) expect(wrapper.exists()).toBe(true) + expect(wrapper.find('[data-test="app-shell"]').exists()).toBe(true) }) it('renders a RouterView in its slot', () => { diff --git a/apps/app/src/layouts/__tests__/PortalLayout.spec.ts b/apps/app/src/layouts/__tests__/PortalLayout.spec.ts index 1893045f..df6457b1 100644 --- a/apps/app/src/layouts/__tests__/PortalLayout.spec.ts +++ b/apps/app/src/layouts/__tests__/PortalLayout.spec.ts @@ -2,97 +2,54 @@ import { describe, expect, it, vi } from 'vitest' import { mount } from '@vue/test-utils' import { createPinia, setActivePinia } from 'pinia' -// Mock auto-imported composables / external pinia stores so the layout's -// - - diff --git a/apps/app/src/layouts/components/AppShell.vue b/apps/app/src/layouts/components/AppShell.vue new file mode 100644 index 00000000..9761e1d2 --- /dev/null +++ b/apps/app/src/layouts/components/AppShell.vue @@ -0,0 +1,214 @@ + + + diff --git a/apps/app/src/layouts/default.vue b/apps/app/src/layouts/default.vue index 3a7257b5..11c00282 100644 --- a/apps/app/src/layouts/default.vue +++ b/apps/app/src/layouts/default.vue @@ -1,57 +1,55 @@ - - -