refactor(modules): move Modules into Company Settings as Module Configuration

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.
This commit is contained in:
Darko Gjorgjijoski
2026-04-11 06:30:00 +02:00
parent e44657bf7e
commit 31a2a66127
9 changed files with 78 additions and 72 deletions

View File

@@ -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'

View File

@@ -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',
},
},
]

View File

@@ -1,20 +1,14 @@
<template>
<BasePage>
<BasePageHeader :title="$t('modules.title')">
<BaseBreadcrumb>
<BaseBreadcrumbItem :title="$t('general.home')" to="dashboard" />
<BaseBreadcrumbItem :title="$t('modules.module', 2)" to="#" active />
</BaseBreadcrumb>
</BasePageHeader>
<p class="mt-4 text-sm text-muted max-w-3xl">
{{ $t('modules.index.description') }}
</p>
<BaseSettingCard
:title="$t('modules.title')"
:description="$t('modules.index.description')"
>
<ModuleSettingsModal />
<!-- Loading skeleton -->
<div
v-if="store.isFetching && store.modules.length === 0"
class="grid mt-8 w-full grid-cols-1 items-start gap-6 lg:grid-cols-2 xl:grid-cols-3"
class="grid mt-8 w-full grid-cols-1 items-start gap-6 lg:grid-cols-2"
>
<div v-for="n in 3" :key="n" class="h-32 bg-surface-tertiary rounded-lg animate-pulse" />
</div>
@@ -41,25 +35,39 @@
<!-- Module list -->
<div
v-else
class="grid mt-8 w-full grid-cols-1 items-start gap-6 lg:grid-cols-2 xl:grid-cols-3"
class="grid mt-8 w-full grid-cols-1 items-start gap-6 lg:grid-cols-2"
>
<CompanyModuleCard
v-for="mod in store.modules"
:key="mod.slug"
:data="mod"
@open-settings="openSettingsModal"
/>
</div>
</BasePage>
</BaseSettingCard>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { useModalStore } from '@/scripts/stores/modal.store'
import { useCompanyModulesStore } from '../store'
import type { CompanyModuleSummary } from '../store'
import CompanyModuleCard from '../components/CompanyModuleCard.vue'
import ModuleSettingsModal from '../components/ModuleSettingsModal.vue'
const store = useCompanyModulesStore()
const modalStore = useModalStore()
onMounted(() => {
store.fetchModules()
})
function openSettingsModal(module: CompanyModuleSummary): void {
modalStore.openModal({
componentName: 'ModuleSettingsModal',
title: module.display_name,
data: module,
size: 'lg',
})
}
</script>

View File

@@ -3,7 +3,7 @@
<BasePageHeader :title="pageTitle">
<BaseBreadcrumb>
<BaseBreadcrumbItem :title="$t('general.home')" to="dashboard" />
<BaseBreadcrumbItem :title="$t('modules.title')" to="/admin/modules" />
<BaseBreadcrumbItem :title="$t('modules.title')" to="/admin/settings/modules" />
<BaseBreadcrumbItem :title="pageTitle" to="#" active />
</BaseBreadcrumb>
</BasePageHeader>

View File

@@ -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