feat(gui-v2): wire OrganizerLayoutV2 to compose the real shell components
Replaces the Plan-1 skeleton stubs: OrganizerLayoutV2 now fills AppShellV2's #sidebar/#topbar/#drawer slots with the ported AppSidebar / AppTopbar / RightDrawer and sources orgNavItems via useV2Nav() (legal now that OrganizerLayoutV2.vue is the layouts-v2 zone). AppShellV2 is unchanged; its contract test stays green. New component test locks the composition (right component per slot, :groups forwarded, no skeleton). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,26 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
// OrganizerLayoutV2 — v2 layout file selected by
|
||||
// definePage({ meta: { layout: 'OrganizerLayoutV2' } }) on pages-v2/**
|
||||
// (RFC-WS-GUI-REDESIGN AD-G2). Plan 1: wires the skeleton + RouterView.
|
||||
// A later plan fills the sidebar/topbar/drawer slots with ported
|
||||
// PrimeVue shell pieces.
|
||||
/**
|
||||
* OrganizerLayoutV2 — v2 shell layout selected by
|
||||
* definePage({ meta: { layout: 'OrganizerLayoutV2' } }) on pages-v2/**
|
||||
* (RFC-WS-GUI-REDESIGN AD-G2).
|
||||
*
|
||||
* Plan 2 Task 7 — composition only: fills AppShellV2's named slots with
|
||||
* the ported PrimeVue shell pieces (AppSidebar / AppTopbar / RightDrawer)
|
||||
* and renders routed pages via <RouterView/> in the default slot.
|
||||
*
|
||||
* Nav data is sourced HERE: the layouts zone may import @/navigation,
|
||||
* whereas components-v2 may NOT (import-boundary matrix). orgNavItems is
|
||||
* folded into V2NavGroup[] by useV2Nav() and passed to AppSidebar :groups.
|
||||
*
|
||||
* No provide/inject: each shell piece reads its own state from
|
||||
* useShellUiStore / useAuthStore (RFC AD-G4). This layout wires
|
||||
* composition + nav data only; it owns no shell state.
|
||||
*/
|
||||
import { orgNavItems } from '@/navigation/vertical'
|
||||
import { useV2Nav } from '@/composables/useV2Nav'
|
||||
import AppShellV2 from '@/layouts/components/AppShellV2.vue'
|
||||
import AppSidebar from '@/components-v2/layout/AppSidebar.vue'
|
||||
import AppTopbar from '@/components-v2/layout/AppTopbar.vue'
|
||||
import RightDrawer from '@/components-v2/layout/RightDrawer.vue'
|
||||
|
||||
const { groups } = useV2Nav(orgNavItems)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppShellV2>
|
||||
<template #sidebar>
|
||||
<nav class="w-72 border-r border-surface-200 p-4 dark:border-surface-800">
|
||||
<span class="text-sm text-surface-500">Crewli v2</span>
|
||||
</nav>
|
||||
<AppSidebar :groups="groups" />
|
||||
</template>
|
||||
|
||||
<template #topbar>
|
||||
<header class="flex h-14 items-center border-b border-surface-200 px-6 dark:border-surface-800">
|
||||
<span class="text-sm text-surface-500">v2 shell (skeleton)</span>
|
||||
</header>
|
||||
<AppTopbar />
|
||||
</template>
|
||||
|
||||
<RouterView />
|
||||
|
||||
<template #drawer>
|
||||
<RightDrawer />
|
||||
</template>
|
||||
</AppShellV2>
|
||||
</template>
|
||||
|
||||
77
apps/app/tests/component/layouts/OrganizerLayoutV2.spec.ts
Normal file
77
apps/app/tests/component/layouts/OrganizerLayoutV2.spec.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import { createMemoryHistory, createRouter } from 'vue-router'
|
||||
import { defineComponent } from 'vue'
|
||||
import OrganizerLayoutV2 from '@/layouts/OrganizerLayoutV2.vue'
|
||||
import AppShellV2 from '@/layouts/components/AppShellV2.vue'
|
||||
|
||||
// Stub the 3 leaf shell components: this test verifies COMPOSITION
|
||||
// (right component in right slot + nav data forwarded), not their
|
||||
// internals (those have their own unit tests). Stubs keep jsdom free
|
||||
// of PrimeVue teleport/overlay/breakpoint machinery.
|
||||
const AppSidebarStub = defineComponent({
|
||||
name: 'AppSidebarStub',
|
||||
props: { groups: { type: Array, default: () => [] } },
|
||||
template: '<aside data-testid="sidebar-stub" :data-groups-count="groups.length" />',
|
||||
})
|
||||
|
||||
const AppTopbarStub = defineComponent({
|
||||
name: 'AppTopbarStub',
|
||||
template: '<header data-testid="topbar-stub" />',
|
||||
})
|
||||
|
||||
const RightDrawerStub = defineComponent({
|
||||
name: 'RightDrawerStub',
|
||||
template: '<aside data-testid="drawer-stub" />',
|
||||
})
|
||||
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [{ path: '/', name: 'home', component: defineComponent({ template: '<div data-testid="page" />' }) }],
|
||||
})
|
||||
|
||||
async function mountLayout() {
|
||||
router.push('/')
|
||||
await router.isReady()
|
||||
|
||||
return mount(OrganizerLayoutV2, {
|
||||
global: {
|
||||
plugins: [createPinia(), router],
|
||||
stubs: {
|
||||
AppSidebar: AppSidebarStub,
|
||||
AppTopbar: AppTopbarStub,
|
||||
RightDrawer: RightDrawerStub,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('OrganizerLayoutV2 (wired shell)', () => {
|
||||
it('composes AppShellV2 with the real shell components in each slot', async () => {
|
||||
const wrapper = await mountLayout()
|
||||
|
||||
expect(wrapper.findComponent(AppShellV2).exists()).toBe(true)
|
||||
expect(wrapper.find('[data-testid="appshell-v2"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-testid="sidebar-stub"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-testid="topbar-stub"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-testid="drawer-stub"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-testid="page"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('forwards orgNavItems folded into V2NavGroup[] to AppSidebar :groups', async () => {
|
||||
const wrapper = await mountLayout()
|
||||
const sidebar = wrapper.find('[data-testid="sidebar-stub"]')
|
||||
|
||||
// orgNavItems has link entries + a {heading:'Beheer'} → useV2Nav folds
|
||||
// into ≥1 group. A non-zero count proves the nav chain is wired.
|
||||
expect(Number(sidebar.attributes('data-groups-count'))).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('no longer renders the Plan-1 skeleton placeholders', async () => {
|
||||
const wrapper = await mountLayout()
|
||||
|
||||
expect(wrapper.text()).not.toContain('skeleton')
|
||||
expect(wrapper.text()).not.toContain('Crewli v2')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user