From 3c25db55a3a7e00930f83128e90b9773d2bd1abf Mon Sep 17 00:00:00 2001 From: Pierre Brisorgueil Date: Tue, 14 Apr 2026 23:09:02 +0200 Subject: [PATCH 1/2] refactor(admin): flatten to single routed tab bar (#3974) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split `admin.content.vue` into four routed views (Users, Organizations, Readiness, Activity) and mount them as children of the `/admin` parent route. The admin layout now renders a single flat tab bar combining the built-ins with the `config.admin.tabs` extras, so downstream modules contribute siblings instead of being stacked under a separate "General" bar. Global concerns (error banner + mailer warning) moved to the layout to stay visible across every admin tab. - Split `admin.content.vue` → `admin.users.view.vue`, `admin.organizations.view.vue`, `admin.readiness.view.vue`, `admin.activity.view.vue` - Update `admin.router.js`: `''` redirects to `Admin Users`; each section gets a dedicated child route (`users`, `organizations`, `readiness`, `activity`) keeping CASL meta propagation - Update `admin.layout.vue`: merge built-in + extra tabs in a single `` row, longest-prefix active-tab resolution keeps detail routes (`/admin/users/:id`) highlighting the parent tab - Readiness and Activity fetch on `mounted()` instead of via a nested tab-model watcher - Redispatch unit tests across the four new views + refresh router and layout specs for the new structure - Document the change in `MIGRATIONS.md` (URL-level breaking change; downstream config unchanged) --- MIGRATIONS.md | 71 +++ src/lib/helpers/tests/router.unit.tests.js | 2 +- src/modules/admin/router/admin.router.js | 54 +- .../tests/admin.activity.view.unit.tests.js | 133 ++--- .../admin/tests/admin.content.unit.tests.js | 93 ---- .../admin/tests/admin.layout.unit.tests.js | 140 ++--- .../admin.organizations.view.unit.tests.js | 71 +++ .../tests/admin.readiness.view.unit.tests.js | 82 +-- .../admin/tests/admin.router.unit.tests.js | 52 +- .../tests/admin.users.view.unit.tests.js | 124 +++++ .../admin/views/admin.activity.view.vue | 231 +++++++++ src/modules/admin/views/admin.content.vue | 488 ------------------ src/modules/admin/views/admin.layout.vue | 107 +++- .../admin/views/admin.organizations.view.vue | 62 +++ .../admin/views/admin.readiness.view.vue | 89 ++++ src/modules/admin/views/admin.users.view.vue | 170 ++++++ 16 files changed, 1136 insertions(+), 833 deletions(-) delete mode 100644 src/modules/admin/tests/admin.content.unit.tests.js create mode 100644 src/modules/admin/tests/admin.organizations.view.unit.tests.js create mode 100644 src/modules/admin/tests/admin.users.view.unit.tests.js create mode 100644 src/modules/admin/views/admin.activity.view.vue delete mode 100644 src/modules/admin/views/admin.content.vue create mode 100644 src/modules/admin/views/admin.organizations.view.vue create mode 100644 src/modules/admin/views/admin.readiness.view.vue create mode 100644 src/modules/admin/views/admin.users.view.vue diff --git a/MIGRATIONS.md b/MIGRATIONS.md index c26104907..9b352f164 100644 --- a/MIGRATIONS.md +++ b/MIGRATIONS.md @@ -4,6 +4,77 @@ Breaking changes and upgrade notes for downstream projects. --- +## Admin tabs flattened to a single routed row (2026-04-14) + +**Breaking change (URL-level).** The admin section now exposes its four +built-in sections (Users, Organizations, Readiness, Activity) as routed +siblings of the downstream `config.admin.tabs` extras. There is no more +nested "General" tab with an internal `v-window` — every tab is a real +URL and renders through the same ``. + +### What changed in the stack + +- **REMOVED:** `src/modules/admin/views/admin.content.vue` — the inner + nested tab bar is gone. Its four window items are now dedicated views. +- **NEW:** + - `src/modules/admin/views/admin.users.view.vue` + - `src/modules/admin/views/admin.organizations.view.vue` + - `src/modules/admin/views/admin.readiness.view.vue` + - `src/modules/admin/views/admin.activity.view.vue` +- **CHANGED:** `src/modules/admin/router/admin.router.js` — the parent + `/admin` route now has dedicated children: + - `''` → redirect to `{ name: 'Admin Users' }` + - `users` → `Admin Users` + - `users/:id` → `Admin User` + - `organizations` → `Admin Organizations` + - `organizations/:organizationId` → `Admin Organization` + - `readiness` → `Admin Readiness` + - `activity` → `Admin Activity` +- **CHANGED:** `src/modules/admin/views/admin.layout.vue` — the tab bar + now renders built-in tabs + `config.admin.tabs` extras in one flat row. + The global error banner and the mailer warning were moved from the old + `admin.content.vue` into the layout so they stay visible across every + admin tab (including downstream extras). +- Readiness and Activity now fetch their data on `mounted()` (previously + via a `watch: { tab }` inside the old nested window). + +### Action for downstream projects + +**No config change is required.** The mechanism for contributing extra +admin tabs via `config.admin.tabs` + `injectAdminChildren` is unchanged, +and extras continue to render inline inside the admin layout. + +1. Run `/update-stack` to pull the changes. +2. Verify hard-coded links in your project. The old URL `/admin` used to + land on the nested "General" tab; it now 301-redirects to + `/admin/users`, so existing links keep working. If you want to point + somewhere else, use one of the new stable URLs: + - `/admin/users` + - `/admin/organizations` + - `/admin/readiness` + - `/admin/activity` + Optional check: `grep -r "/admin" src/`. +3. Downstream projects that override `admin.content.vue` must delete the + override — the file no longer exists. Replace any such override with + a dedicated override of `admin.users.view.vue` / + `admin.organizations.view.vue` / `admin.readiness.view.vue` / + `admin.activity.view.vue`, or attach a custom tab via + `config.admin.tabs` + `injectAdminChildren`. +4. No route-name breakage for downstream extras — `injectAdminChildren` + still mounts your routes as children of the same `/admin` parent. + +### Why + +Two stacked tab bars on `*/admin` (top-level General + extras, nested +Users / Organizations / Readiness / Activity) was visually noisy and +duplicated navigation. The original intent of `config.admin.tabs` was +for downstream extras to live **alongside** the built-ins — not above +them. Flattening gives every admin section a real URL, preserves +deep-linking and browser back/forward, and keeps downstream extras +config-driven with zero migration work. + +--- + ## Admin extra tabs are now nested routes (2026-04-12) **Breaking change.** Downstream projects that added admin tabs via diff --git a/src/lib/helpers/tests/router.unit.tests.js b/src/lib/helpers/tests/router.unit.tests.js index 72ad5904a..7f2893e9f 100644 --- a/src/lib/helpers/tests/router.unit.tests.js +++ b/src/lib/helpers/tests/router.unit.tests.js @@ -10,7 +10,7 @@ const makeAdminRoutes = () => [ path: '/admin', component: { name: 'AdminLayout' }, children: [ - { path: '', name: 'Admin', component: { name: 'AdminContent' } }, + { path: 'users', name: 'Admin Users', component: { name: 'AdminUsers' } }, { path: 'users/:id', name: 'Admin User', component: { name: 'AdminUser' } }, ], }, diff --git a/src/modules/admin/router/admin.router.js b/src/modules/admin/router/admin.router.js index afc8f8cdb..b983c3c8b 100644 --- a/src/modules/admin/router/admin.router.js +++ b/src/modules/admin/router/admin.router.js @@ -2,7 +2,10 @@ * Module dependencies. */ import adminLayout from '../views/admin.layout.vue'; -import adminContent from '../views/admin.content.vue'; +import adminUsers from '../views/admin.users.view.vue'; +import adminOrganizations from '../views/admin.organizations.view.vue'; +import adminReadiness from '../views/admin.readiness.view.vue'; +import adminActivity from '../views/admin.activity.view.vue'; import adminUser from '../views/admin.user.view.vue'; import adminOrganization from '../views/admin.organization.view.vue'; @@ -10,14 +13,17 @@ import adminOrganization from '../views/admin.organization.view.vue'; * Router configuration. * * The admin module exports a **parent route** (`/admin`) whose component - * is the admin layout (page header + tab bar + ``). Built-in - * views (general content, user/organization detail) and any downstream - * injected child routes are rendered inside that ``. + * is the admin layout (page header + tab bar + ``). Each + * built-in section (Users, Organizations, Readiness, Activity) is a + * routed child so that the tab bar and the downstream-contributed extras + * (`config.admin.tabs`) live in one flat navigation row. * - * Downstream modules should **not** add sibling routes like - * `/admin/knowledge` anymore. Instead, register them via the - * `injectAdminChildren` helper in `@/lib/helpers/router` so they become - * children of this parent route. + * Detail views (`users/:id`, `organizations/:organizationId`) are also + * children of the same parent — they deep-link inside the layout. + * + * Downstream modules that contribute an "admin tab" must register their + * routes via the `injectAdminChildren` helper in `@/lib/helpers/router` + * with **relative** paths (e.g. `'knowledge'`, not `'/admin/knowledge'`). */ export default [ { @@ -33,8 +39,12 @@ export default [ children: [ { path: '', - name: 'Admin General', - component: adminContent, + redirect: { name: 'Admin Users' }, + }, + { + path: 'users', + name: 'Admin Users', + component: adminUsers, meta: { action: 'manage', subject: 'UserAdmin', }, @@ -48,6 +58,14 @@ export default [ action: 'manage', subject: 'UserAdmin', }, }, + { + path: 'organizations', + name: 'Admin Organizations', + component: adminOrganizations, + meta: { + action: 'manage', subject: 'UserAdmin', + }, + }, { path: 'organizations/:organizationId', name: 'Admin Organization', @@ -57,6 +75,22 @@ export default [ action: 'manage', subject: 'UserAdmin', }, }, + { + path: 'readiness', + name: 'Admin Readiness', + component: adminReadiness, + meta: { + action: 'manage', subject: 'UserAdmin', + }, + }, + { + path: 'activity', + name: 'Admin Activity', + component: adminActivity, + meta: { + action: 'manage', subject: 'UserAdmin', + }, + }, ], }, ]; diff --git a/src/modules/admin/tests/admin.activity.view.unit.tests.js b/src/modules/admin/tests/admin.activity.view.unit.tests.js index 6210d11fb..e71719360 100644 --- a/src/modules/admin/tests/admin.activity.view.unit.tests.js +++ b/src/modules/admin/tests/admin.activity.view.unit.tests.js @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { setActivePinia, createPinia } from 'pinia'; import { shallowMount } from '@vue/test-utils'; import { useAdminStore } from '../stores/admin.store'; -import AdminView from '../views/admin.content.vue'; +import AdminActivity from '../views/admin.activity.view.vue'; // Mock axios vi.mock('../../../lib/services/axios', () => ({ @@ -19,49 +19,41 @@ vi.mock('../../../lib/services/config', () => ({ api: { protocol: 'http', host: 'localhost', port: '3000', base: 'api' }, cookie: { prefix: 'devkit' }, vuetify: { theme: { flat: true, rounded: 'rounded-lg' } }, - whitelists: { users: { roles: ['user', 'admin'] } }, }, })); -// Mock helpers -vi.mock('../../../lib/helpers/roleColor', () => ({ default: () => 'primary' })); -vi.mock('../../../lib/helpers/orgColor', () => ({ default: () => 'blue' })); - const vuetifyStubs = { - PageHeader: true, - coreDataTableComponent: true, - 'router-link': { template: '' }, 'v-container': { template: '
' }, - 'v-row': { template: '
' }, - 'v-col': { template: '
' }, - 'v-card': { template: '
' }, - 'v-tabs': { template: '
' }, - 'v-tab': { template: '
' }, - 'v-divider': { template: '
' }, - 'v-window': { template: '
' }, - 'v-window-item': { template: '
' }, + 'v-alert': { template: '
' }, 'v-table': { template: '
' }, 'v-chip': { template: '' }, 'v-icon': { template: '' }, 'v-progress-linear': { template: '
' }, 'v-btn': { template: '
' }, - 'v-menu': { template: '
' }, - 'v-list': { template: '
' }, - 'v-list-subheader': { template: '
' }, - 'v-list-item': { template: '
' }, - 'v-list-item-title': { template: '
' }, - 'v-alert': { template: '
' }, - 'v-dialog': { template: '
' }, - 'v-card-title': { template: '
' }, - 'v-card-text': { template: '
' }, - 'v-card-actions': { template: '
' }, - 'v-spacer': { template: '
' }, 'v-text-field': { template: '' }, 'v-select': { template: '' }, - 'v-select': { template: '