diff --git a/config/invoiceshelf.php b/config/invoiceshelf.php index 1f8b15dc..d15408c7 100644 --- a/config/invoiceshelf.php +++ b/config/invoiceshelf.php @@ -277,6 +277,16 @@ return [ 'ability' => '', 'model' => '', ], + [ + 'title' => 'settings.menu_title.module_configuration', + 'group' => '', + 'name' => 'Module Configuration', + 'link' => '/admin/settings/modules', + 'icon' => 'PuzzlePieceIcon', + 'owner_only' => false, + 'ability' => 'manage modules', + 'model' => '', + ], ], /* @@ -367,18 +377,6 @@ return [ 'ability' => 'view-expense', 'model' => Expense::class, ], - [ - 'title' => 'navigation.modules', - 'group' => 'admin', - 'group_label' => 'navigation.admin', - 'priority' => 10, - 'link' => '/admin/modules', - 'icon' => 'PuzzlePieceIcon', - 'name' => 'Modules', - 'owner_only' => false, - 'ability' => 'manage modules', - 'model' => '', - ], [ 'title' => 'navigation.members', 'group' => 'admin', diff --git a/lang/en.json b/lang/en.json index 71794564..30ae3fff 100644 --- a/lang/en.json +++ b/lang/en.json @@ -915,7 +915,8 @@ "exchange_rate": "Exchange Rate", "address_information": "Address Information", "pdf_generation": "PDF Generation", - "appearance": "Appearance" + "appearance": "Appearance", + "module_configuration": "Module Configuration" }, "appearance": { "title": "Appearance", diff --git a/resources/scripts/features/company/modules/index.ts b/resources/scripts/features/company/modules/index.ts index fbeb6ab1..8f5b1b86 100644 --- a/resources/scripts/features/company/modules/index.ts +++ b/resources/scripts/features/company/modules/index.ts @@ -1,5 +1,3 @@ -export { moduleRoutes } from './routes' - export { useCompanyModulesStore } from './store' export type { CompanyModuleSummary, @@ -8,7 +6,7 @@ export type { // Views export { default as CompanyModulesIndexView } from './views/CompanyModulesIndexView.vue' -export { default as ModuleSettingsView } from './views/ModuleSettingsView.vue' // Components export { default as CompanyModuleCard } from './components/CompanyModuleCard.vue' +export { default as ModuleSettingsModal } from './components/ModuleSettingsModal.vue' diff --git a/resources/scripts/features/company/modules/routes.ts b/resources/scripts/features/company/modules/routes.ts deleted file mode 100644 index e7a6ae24..00000000 --- a/resources/scripts/features/company/modules/routes.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { RouteRecordRaw } from 'vue-router' - -const CompanyModulesIndexView = () => import('./views/CompanyModulesIndexView.vue') -const ModuleSettingsView = () => import('./views/ModuleSettingsView.vue') - -/** - * Company-context module routes. - * - * - `/admin/modules` — read-only Active Modules index, lists every module the - * super admin has activated on this instance with a "Settings" link. - * - `/admin/modules/:slug/settings` — schema-rendered settings form for a - * specific active module, backed by the InvoiceShelf\Modules\Registry::settingsFor() - * schema and CompanySetting persistence (per-company values). - * - * The marketplace browser (install/uninstall/activate) lives in the super-admin - * context at `/admin/administration/modules`, see features/admin/modules/routes.ts. - */ -export const moduleRoutes: RouteRecordRaw[] = [ - { - path: 'modules', - name: 'modules.index', - component: CompanyModulesIndexView, - meta: { - requiresAuth: true, - ability: 'manage-module', - title: 'modules.title', - }, - }, - { - path: 'modules/:slug/settings', - name: 'modules.settings', - component: ModuleSettingsView, - meta: { - requiresAuth: true, - ability: 'manage-module', - title: 'modules.settings.title', - }, - }, -] diff --git a/resources/scripts/features/company/modules/views/CompanyModulesIndexView.vue b/resources/scripts/features/company/modules/views/CompanyModulesIndexView.vue index f593951f..c8b7895e 100644 --- a/resources/scripts/features/company/modules/views/CompanyModulesIndexView.vue +++ b/resources/scripts/features/company/modules/views/CompanyModulesIndexView.vue @@ -1,20 +1,14 @@ - - - - - - - - - - {{ $t('modules.index.description') }} - + + @@ -41,25 +35,39 @@ - + diff --git a/resources/scripts/features/company/modules/views/ModuleSettingsView.vue b/resources/scripts/features/company/modules/views/ModuleSettingsView.vue index c76d9196..0df593dd 100644 --- a/resources/scripts/features/company/modules/views/ModuleSettingsView.vue +++ b/resources/scripts/features/company/modules/views/ModuleSettingsView.vue @@ -3,7 +3,7 @@ - + diff --git a/resources/scripts/features/company/settings/routes.ts b/resources/scripts/features/company/settings/routes.ts index c910a522..2f18303e 100644 --- a/resources/scripts/features/company/settings/routes.ts +++ b/resources/scripts/features/company/settings/routes.ts @@ -56,6 +56,10 @@ const settingsRoutes: RouteRecordRaw[] = [ path: 'mail-configuration', redirect: { name: 'settings.mail-config' }, }, + { + path: 'modules-configuration', + redirect: { name: 'settings.modules' }, + }, { path: 'company-info', name: 'settings.company-info', @@ -163,6 +167,15 @@ const settingsRoutes: RouteRecordRaw[] = [ }, component: () => import('./views/RolesView.vue'), }, + { + path: 'modules', + name: 'settings.modules', + meta: { + requiresAuth: true, + isOwner: true, + }, + component: () => import('@/scripts/features/company/modules/views/CompanyModulesIndexView.vue'), + }, { path: 'danger-zone', name: 'settings.danger-zone', @@ -230,6 +243,17 @@ const settingsRoutes: RouteRecordRaw[] = [ path: 'mail-configuration', redirect: { name: 'settings.mail-config' }, }, + // Legacy module routes — preserved so existing bookmarks (and the old in-app + // sidebar slot at /admin/modules) still resolve to the Module Configuration + // page, which now lives under Company Settings. + { + path: 'modules', + redirect: { name: 'settings.modules' }, + }, + { + path: 'modules/:slug/settings', + redirect: { name: 'settings.modules' }, + }, ] export default settingsRoutes diff --git a/resources/scripts/router/index.ts b/resources/scripts/router/index.ts index 98fd0e79..fd56ba87 100644 --- a/resources/scripts/router/index.ts +++ b/resources/scripts/router/index.ts @@ -22,7 +22,6 @@ import itemRoutes from '../features/company/items/routes' import memberRoutes from '../features/company/members/routes' import reportRoutes from '../features/company/reports/routes' import settingsRoutes from '../features/company/settings/routes' -import { moduleRoutes } from '../features/company/modules/routes' // Guard import { authGuard } from './guards' @@ -57,7 +56,6 @@ const companyChildren: RouteRecordRaw[] = [ ...memberRoutes, ...reportRoutes, ...settingsRoutes, - ...moduleRoutes, ] /** @@ -80,6 +78,7 @@ const routes: RouteRecordRaw[] = [ // Admin area: company-scoped routes { path: '/admin', + name: 'admin', component: CompanyLayout, meta: { requiresAuth: true }, children: companyChildren, diff --git a/tests/Feature/Company/Modules/ModuleRoutesConfigTest.php b/tests/Feature/Company/Modules/ModuleRoutesConfigTest.php new file mode 100644 index 00000000..48729822 --- /dev/null +++ b/tests/Feature/Company/Modules/ModuleRoutesConfigTest.php @@ -0,0 +1,17 @@ +not->toBeFalse(); + + // The settings.modules child route must declare isOwner so non-owners can't + // reach the Module Configuration page even if they hit the URL directly. + expect($contents)->toMatch("/name:\\s*'settings\\.modules'[\\s\\S]*?isOwner:\\s*true/"); + + // The frontend route deliberately does not duplicate the backend ability + // check — that's enforced server-side via the menu filter and the + // controller-level Bouncer policy. + expect($contents)->not->toContain("ability: 'manage-module'"); + expect($contents)->not->toContain("ability: 'manage modules'"); +});
- {{ $t('modules.index.description') }} -