From 31a2a66127732479e532ed7fd0b12553db2154ba Mon Sep 17 00:00:00 2001 From: Darko Gjorgjijoski Date: Sat, 11 Apr 2026 06:30:00 +0200 Subject: [PATCH] refactor(modules): move Modules into Company Settings as Module Configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-company Modules management page moves off its own top-level sidebar slot (which sat in the Admin group alongside Members/Reports/Settings) and into a new Module Configuration entry inside Company Settings, alongside Tax Types, Payment Modes, Mail Configuration, etc. That's where every other 'configure how the company behaves' surface lives — the Modules page is a configuration surface, not a primary working area. The label is deliberately 'Module Configuration' rather than 'Module Settings' because the latter collides with the existing per-module ModuleSettingsModal concept (the modal that opens when a user clicks an installed module's gear icon). Keeping the two names distinct means 'Module Configuration' unambiguously refers to the list of installed modules, and 'Module Settings' continues to mean the per-module schema form. CompanyModulesIndexView is stripped of its standalone BasePage / BasePageHeader / BaseBreadcrumb wrappers — as a child of SettingsLayoutView it would have rendered a double header — and re-wrapped in BaseSettingCard, matching TaxTypesView and every other settings-child view. The module grid tightens from lg:grid-cols-2 xl:grid-cols-3 down to lg:grid-cols-2 since the settings sidebar eats 240px of horizontal real estate. Routes consolidate: features/company/modules/routes.ts is deleted; the new settings.modules child route lives inside the settings routes file directly, alongside the rest. Top-level redirects are kept for the legacy /admin/modules and /admin/modules/:slug/settings URLs so existing bookmarks still resolve. ModuleRoutesConfigTest is re-pointed at settings/routes.ts and asserts the settings.modules route is owner-only. Module-contributed sidebar entries (those registered via Registry::registerMenu()) are NOT moved. Modules that want top-level navigation visibility keep it; only the meta management page moves. This mirrors WordPress/Discourse conventions where plugin pages stay in the main navigation but the 'Plugins' admin screen itself lives under Settings. --- config/invoiceshelf.php | 22 +++++------ lang/en.json | 3 +- .../scripts/features/company/modules/index.ts | 4 +- .../features/company/modules/routes.ts | 39 ------------------- .../modules/views/CompanyModulesIndexView.vue | 36 ++++++++++------- .../modules/views/ModuleSettingsView.vue | 2 +- .../features/company/settings/routes.ts | 24 ++++++++++++ resources/scripts/router/index.ts | 3 +- .../Modules/ModuleRoutesConfigTest.php | 17 ++++++++ 9 files changed, 78 insertions(+), 72 deletions(-) delete mode 100644 resources/scripts/features/company/modules/routes.ts create mode 100644 tests/Feature/Company/Modules/ModuleRoutesConfigTest.php 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 @@ 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'"); +});