mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-16 01:34:08 +00:00
feat(modules): relocate marketplace browser to super-admin context
The module marketplace browser UI (ModuleIndexView, ModuleDetailView,
ModuleCard, the four-step installer store) was filed under
features/company/modules/ only by historical accident — it's authorized via
the manage modules ability (super-admin-only) and conceptually belongs in the
admin context, not the company context.
- Move features/company/modules/{store.ts, views/ModuleIndexView.vue,
views/ModuleDetailView.vue, components/ModuleCard.vue} to
features/admin/modules/.
- Update hardcoded /admin/modules/... paths in the moved files to
/admin/administration/modules/... so the breadcrumbs and ModuleCard
navigation target the new admin-context routes.
- Tighten the four-step installer's silent catch {} blocks in the moved
store.ts: errors were being swallowed, now they dispatch through the
global notification store instead.
- New features/admin/modules/routes.ts declares admin.modules.index +
admin.modules.view as children of /admin/administration with
meta.isSuperAdmin: true.
- features/admin/{index,routes}.ts re-export and mount the relocated routes.
- config/invoiceshelf.php gains a new AdminModules entry in admin_menu
pointing at /admin/administration/modules with super_admin_only: true.
- The dev-gated navigation.modules entry in main_menu is replaced (not
deleted) with a non-gated entry pointing at the new company-context
Active Modules index page that lands in the next commit. The
ability is set to manage modules so non-owners can't see it.
The new company-context Active Modules index, schema-driven settings page,
and dynamic sidebar group are introduced in subsequent commits.
This commit is contained in:
193
resources/scripts/features/admin/modules/store.ts
Normal file
193
resources/scripts/features/admin/modules/store.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { moduleService } from '../../../api/services/module.service'
|
||||
import type {
|
||||
Module,
|
||||
ModuleReview,
|
||||
ModuleFaq,
|
||||
ModuleLink,
|
||||
ModuleScreenshot,
|
||||
} from '../../../types/domain/module'
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Types
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
export interface ModuleDetailMeta {
|
||||
modules: Module[]
|
||||
}
|
||||
|
||||
export interface ModuleDetailResponse {
|
||||
data: Module
|
||||
meta: ModuleDetailMeta
|
||||
}
|
||||
|
||||
export interface InstallationStep {
|
||||
translationKey: string
|
||||
stepUrl: string
|
||||
time: string | null
|
||||
started: boolean
|
||||
completed: boolean
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Store
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
export interface ModuleState {
|
||||
currentModule: ModuleDetailResponse | null
|
||||
modules: Module[]
|
||||
apiToken: string | null
|
||||
currentUser: {
|
||||
api_token: string | null
|
||||
}
|
||||
enableModules: string[]
|
||||
}
|
||||
|
||||
export const useModuleStore = defineStore('modules', {
|
||||
state: (): ModuleState => ({
|
||||
currentModule: null,
|
||||
modules: [],
|
||||
apiToken: null,
|
||||
currentUser: {
|
||||
api_token: null,
|
||||
},
|
||||
enableModules: [],
|
||||
}),
|
||||
|
||||
getters: {
|
||||
salesTaxUSEnabled: (state): boolean =>
|
||||
state.enableModules.includes('SalesTaxUS'),
|
||||
|
||||
installedModules: (state): Module[] =>
|
||||
state.modules.filter((m) => m.installed),
|
||||
},
|
||||
|
||||
actions: {
|
||||
async fetchModules(): Promise<void> {
|
||||
const response = await moduleService.list()
|
||||
this.modules = response.data
|
||||
},
|
||||
|
||||
async fetchModule(slug: string): Promise<ModuleDetailResponse> {
|
||||
const response = await moduleService.get(slug)
|
||||
const data = response as unknown as ModuleDetailResponse
|
||||
|
||||
if ((data as Record<string, unknown>).error === 'invalid_token') {
|
||||
this.currentModule = null
|
||||
this.modules = []
|
||||
this.apiToken = null
|
||||
this.currentUser.api_token = null
|
||||
return data
|
||||
}
|
||||
|
||||
this.currentModule = data
|
||||
return data
|
||||
},
|
||||
|
||||
async checkApiToken(token: string): Promise<{ success: boolean; error?: string }> {
|
||||
const response = await moduleService.checkToken(token)
|
||||
return {
|
||||
success: response.success ?? false,
|
||||
error: response.error,
|
||||
}
|
||||
},
|
||||
|
||||
async disableModule(moduleName: string): Promise<{ success: boolean }> {
|
||||
return moduleService.disable(moduleName)
|
||||
},
|
||||
|
||||
async enableModule(moduleName: string): Promise<{ success: boolean }> {
|
||||
return moduleService.enable(moduleName)
|
||||
},
|
||||
|
||||
async installModule(
|
||||
moduleName: string,
|
||||
version: string,
|
||||
onStepUpdate?: (step: InstallationStep) => void,
|
||||
): Promise<boolean> {
|
||||
const steps: InstallationStep[] = [
|
||||
{
|
||||
translationKey: 'modules.download_zip_file',
|
||||
stepUrl: '/api/v1/modules/download',
|
||||
time: null,
|
||||
started: false,
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
translationKey: 'modules.unzipping_package',
|
||||
stepUrl: '/api/v1/modules/unzip',
|
||||
time: null,
|
||||
started: false,
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
translationKey: 'modules.copying_files',
|
||||
stepUrl: '/api/v1/modules/copy',
|
||||
time: null,
|
||||
started: false,
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
translationKey: 'modules.completing_installation',
|
||||
stepUrl: '/api/v1/modules/complete',
|
||||
time: null,
|
||||
started: false,
|
||||
completed: false,
|
||||
},
|
||||
]
|
||||
|
||||
let path: string | null = null
|
||||
|
||||
for (const step of steps) {
|
||||
step.started = true
|
||||
onStepUpdate?.(step)
|
||||
|
||||
try {
|
||||
const stepFns: Record<string, () => Promise<Record<string, unknown>>> = {
|
||||
'/api/v1/modules/download': () =>
|
||||
moduleService.download({ module: moduleName, version, path: path ?? undefined } as never) as Promise<Record<string, unknown>>,
|
||||
'/api/v1/modules/unzip': () =>
|
||||
moduleService.unzip({ module: moduleName, version, path: path ?? undefined } as never) as Promise<Record<string, unknown>>,
|
||||
'/api/v1/modules/copy': () =>
|
||||
moduleService.copy({ module: moduleName, version, path: path ?? undefined } as never) as Promise<Record<string, unknown>>,
|
||||
'/api/v1/modules/complete': () =>
|
||||
moduleService.complete({ module: moduleName, version, path: path ?? undefined } as never) as Promise<Record<string, unknown>>,
|
||||
}
|
||||
|
||||
const result = await stepFns[step.stepUrl]()
|
||||
step.completed = true
|
||||
onStepUpdate?.(step)
|
||||
|
||||
if ((result as Record<string, unknown>).path) {
|
||||
path = (result as Record<string, unknown>).path as string
|
||||
}
|
||||
|
||||
if (!(result as Record<string, unknown>).success) {
|
||||
const message = (result as Record<string, unknown>).error
|
||||
if (typeof message === 'string') {
|
||||
const { useNotificationStore } = await import('@/scripts/stores/notification.store')
|
||||
useNotificationStore().showNotification({
|
||||
type: 'error',
|
||||
message,
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
step.completed = true
|
||||
onStepUpdate?.(step)
|
||||
const { useNotificationStore } = await import('@/scripts/stores/notification.store')
|
||||
useNotificationStore().showNotification({
|
||||
type: 'error',
|
||||
message: err instanceof Error ? err.message : 'Module installation failed',
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export type ModuleStore = ReturnType<typeof useModuleStore>
|
||||
Reference in New Issue
Block a user