feat(frontend): Q1+Q2 — Plugin-Routen kommen aus Manifesten, statische Duplikate entfernt
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
Phase Q1 (Seiten-Routen) + Q2 (Settings-Routen): Die Plugin-Manifeste sind ab sofort die einzige Quelle fuer Plugin-Frontend-Routen. routes/index.tsx enthaelt nur noch Core-Routen + die StartLayout-Hub-Baeume (/agents, /automation, /logs, /help — verschachtelte Sub-Navigation). - PluginRouteRenderer: variante 'settings' rendert settings_pages mit bare Sub-Segments (Descendant-Matching im /settings-Subtree); Variante 'pages' (Default) behaelt absolute Pfade. Getrennte Entry-Listen verhindern Pfad-Kollisionen (settings 'mail' vs. page '/mail'). - Entfernt: 14 statische AppShell-Plugin-Routen + 9 statische Settings-Routen + 20 tote Lazy-Imports (search, calendar+kanban, dms, dms/trash, mail, mail/settings, reports, tasks, communication, workflows, import-export, tags, wiki, roles, users, groups, notifications, ai, ai-proactive, automation, documents). - Manifeste ergaenzt: Calendar +/calendar/kanban, Tags +/tags (+ Menue-Item, Route war sonst unerreichbar), Automation: /workflows-Permission auf workflows:read (Paritaet zur ersetzten statischen Route). - Automation: tote flache Manifest-Eintraeger fuer /agents + /automation entfernt (StartLayout-Hub-Baeume gewinnen diese Pfade immer — die Eintraege matchten nie). - Komponenten-Map regeneriert (37 Eintraege, CalendarKanban + Tags neu). Verifikation: tsc exit 0; production build exit 0; Vitest Dashboard + MiniAppWindow + pluginStore 35/35; Backend-Regressionen Route-Order, M5-MiniApps, N4-Scope, N3-Filtering 49/49; compileall sauber; Cross-Plugin-Checker 497 Dateien / 0 verbotene Imports; ruff clean.
This commit is contained in:
@@ -102,7 +102,7 @@ class AutomationPlugin(BasePlugin):
|
||||
path="/workflows",
|
||||
icon="Workflow",
|
||||
order=52,
|
||||
permission="automation:read",
|
||||
permission="workflows:read",
|
||||
),
|
||||
FrontendMenuItem(
|
||||
label_key="nav.importExport",
|
||||
@@ -137,24 +137,15 @@ class AutomationPlugin(BasePlugin):
|
||||
permission="contacts:read",
|
||||
),
|
||||
],
|
||||
# Phase Q1: /agents and /automation are served by the static
|
||||
# StartLayout hub trees (sub-navigation). Flat manifest entries for
|
||||
# them were dead duplicates (never matched) and were removed.
|
||||
page_routes=[
|
||||
FrontendPageRoute(
|
||||
path="/automation",
|
||||
component="@/pages/AutomationDashboard",
|
||||
order=50,
|
||||
permission="automation:read",
|
||||
),
|
||||
FrontendPageRoute(
|
||||
path="/agents",
|
||||
component="@/pages/AgentDashboard",
|
||||
order=51,
|
||||
permission="agents:read",
|
||||
),
|
||||
FrontendPageRoute(
|
||||
path="/workflows",
|
||||
component="@/pages/Workflows",
|
||||
order=52,
|
||||
permission="automation:read",
|
||||
permission="workflows:read",
|
||||
),
|
||||
FrontendPageRoute(
|
||||
path="/import-export",
|
||||
|
||||
@@ -64,6 +64,8 @@ class CalendarPlugin(BasePlugin):
|
||||
],
|
||||
page_routes=[
|
||||
FrontendPageRoute(path='/calendar', component='@/pages/Calendar', protected=True, permission='calendar:read'),
|
||||
# Q1: kanban view was static-only before - now manifest-declared.
|
||||
FrontendPageRoute(path='/calendar/kanban', component='@/pages/CalendarKanban', protected=True, permission='calendar:read'),
|
||||
],
|
||||
# BUG (ghost component): ContactCalendarTab does not exist in the
|
||||
# frontend — tab removed until implemented (Block I-D).
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.plugins.base import BasePlugin
|
||||
from app.plugins.manifest import PluginManifest, PluginRouteDef
|
||||
from app.plugins.manifest import FrontendMenuItem, FrontendPageRoute, PluginManifest, PluginRouteDef
|
||||
|
||||
|
||||
class TagsPlugin(BasePlugin):
|
||||
@@ -30,6 +30,26 @@ class TagsPlugin(BasePlugin):
|
||||
"tags:delete",
|
||||
"tags:admin",
|
||||
],
|
||||
# Q1: the /tags page was a static-only route before - now
|
||||
# manifest-declared (route + sidebar menu item).
|
||||
menu_items=[
|
||||
FrontendMenuItem(
|
||||
label_key="nav.tags",
|
||||
label="Tags",
|
||||
path="/tags",
|
||||
icon="Tags",
|
||||
order=80,
|
||||
permission="tags:read",
|
||||
),
|
||||
],
|
||||
page_routes=[
|
||||
FrontendPageRoute(
|
||||
path="/tags",
|
||||
component="@/pages/Tags",
|
||||
protected=True,
|
||||
permission="tags:read",
|
||||
),
|
||||
],
|
||||
is_core=True,
|
||||
# BUG (ghost component): ContactTagsTab does not exist in the
|
||||
# frontend — tab removed until implemented (Block I-D).
|
||||
|
||||
@@ -22,7 +22,19 @@ import { PluginPage } from './PluginLoader';
|
||||
* route group in routes/index.tsx:
|
||||
* { path: '*', element: <PluginRouteRenderer /> }
|
||||
*/
|
||||
export function PluginRouteRenderer() {
|
||||
/**
|
||||
* variant:
|
||||
* - 'pages' (default): renders manifest page_routes. Mounted at the
|
||||
* AppShell catch-all — descendant <Routes> match the full pathname,
|
||||
* so entries keep their absolute paths (e.g. '/mail').
|
||||
* - 'settings': renders manifest settings_pages. Mounted INSIDE the
|
||||
* /settings subtree catch-all — descendant matching works on the
|
||||
* REMAINING pathname ('/mail' after the '/settings' prefix is stripped),
|
||||
* so entries keep the bare settings sub-segment ('mail'). Mixing both
|
||||
* entry kinds in one list would create path collisions (e.g. settings
|
||||
* 'mail' vs. page '/mail' rendering the inbox).
|
||||
*/
|
||||
export function PluginRouteRenderer({ variant = 'pages' }: { variant?: 'pages' | 'settings' } = {}) {
|
||||
const location = useLocation();
|
||||
const manifests = usePluginStore((s) => s.manifests);
|
||||
const loaded = usePluginStore((s) => s.loaded);
|
||||
@@ -30,16 +42,33 @@ export function PluginRouteRenderer() {
|
||||
// Flatten manifests → route entries with their owning plugin attached.
|
||||
// Attaching the name here (instead of a later find()) stays correct even
|
||||
// if two plugins declare overlapping paths.
|
||||
const entries = useMemo(
|
||||
() =>
|
||||
manifests.flatMap((m) =>
|
||||
type RendererEntry = {
|
||||
route: { path: string; component: string; permission?: string };
|
||||
pluginName: string;
|
||||
};
|
||||
|
||||
const entries = useMemo<RendererEntry[]>(() => {
|
||||
if (variant === 'settings') {
|
||||
return manifests.flatMap((m) =>
|
||||
(Array.isArray(m.settings_pages) ? m.settings_pages : []).map((p) => ({
|
||||
route: {
|
||||
// bare sub-segment ('mail') — descendant matching strips the
|
||||
// matched '/settings' prefix; tolerate 'settings/mail' too.
|
||||
path: p.path.replace(/^\/?settings\//, ''),
|
||||
component: p.component,
|
||||
permission: p.permission || undefined,
|
||||
},
|
||||
pluginName: m.display_name || m.name || p.path,
|
||||
}))
|
||||
);
|
||||
}
|
||||
return manifests.flatMap((m) =>
|
||||
m.page_routes.map((r) => ({
|
||||
route: r,
|
||||
pluginName: m.display_name || m.name || r.path,
|
||||
}))
|
||||
),
|
||||
[manifests]
|
||||
);
|
||||
}, [manifests, variant]);
|
||||
|
||||
// If manifests haven't loaded yet, show a spinner (not null/blank)
|
||||
if (!loaded) {
|
||||
|
||||
@@ -31,10 +31,9 @@ export const PLUGIN_COMPONENT_MAP: Record<string, LazyComponentFactory> = {
|
||||
'@/components/dashboard/WikiRecentWidget': () => import('@/components/dashboard/WikiRecentWidget').then((m) => ({ default: m.WikiRecentWidget })),
|
||||
'@/pages/AIAssistant': () => import('@/pages/AIAssistant').then(normalizeModule),
|
||||
'@/pages/AISettings': () => import('@/pages/AISettings').then((m) => ({ default: m.AISettingsPage })),
|
||||
'@/pages/AgentDashboard': () => import('@/pages/AgentDashboard').then((m) => ({ default: m.AgentDashboardPage })),
|
||||
'@/pages/AutomationDashboard': () => import('@/pages/AutomationDashboard').then((m) => ({ default: m.AutomationDashboardPage })),
|
||||
'@/pages/AutomationSettings': () => import('@/pages/AutomationSettings').then((m) => ({ default: m.AutomationSettingsPage })),
|
||||
'@/pages/Calendar': () => import('@/pages/Calendar').then(normalizeModule),
|
||||
'@/pages/CalendarKanban': () => import('@/pages/CalendarKanban').then(normalizeModule),
|
||||
'@/pages/Communication': () => import('@/pages/Communication').then(normalizeModule),
|
||||
'@/pages/ContactDetailPage': () => import('@/pages/ContactDetailPage').then((m) => ({ default: m.ContactDetailPage })),
|
||||
'@/pages/ContactsList': () => import('@/pages/ContactsList').then((m) => ({ default: m.ContactsListPage })),
|
||||
@@ -52,6 +51,7 @@ export const PLUGIN_COMPONENT_MAP: Record<string, LazyComponentFactory> = {
|
||||
'@/pages/SettingsNotifications': () => import('@/pages/SettingsNotifications').then((m) => ({ default: m.SettingsNotificationsPage })),
|
||||
'@/pages/SettingsRoles': () => import('@/pages/SettingsRoles').then((m) => ({ default: m.SettingsRolesPage })),
|
||||
'@/pages/SettingsUsers': () => import('@/pages/SettingsUsers').then((m) => ({ default: m.SettingsUsersPage })),
|
||||
'@/pages/Tags': () => import('@/pages/Tags').then((m) => ({ default: m.TagsPage })),
|
||||
'@/pages/Tasks': () => import('@/pages/Tasks').then((m) => ({ default: m.TasksPage })),
|
||||
'@/pages/Wiki': () => import('@/pages/Wiki').then((m) => ({ default: m.WikiPage })),
|
||||
'@/pages/Workflows': () => import('@/pages/Workflows').then((m) => ({ default: m.WorkflowsPage })),
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// TODO: P2-F1 — Replace hardcoded plugin routes with dynamic PluginRouteRenderer
|
||||
// Phase Q1 (2026-09-13): plugin routes are served dynamically from the manifests
|
||||
// via PluginRouteRenderer. Only core routes and the StartLayout hub trees
|
||||
// (/agents, /automation, /logs, /help — sub-navigation layouts) stay static.
|
||||
import React, { Suspense } from 'react';
|
||||
import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom';
|
||||
import { AppShell } from '@/components/layout/AppShell';
|
||||
@@ -17,25 +19,14 @@ import { ErrorBoundary } from '@/components/common/ErrorBoundary';
|
||||
// Lazy-loaded pages (code-splitting)
|
||||
const DashboardPage = React.lazy(() => import('@/pages/Dashboard').then(m => ({ default: m.DashboardPage })));
|
||||
const AuditLogPage = React.lazy(() => import('@/pages/AuditLog').then(m => ({ default: m.AuditLogPage })));
|
||||
const GlobalSearchResultsPage = React.lazy(() => import('@/pages/GlobalSearchResults').then(m => ({ default: m.GlobalSearchResultsPage })));
|
||||
const SettingsPage = React.lazy(() => import('@/pages/Settings').then(m => ({ default: m.SettingsPage })));
|
||||
const SettingsProfilePage = React.lazy(() => import('@/pages/SettingsProfile').then(m => ({ default: m.SettingsProfilePage })));
|
||||
const SettingsRolesPage = React.lazy(() => import('@/pages/SettingsRoles').then(m => ({ default: m.SettingsRolesPage })));
|
||||
const SettingsUsersPage = React.lazy(() => import('@/pages/SettingsUsers').then(m => ({ default: m.SettingsUsersPage })));
|
||||
const SettingsGroupsPage = React.lazy(() => import('@/pages/SettingsGroups').then(m => ({ default: m.SettingsGroupsPage })));
|
||||
const SettingsPluginsPage = React.lazy(() => import('@/pages/SettingsPlugins').then(m => ({ default: m.SettingsPluginsPage })));
|
||||
const SettingsSystemPage = React.lazy(() => import('@/pages/SettingsSystem').then(m => ({ default: m.SettingsSystemPage })));
|
||||
const SettingsCurrenciesPage = React.lazy(() => import('@/pages/SettingsCurrencies').then(m => ({ default: m.SettingsCurrenciesPage })));
|
||||
const SettingsTaxesPage = React.lazy(() => import('@/pages/SettingsTaxes').then(m => ({ default: m.SettingsTaxesPage })));
|
||||
const SettingsSequencesPage = React.lazy(() => import('@/pages/SettingsSequences').then(m => ({ default: m.SettingsSequencesPage })));
|
||||
const CalendarPage = React.lazy(() => import('@/pages/Calendar').then(m => ({ default: m.CalendarPage })));
|
||||
const CalendarKanbanPage = React.lazy(() => import('@/pages/CalendarKanban').then(m => ({ default: m.CalendarKanbanPage })));
|
||||
const DmsPage = React.lazy(() => import('@/pages/Dms').then(m => ({ default: m.DmsPage })));
|
||||
const DmsTrashPage = React.lazy(() => import('@/pages/DmsTrash').then(m => ({ default: m.DmsTrashPage })));
|
||||
const TrashPage = React.lazy(() => import('@/pages/Trash').then(m => ({ default: m.TrashPage })));
|
||||
const MailPage = React.lazy(() => import('@/pages/Mail').then(m => ({ default: m.MailPage })));
|
||||
const MailSettingsPage = React.lazy(() => import('@/pages/MailSettings').then(m => ({ default: m.MailSettingsPage })));
|
||||
const SettingsNotificationsPage = React.lazy(() => import('@/pages/SettingsNotifications').then(m => ({ default: m.SettingsNotificationsPage })));
|
||||
const AISettingsPage = React.lazy(() => import('@/pages/AISettings').then(m => ({ default: m.AISettingsPage })));
|
||||
const ProactiveAISettings = React.lazy(() => import('@/pages/ProactiveAISettings').then(m => ({ default: m.ProactiveAISettings })));
|
||||
const SettingsThemePage = React.lazy(() => import('@/pages/SettingsTheme').then(m => ({ default: m.SettingsThemePage })));
|
||||
@@ -49,15 +40,7 @@ const SettingsMenuOrderPage = React.lazy(() => import('@/pages/SettingsMenuOrder
|
||||
const SettingsStammdatenPage = React.lazy(() => import('@/pages/SettingsStammdaten').then(m => ({ default: m.SettingsStammdatenPage })));
|
||||
const SettingsUserManagementPage = React.lazy(() => import('@/pages/SettingsUserManagement').then(m => ({ default: m.SettingsUserManagementPage })));
|
||||
const AutomationDashboardPage = React.lazy(() => import('@/pages/AutomationDashboard').then(m => ({ default: m.AutomationDashboardPage })));
|
||||
const AgentDashboardPage = React.lazy(() => import('@/pages/AgentDashboard').then(m => ({ default: m.AgentDashboardPage })));
|
||||
const AutomationSettingsPage = React.lazy(() => import('@/pages/AutomationSettings').then(m => ({ default: m.AutomationSettingsPage })));
|
||||
const ReportsPage = React.lazy(() => import('@/pages/Reports').then(m => ({ default: m.ReportsPage })));
|
||||
const DocumentSettingsPage = React.lazy(() => import('@/pages/DocumentSettings').then(m => ({ default: m.DocumentSettingsPage })));
|
||||
const TasksPage = React.lazy(() => import('@/pages/Tasks').then(m => ({ default: m.TasksPage })));
|
||||
const CommunicationPage = React.lazy(() => import('@/pages/Communication').then(m => ({ default: m.CommunicationPage })));
|
||||
const WorkflowsPage = React.lazy(() => import('@/pages/Workflows').then(m => ({ default: m.WorkflowsPage })));
|
||||
const ImportExportPage = React.lazy(() => import('@/pages/ImportExport').then(m => ({ default: m.ImportExportPage })));
|
||||
const TagsPage = React.lazy(() => import('@/pages/Tags').then(m => ({ default: m.TagsPage })));
|
||||
const CustomFieldsPage = React.lazy(() => import('@/pages/CustomFields').then(m => ({ default: m.CustomFieldsPage })));
|
||||
const ActivityTimelinePage = React.lazy(() => import('@/pages/ActivityTimeline').then(m => ({ default: m.ActivityTimelinePage })));
|
||||
const ApprovalsPage = React.lazy(() => import('@/pages/Approvals').then(m => ({ default: m.ApprovalsPage })));
|
||||
@@ -85,7 +68,6 @@ const LogsOverviewPage = React.lazy(() => import('@/pages/logs/LogsOverview').th
|
||||
const LogsPlaceholderPage = React.lazy(() => import('@/pages/logs/LogsPlaceholder').then(m => ({ default: m.LogsPlaceholderPage })));
|
||||
const HelpApiDocsPage = React.lazy(() => import('@/pages/help/HelpApiDocs').then(m => ({ default: m.HelpApiDocsPage })));
|
||||
const ApiDocsPage = React.lazy(() => import('@/pages/ApiDocs').then(m => ({ default: m.ApiDocsPage })));
|
||||
const WikiPage = React.lazy(() => import('@/pages/Wiki').then(m => ({ default: m.WikiPage })));
|
||||
const SystemDashboardPage = React.lazy(() => import('@/pages/SystemDashboard').then(m => ({ default: m.SystemDashboardPage })));
|
||||
|
||||
/** Centered spinner fallback for lazy-loaded routes */
|
||||
@@ -205,30 +187,25 @@ const router = createBrowserRouter([
|
||||
children: [
|
||||
{ path: 'stammdaten', element: withSuspense(<SettingsStammdatenPage />) },
|
||||
{ path: 'user-management', element: withSuspense(<SettingsUserManagementPage />) },
|
||||
{ path: 'roles', element: withSuspense(<SettingsRolesPage />) },
|
||||
{ path: 'users', element: withSuspense(<SettingsUsersPage />) },
|
||||
{ path: 'groups', element: withSuspense(<SettingsGroupsPage />) },
|
||||
// Phase Q2: plugin settings sub-routes (roles, users, groups, mail,
|
||||
// notifications, ai, ai-proactive, automation, documents) are served
|
||||
// by the manifests via PluginRouteRenderer - single source of truth.
|
||||
{ path: 'plugins', element: withSuspense(<SettingsPluginsPage />) },
|
||||
{ path: 'system', element: withSuspense(<SettingsSystemPage />) },
|
||||
{ path: 'currencies', element: withSuspense(<SettingsCurrenciesPage />) },
|
||||
{ path: 'taxes', element: withSuspense(<SettingsTaxesPage />) },
|
||||
{ path: 'sequences', element: withSuspense(<SettingsSequencesPage />) },
|
||||
{ path: 'mail', element: withSuspense(<MailSettingsPage />) },
|
||||
{ path: 'notifications', element: withSuspense(<SettingsNotificationsPage />) },
|
||||
{ path: 'ai', element: withSuspense(<AISettingsPage />) },
|
||||
{ path: 'ai-proactive', element: withSuspense(<ProactiveAISettings />) },
|
||||
{ path: 'theme', element: withSuspense(<SettingsThemePage />) },
|
||||
{ path: 'automation', element: withSuspense(<AutomationSettingsPage />) },
|
||||
{ path: 'mcp', element: withSuspense(<SettingsMcpPage />) },
|
||||
{ path: 'ai-settings', element: withSuspense(<SettingsAIPage />) },
|
||||
{ path: 'menu', element: withSuspense(<SettingsMenuOrderPage />) },
|
||||
{ path: 'custom-fields', element: withSuspense(<CustomFieldsPage />) },
|
||||
{ path: 'webhooks', element: withSuspense(<SettingsWebhooksPage />) },
|
||||
{ path: 'workspaces', element: withSuspense(<WorkspaceManagerPage />) },
|
||||
{ path: 'documents', element: withSuspense(<DocumentSettingsPage />) },
|
||||
{ path: 'backup', element: withSuspense(<SettingsBackupPage />) },
|
||||
{ path: 'rechte', element: <PermissionRoute permission="settings:read">{withSuspense(<SettingsRechtePage />)}</PermissionRoute> },
|
||||
{ path: '*', element: <ErrorBoundary>{<PluginRouteRenderer />}</ErrorBoundary> },
|
||||
// Phase Q2: plugin settings sub-pages render with bare sub-segments
|
||||
{ path: '*', element: <ErrorBoundary>{<PluginRouteRenderer variant="settings" />}</ErrorBoundary> },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -243,24 +220,14 @@ const router = createBrowserRouter([
|
||||
{ path: '/', element: <Navigate to="/start" replace /> },
|
||||
{ path: '/dashboard', element: withSuspense(<DashboardPage />) },
|
||||
{ path: '/audit-log', element: <PermissionRoute permission="audit:read">{withSuspense(<AuditLogPage />)}</PermissionRoute> },
|
||||
{ path: '/search', element: withSuspense(<GlobalSearchResultsPage />) },
|
||||
{ path: '/calendar', element: <PermissionRoute permission="calendar:read">{withSuspense(<CalendarPage />)}</PermissionRoute> },
|
||||
{ path: '/calendar/kanban', element: <PermissionRoute permission="calendar:read">{withSuspense(<CalendarKanbanPage />)}</PermissionRoute> },
|
||||
{ path: '/dms', element: <PermissionRoute permission="dms:read">{withSuspense(<DmsPage />)}</PermissionRoute> },
|
||||
{ path: '/dms/trash', element: <PermissionRoute permission="dms:read">{withSuspense(<DmsTrashPage />)}</PermissionRoute> },
|
||||
// Phase Q1: plugin business routes (search, calendar + kanban, dms,
|
||||
// dms/trash, mail, mail/settings, reports, tasks, communication,
|
||||
// workflows, import-export, tags, wiki) are served by the manifests
|
||||
// via PluginRouteRenderer below - single source of truth.
|
||||
{ path: '/trash', element: <PermissionRoute permission="contacts:read">{withSuspense(<TrashPage />)}</PermissionRoute> },
|
||||
{ path: '/mail', element: <PermissionRoute permission="mail:read">{withSuspense(<MailPage />)}</PermissionRoute> },
|
||||
{ path: '/mail/settings', element: <PermissionRoute permission="mail:config">{withSuspense(<MailSettingsPage />)}</PermissionRoute> },
|
||||
{ path: '/reports', element: <PermissionRoute permission="reports:read">{withSuspense(<ReportsPage />)}</PermissionRoute> },
|
||||
{ path: '/tasks', element: <PermissionRoute permission="tasks:read">{withSuspense(<TasksPage />)}</PermissionRoute> },
|
||||
{ path: '/communication', element: <PermissionRoute permission="comm:read">{withSuspense(<CommunicationPage />)}</PermissionRoute> },
|
||||
{ path: '/workflows', element: <PermissionRoute permission="workflows:read">{withSuspense(<WorkflowsPage />)}</PermissionRoute> },
|
||||
{ path: '/import-export', element: <PermissionRoute permission="import_export:read">{withSuspense(<ImportExportPage />)}</PermissionRoute> },
|
||||
{ path: 'tags', element: <PermissionRoute permission="tags:read">{withSuspense(<TagsPage />)}</PermissionRoute> },
|
||||
{ path: '/approvals', element: <PermissionRoute permission="approvals:read">{withSuspense(<ApprovalsPage />)}</PermissionRoute> },
|
||||
{ path: '/api-docs', element: <PermissionRoute permission="settings:read">{withSuspense(<ApiDocsPage />)}</PermissionRoute> },
|
||||
{ path: '/activity', element: <PermissionRoute permission="audit:read">{withSuspense(<ActivityTimelinePage />)}</PermissionRoute> },
|
||||
{ path: '/wiki', element: <PermissionRoute permission="wiki:read">{withSuspense(<WikiPage />)}</PermissionRoute> },
|
||||
{ path: '/system-dashboard', element: withSuspense(<SystemDashboardPage />) },
|
||||
{ path: '/profile', element: withSuspense(<SettingsProfilePage />) },
|
||||
{ path: '*', element: <ErrorBoundary>{<PluginRouteRenderer />}</ErrorBoundary> },
|
||||
|
||||
Reference in New Issue
Block a user