diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..dc5f215 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,4 @@ + +# ADR-013 Phase 4 staged plugin frontends (generated by scripts/stage-frontend.mjs) +src/.plugins-staged/ +src/router/routes.gen.js diff --git a/frontend/package.json b/frontend/package.json index bad73bd..b6d303c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,7 +4,10 @@ "private": true, "type": "module", "scripts": { + "stage": "node ../scripts/stage-frontend.mjs", + "predev": "npm run stage", "dev": "vite", + "prebuild": "npm run stage", "build": "vite build", "preview": "vite preview", "test": "vitest run", diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index ba99a3e..8bcb0a9 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -6,14 +6,27 @@ import { setupComplete, setupSkipped, isSetupLoaded, refreshSetupState } from '. import { loadEnabledPlugins, isPluginEnabled } from '../composables/enabledPlugins' import { useToast } from '../composables/toast' import { getFacilityName } from '../utils/siteSettings' +import { stagedChildren, stagedToplevel } from './routes.gen' // Auto-discover all route modules from routes/ directory. A module's default // export is AppLayout child routes; an optional `toplevel` export is full-screen // routes (kiosk, print pages) that live OUTSIDE AppLayout. Both let a plugin own // its routes, so pruning the plugin removes them with no core edit (ADR-013). const routeModules = import.meta.glob('./routes/*.js', { eager: true }) -const rawChildren = Object.values(routeModules).flatMap(m => m.default) -const pluginTopLevel = Object.values(routeModules).flatMap(m => m.toplevel || []) +// Staged plugin frontends (relocated to plugins//frontend/) are +// aggregated into routes.gen.js by scripts/stage-frontend.mjs. In-tree route +// files (./routes/*.js) cover plugins not yet relocated - dual-location during +// the Phase 4 transition. +const rawChildren = [ + // A route file may export only `toplevel` (no AppLayout children), so guard + // the default with an empty array. + ...Object.values(routeModules).flatMap(m => m.default || []), + ...stagedChildren, +] +const pluginTopLevel = [ + ...Object.values(routeModules).flatMap(m => m.toplevel || []), + ...stagedToplevel, +] // Gather the settings pages (spread across plugin route files) and nest them // under a single two-pane shell so the grouped rail stays put while the right diff --git a/frontend/src/router/routes/printedparts.js b/frontend/src/router/routes/printedparts.js deleted file mode 100644 index 91f6066..0000000 --- a/frontend/src/router/routes/printedparts.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Printedparts plugin routes. - * - * Auto-discovered by the router via import.meta.glob, so no registration - * edit is needed. Every route carries meta.plugin 'printedparts' so the ADR-009 - * guard redirects to the dashboard when the printedparts backend plugin is - * disabled. Form routes add requiresAuth so anonymous users cannot reach - * create or edit. - */ -export default [ - { - path: 'printedparts', - name: 'printedparts', - component: () => import('../../views/printedparts/PrintedItemsList.vue'), - meta: { requiresAuth: true, plugin: 'printedparts' } - }, - { - path: 'printedparts/new', - name: 'printedparts-new', - component: () => import('../../views/printedparts/PrintedItemForm.vue'), - meta: { requiresAuth: true, plugin: 'printedparts' } - }, - { - path: 'printedparts/:id', - name: 'printedparts-detail', - component: () => import('../../views/printedparts/PrintedItemDetail.vue'), - meta: { requiresAuth: true, plugin: 'printedparts' } - }, - { - path: 'printedparts/:id/edit', - name: 'printedparts-edit', - component: () => import('../../views/printedparts/PrintedItemForm.vue'), - meta: { requiresAuth: true, plugin: 'printedparts' } - }, - { - path: 'settings/printedparts', - name: 'settings-printedparts', - component: () => import('../../views/settings/PrintedPartsSettings.vue'), - meta: { requiresAuth: true, requiresAdmin: true, plugin: 'printedparts' } - } -] - -// Full-screen routes (outside AppLayout), collected by the router's `toplevel` -// pass so pruning printedparts removes them without a core edit (ADR-013). -export const toplevel = [ - { - // Touch kiosk for taking 3D-printed parts: scan bin, scan badge, keypad. - // Open on purpose - see the decision record in the printedparts proposal. - path: '/parts-kiosk', - name: 'parts-kiosk', - component: () => import('../../views/printedparts/PartsKiosk.vue'), - meta: { plugin: 'printedparts' } - }, - { - // Requires login: lists the whole catalog, printedparts.view-gated at the API. - path: '/print/printedparts-labels', - name: 'print-printedparts-labels', - component: () => import('../../views/print/PrintedPartsLabels.vue'), - meta: { requiresAuth: true, plugin: 'printedparts' } - } -] diff --git a/plugins/printedparts/frontend/routes.js b/plugins/printedparts/frontend/routes.js new file mode 100644 index 0000000..46c11b3 --- /dev/null +++ b/plugins/printedparts/frontend/routes.js @@ -0,0 +1,58 @@ +/** + * Printedparts plugin frontend routes (ADR-013 Phase 4: self-contained plugin + * frontend). Views live beside this file under views/; core imports use the + * @/ alias. The stage-frontend step copies this whole frontend/ dir into the + * Vite tree and aggregates these routes, so a per-site build that omits + * printedparts carries none of this code. + * + * `default` = AppLayout child routes; `toplevel` = full-screen routes. + */ +export default [ + { + path: 'printedparts', + name: 'printedparts', + component: () => import('./views/PrintedItemsList.vue'), + meta: { requiresAuth: true, plugin: 'printedparts' } + }, + { + path: 'printedparts/new', + name: 'printedparts-new', + component: () => import('./views/PrintedItemForm.vue'), + meta: { requiresAuth: true, plugin: 'printedparts' } + }, + { + path: 'printedparts/:id', + name: 'printedparts-detail', + component: () => import('./views/PrintedItemDetail.vue'), + meta: { requiresAuth: true, plugin: 'printedparts' } + }, + { + path: 'printedparts/:id/edit', + name: 'printedparts-edit', + component: () => import('./views/PrintedItemForm.vue'), + meta: { requiresAuth: true, plugin: 'printedparts' } + }, + { + path: 'settings/printedparts', + name: 'settings-printedparts', + component: () => import('./views/PrintedPartsSettings.vue'), + meta: { requiresAuth: true, requiresAdmin: true, plugin: 'printedparts' } + } +] + +export const toplevel = [ + { + // Touch kiosk for taking 3D-printed parts: scan bin, scan badge, keypad. + path: '/parts-kiosk', + name: 'parts-kiosk', + component: () => import('./views/PartsKiosk.vue'), + meta: { plugin: 'printedparts' } + }, + { + // Requires login: lists the whole catalog, printedparts.view-gated at the API. + path: '/print/printedparts-labels', + name: 'print-printedparts-labels', + component: () => import('./views/PrintedPartsLabels.vue'), + meta: { requiresAuth: true, plugin: 'printedparts' } + } +] diff --git a/frontend/src/views/printedparts/PartsKiosk.vue b/plugins/printedparts/frontend/views/PartsKiosk.vue similarity index 98% rename from frontend/src/views/printedparts/PartsKiosk.vue rename to plugins/printedparts/frontend/views/PartsKiosk.vue index 543d547..dcf49b3 100644 --- a/frontend/src/views/printedparts/PartsKiosk.vue +++ b/plugins/printedparts/frontend/views/PartsKiosk.vue @@ -91,9 +91,9 @@