@modern-admin/react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/action-guard.d.ts +13 -0
- package/dist/action-guard.d.ts.map +1 -0
- package/dist/action-guard.js +15 -0
- package/dist/action-guard.js.map +1 -0
- package/dist/action-menu.d.ts +17 -0
- package/dist/action-menu.d.ts.map +1 -0
- package/dist/action-menu.jsx +80 -0
- package/dist/action-menu.jsx.map +1 -0
- package/dist/admin-app.d.ts +23 -0
- package/dist/admin-app.d.ts.map +1 -0
- package/dist/admin-app.jsx +407 -0
- package/dist/admin-app.jsx.map +1 -0
- package/dist/admin-router.d.ts +29 -0
- package/dist/admin-router.d.ts.map +1 -0
- package/dist/admin-router.jsx +215 -0
- package/dist/admin-router.jsx.map +1 -0
- package/dist/breadcrumbs.d.ts +17 -0
- package/dist/breadcrumbs.d.ts.map +1 -0
- package/dist/breadcrumbs.jsx +40 -0
- package/dist/breadcrumbs.jsx.map +1 -0
- package/dist/client.d.ts +526 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +582 -0
- package/dist/client.js.map +1 -0
- package/dist/component-loader.d.ts +10 -0
- package/dist/component-loader.d.ts.map +1 -0
- package/dist/component-loader.js +23 -0
- package/dist/component-loader.js.map +1 -0
- package/dist/components/ai-assistant-widget.d.ts +3 -0
- package/dist/components/ai-assistant-widget.d.ts.map +1 -0
- package/dist/components/ai-assistant-widget.jsx +390 -0
- package/dist/components/ai-assistant-widget.jsx.map +1 -0
- package/dist/components/ai-fill-dialog.d.ts +9 -0
- package/dist/components/ai-fill-dialog.d.ts.map +1 -0
- package/dist/components/ai-fill-dialog.jsx +105 -0
- package/dist/components/ai-fill-dialog.jsx.map +1 -0
- package/dist/components/chart-builder-dialog.d.ts +10 -0
- package/dist/components/chart-builder-dialog.d.ts.map +1 -0
- package/dist/components/chart-builder-dialog.jsx +433 -0
- package/dist/components/chart-builder-dialog.jsx.map +1 -0
- package/dist/components/chart-widget.d.ts +12 -0
- package/dist/components/chart-widget.d.ts.map +1 -0
- package/dist/components/chart-widget.jsx +365 -0
- package/dist/components/chart-widget.jsx.map +1 -0
- package/dist/components/global-search-dialog.d.ts +7 -0
- package/dist/components/global-search-dialog.d.ts.map +1 -0
- package/dist/components/global-search-dialog.jsx +187 -0
- package/dist/components/global-search-dialog.jsx.map +1 -0
- package/dist/components/group-settings-dialog.d.ts +13 -0
- package/dist/components/group-settings-dialog.d.ts.map +1 -0
- package/dist/components/group-settings-dialog.jsx +53 -0
- package/dist/components/group-settings-dialog.jsx.map +1 -0
- package/dist/components/move-chart-dialog.d.ts +18 -0
- package/dist/components/move-chart-dialog.d.ts.map +1 -0
- package/dist/components/move-chart-dialog.jsx +68 -0
- package/dist/components/move-chart-dialog.jsx.map +1 -0
- package/dist/components/reference-multi-table-dialog.d.ts +12 -0
- package/dist/components/reference-multi-table-dialog.d.ts.map +1 -0
- package/dist/components/reference-multi-table-dialog.jsx +126 -0
- package/dist/components/reference-multi-table-dialog.jsx.map +1 -0
- package/dist/components/related-records-tabs.d.ts +8 -0
- package/dist/components/related-records-tabs.d.ts.map +1 -0
- package/dist/components/related-records-tabs.jsx +75 -0
- package/dist/components/related-records-tabs.jsx.map +1 -0
- package/dist/components/revisions-button.d.ts +7 -0
- package/dist/components/revisions-button.d.ts.map +1 -0
- package/dist/components/revisions-button.jsx +152 -0
- package/dist/components/revisions-button.jsx.map +1 -0
- package/dist/components/wizard-form.d.ts +43 -0
- package/dist/components/wizard-form.d.ts.map +1 -0
- package/dist/components/wizard-form.jsx +136 -0
- package/dist/components/wizard-form.jsx.map +1 -0
- package/dist/dashboard/time-series.d.ts +20 -0
- package/dist/dashboard/time-series.d.ts.map +1 -0
- package/dist/dashboard/time-series.js +108 -0
- package/dist/dashboard/time-series.js.map +1 -0
- package/dist/dialogs.d.ts +35 -0
- package/dist/dialogs.d.ts.map +1 -0
- package/dist/dialogs.jsx +152 -0
- package/dist/dialogs.jsx.map +1 -0
- package/dist/export.d.ts +39 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/export.js +114 -0
- package/dist/export.js.map +1 -0
- package/dist/extension-registry.d.ts +122 -0
- package/dist/extension-registry.d.ts.map +1 -0
- package/dist/extension-registry.js +93 -0
- package/dist/extension-registry.js.map +1 -0
- package/dist/header-controls.d.ts +4 -0
- package/dist/header-controls.d.ts.map +1 -0
- package/dist/header-controls.jsx +70 -0
- package/dist/header-controls.jsx.map +1 -0
- package/dist/hooks.d.ts +104 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +374 -0
- package/dist/hooks.js.map +1 -0
- package/dist/hotkey-help.d.ts +3 -0
- package/dist/hotkey-help.d.ts.map +1 -0
- package/dist/hotkey-help.jsx +32 -0
- package/dist/hotkey-help.jsx.map +1 -0
- package/dist/hotkey-registry.d.ts +18 -0
- package/dist/hotkey-registry.d.ts.map +1 -0
- package/dist/hotkey-registry.jsx +34 -0
- package/dist/hotkey-registry.jsx.map +1 -0
- package/dist/i18n.d.ts +74 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.jsx +127 -0
- package/dist/i18n.jsx.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/notify.d.ts +41 -0
- package/dist/notify.d.ts.map +1 -0
- package/dist/notify.jsx +58 -0
- package/dist/notify.jsx.map +1 -0
- package/dist/pages/ai-assistant-settings-section.d.ts +3 -0
- package/dist/pages/ai-assistant-settings-section.d.ts.map +1 -0
- package/dist/pages/ai-assistant-settings-section.jsx +126 -0
- package/dist/pages/ai-assistant-settings-section.jsx.map +1 -0
- package/dist/pages/audit-log-page.d.ts +3 -0
- package/dist/pages/audit-log-page.d.ts.map +1 -0
- package/dist/pages/audit-log-page.jsx +354 -0
- package/dist/pages/audit-log-page.jsx.map +1 -0
- package/dist/pages/edit-page.d.ts +7 -0
- package/dist/pages/edit-page.d.ts.map +1 -0
- package/dist/pages/edit-page.jsx +614 -0
- package/dist/pages/edit-page.jsx.map +1 -0
- package/dist/pages/export-dialog.d.ts +11 -0
- package/dist/pages/export-dialog.d.ts.map +1 -0
- package/dist/pages/export-dialog.jsx +102 -0
- package/dist/pages/export-dialog.jsx.map +1 -0
- package/dist/pages/home-page.d.ts +3 -0
- package/dist/pages/home-page.d.ts.map +1 -0
- package/dist/pages/home-page.jsx +211 -0
- package/dist/pages/home-page.jsx.map +1 -0
- package/dist/pages/list-page.d.ts +42 -0
- package/dist/pages/list-page.d.ts.map +1 -0
- package/dist/pages/list-page.jsx +1596 -0
- package/dist/pages/list-page.jsx.map +1 -0
- package/dist/pages/login-page.d.ts +11 -0
- package/dist/pages/login-page.d.ts.map +1 -0
- package/dist/pages/login-page.jsx +157 -0
- package/dist/pages/login-page.jsx.map +1 -0
- package/dist/pages/settings-page.d.ts +5 -0
- package/dist/pages/settings-page.d.ts.map +1 -0
- package/dist/pages/settings-page.jsx +787 -0
- package/dist/pages/settings-page.jsx.map +1 -0
- package/dist/pages/settings-shared.d.ts +51 -0
- package/dist/pages/settings-shared.d.ts.map +1 -0
- package/dist/pages/settings-shared.jsx +66 -0
- package/dist/pages/settings-shared.jsx.map +1 -0
- package/dist/pages/show-page.d.ts +7 -0
- package/dist/pages/show-page.d.ts.map +1 -0
- package/dist/pages/show-page.jsx +147 -0
- package/dist/pages/show-page.jsx.map +1 -0
- package/dist/pages/wizard-create-page.d.ts +14 -0
- package/dist/pages/wizard-create-page.d.ts.map +1 -0
- package/dist/pages/wizard-create-page.jsx +106 -0
- package/dist/pages/wizard-create-page.jsx.map +1 -0
- package/dist/property-renderer.d.ts +8 -0
- package/dist/property-renderer.d.ts.map +1 -0
- package/dist/property-renderer.jsx +690 -0
- package/dist/property-renderer.jsx.map +1 -0
- package/dist/provider.d.ts +20 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.jsx +32 -0
- package/dist/provider.jsx.map +1 -0
- package/dist/realtime.d.ts +22 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +38 -0
- package/dist/realtime.js.map +1 -0
- package/dist/reference.d.ts +52 -0
- package/dist/reference.d.ts.map +1 -0
- package/dist/reference.jsx +224 -0
- package/dist/reference.jsx.map +1 -0
- package/dist/relations.d.ts +11 -0
- package/dist/relations.d.ts.map +1 -0
- package/dist/relations.js +36 -0
- package/dist/relations.js.map +1 -0
- package/dist/router.d.ts +82 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.jsx +187 -0
- package/dist/router.jsx.map +1 -0
- package/dist/show-when.d.ts +7 -0
- package/dist/show-when.d.ts.map +1 -0
- package/dist/show-when.js +77 -0
- package/dist/show-when.js.map +1 -0
- package/dist/types.d.ts +194 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +18 -0
- package/dist/types.js.map +1 -0
- package/dist/use-dashboard-charts.d.ts +93 -0
- package/dist/use-dashboard-charts.d.ts.map +1 -0
- package/dist/use-dashboard-charts.js +263 -0
- package/dist/use-dashboard-charts.js.map +1 -0
- package/dist/use-hotkey.d.ts +17 -0
- package/dist/use-hotkey.d.ts.map +1 -0
- package/dist/use-hotkey.js +103 -0
- package/dist/use-hotkey.js.map +1 -0
- package/dist/user-directory.d.ts +18 -0
- package/dist/user-directory.d.ts.map +1 -0
- package/dist/user-directory.js +51 -0
- package/dist/user-directory.js.map +1 -0
- package/dist/validation.d.ts +22 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +338 -0
- package/dist/validation.js.map +1 -0
- package/package.json +59 -0
- package/src/action-guard.ts +20 -0
- package/src/action-menu.tsx +161 -0
- package/src/admin-app.tsx +630 -0
- package/src/admin-router.tsx +273 -0
- package/src/breadcrumbs.tsx +75 -0
- package/src/client.ts +1093 -0
- package/src/component-loader.ts +33 -0
- package/src/components/ai-assistant-widget.tsx +565 -0
- package/src/components/ai-fill-dialog.tsx +143 -0
- package/src/components/chart-builder-dialog.tsx +618 -0
- package/src/components/chart-widget.tsx +654 -0
- package/src/components/global-search-dialog.tsx +272 -0
- package/src/components/group-settings-dialog.tsx +93 -0
- package/src/components/move-chart-dialog.tsx +130 -0
- package/src/components/reference-multi-table-dialog.tsx +196 -0
- package/src/components/related-records-tabs.tsx +130 -0
- package/src/components/revisions-button.tsx +237 -0
- package/src/components/wizard-form.tsx +302 -0
- package/src/dashboard/time-series.ts +125 -0
- package/src/dialogs.tsx +265 -0
- package/src/export.ts +140 -0
- package/src/extension-registry.ts +195 -0
- package/src/header-controls.tsx +125 -0
- package/src/hooks.ts +509 -0
- package/src/hotkey-help.tsx +56 -0
- package/src/hotkey-registry.tsx +60 -0
- package/src/i18n.tsx +267 -0
- package/src/index.ts +192 -0
- package/src/notify.tsx +94 -0
- package/src/pages/ai-assistant-settings-section.tsx +167 -0
- package/src/pages/audit-log-page.tsx +580 -0
- package/src/pages/edit-page.tsx +743 -0
- package/src/pages/export-dialog.tsx +154 -0
- package/src/pages/home-page.tsx +318 -0
- package/src/pages/list-page.tsx +2645 -0
- package/src/pages/login-page.tsx +242 -0
- package/src/pages/settings-page.tsx +1143 -0
- package/src/pages/settings-shared.tsx +134 -0
- package/src/pages/show-page.tsx +223 -0
- package/src/pages/wizard-create-page.tsx +164 -0
- package/src/property-renderer.tsx +1143 -0
- package/src/provider.tsx +70 -0
- package/src/realtime.ts +55 -0
- package/src/reference.tsx +386 -0
- package/src/relations.ts +55 -0
- package/src/router.tsx +211 -0
- package/src/show-when.ts +76 -0
- package/src/types.ts +198 -0
- package/src/use-dashboard-charts.ts +362 -0
- package/src/use-hotkey.ts +128 -0
- package/src/user-directory.ts +56 -0
- package/src/validation.ts +361 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// Code-based route tree wired up to @tanstack/react-router. Imported by
|
|
2
|
+
// `admin-app.tsx` and renders the entire authenticated shell as the root
|
|
3
|
+
// route's component. Each leaf route is a thin wrapper that pulls typed
|
|
4
|
+
// `params` and renders the existing page component (page props remain a
|
|
5
|
+
// public API surface — no changes to ResourceListPage/ResourceShowPage/etc.).
|
|
6
|
+
//
|
|
7
|
+
// History: browser (`createBrowserHistory`) — clean path-based URLs.
|
|
8
|
+
// Requires an SPA fallback rule on the server (e.g. `try_files ... index.html`
|
|
9
|
+
// in nginx, historyApiFallback in Vite). See `router.tsx` and `docs/frontend.md`.
|
|
10
|
+
//
|
|
11
|
+
// Basepath: `AdminRouterProvider` accepts a `basepath` prop (e.g. `/admin`)
|
|
12
|
+
// injected automatically from `window.__MODERN_ADMIN__.basePath`. The router
|
|
13
|
+
// is created with that basepath so TSR strips it from URLs before route
|
|
14
|
+
// matching. `BasepathContext` exposes it to `Link` and `useNavigate` so they
|
|
15
|
+
// can prepend it when pushing to browser history.
|
|
16
|
+
//
|
|
17
|
+
// Search params: TSR's default JSON-style parser would mangle our existing
|
|
18
|
+
// `filters[<path>]=<value>` URL format. We make `parseSearch`/`stringifySearch`
|
|
19
|
+
// no-ops (TSR keeps `searchStr` raw); `useRoute()` re-parses `searchStr` into
|
|
20
|
+
// `ListQueryState` via `parseLocation` in `router.tsx`.
|
|
21
|
+
|
|
22
|
+
import * as React from 'react'
|
|
23
|
+
import {
|
|
24
|
+
createBrowserHistory,
|
|
25
|
+
createRootRouteWithContext,
|
|
26
|
+
createRoute,
|
|
27
|
+
createRouter,
|
|
28
|
+
Outlet,
|
|
29
|
+
RouterProvider,
|
|
30
|
+
} from '@tanstack/react-router'
|
|
31
|
+
import { BasepathContext } from './router.js'
|
|
32
|
+
import { getRouteExtension } from './extension-registry.js'
|
|
33
|
+
import { ResourceListPage } from './pages/list-page.js'
|
|
34
|
+
import { ResourceShowPage } from './pages/show-page.js'
|
|
35
|
+
import { ResourceEditPage } from './pages/edit-page.js'
|
|
36
|
+
import { ResourceWizardCreatePage } from './pages/wizard-create-page.js'
|
|
37
|
+
import { HomePage } from './pages/home-page.js'
|
|
38
|
+
import { SettingsPage } from './pages/settings-page.js'
|
|
39
|
+
import { AuditLogPage } from './pages/audit-log-page.js'
|
|
40
|
+
import { useI18n } from './i18n.js'
|
|
41
|
+
import type { WizardStep } from './components/wizard-form.js'
|
|
42
|
+
|
|
43
|
+
// ─── Route tree ───────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
interface RouterContext {
|
|
46
|
+
ShellLayout: React.ComponentType<{ children: React.ReactNode }>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const rootRoute = createRootRouteWithContext<RouterContext>()({
|
|
50
|
+
// The actual shell (sidebar + header + main) lives in `admin-app.tsx`.
|
|
51
|
+
// It's passed in via the router's context at provider time so we don't
|
|
52
|
+
// create a circular module dependency between admin-app and admin-router.
|
|
53
|
+
component: function RootRouteShell() {
|
|
54
|
+
const { ShellLayout } = rootRoute.useRouteContext()
|
|
55
|
+
return (
|
|
56
|
+
<ShellLayout>
|
|
57
|
+
<Outlet />
|
|
58
|
+
</ShellLayout>
|
|
59
|
+
)
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const homeRoute = createRoute({
|
|
64
|
+
getParentRoute: () => rootRoute,
|
|
65
|
+
path: '/',
|
|
66
|
+
component: function HomeRouteComponent() {
|
|
67
|
+
return <HomePage />
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const auditLogRoute = createRoute({
|
|
72
|
+
getParentRoute: () => rootRoute,
|
|
73
|
+
path: '/audit-log',
|
|
74
|
+
component: function AuditLogRouteComponent() {
|
|
75
|
+
return <AuditLogPage />
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const resourceListRoute = createRoute({
|
|
80
|
+
getParentRoute: () => rootRoute,
|
|
81
|
+
path: '/resources/$resourceId',
|
|
82
|
+
component: function ResourceListRouteComponent() {
|
|
83
|
+
const { resourceId } = resourceListRoute.useParams()
|
|
84
|
+
return <ResourceListPage resourceId={resourceId} />
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Products new-record route uses a 3-step WizardForm as a showcase.
|
|
89
|
+
// Step 3 has no `properties` list — it becomes the catch-all for every
|
|
90
|
+
// property not claimed by steps 1 or 2 (thumbnail, accentColor, gallery, tags).
|
|
91
|
+
function ProductsNewPage(): React.ReactElement {
|
|
92
|
+
const { t } = useI18n()
|
|
93
|
+
const steps: WizardStep[] = [
|
|
94
|
+
{ label: t('wizard:products.step1'), properties: ['name', 'sku', 'inStock'] },
|
|
95
|
+
{ label: t('wizard:products.step2'), properties: ['price', 'currencyCode', 'quantity'] },
|
|
96
|
+
{ label: t('wizard:products.step3') }, // catch-all: thumbnail, accentColor, gallery, tags
|
|
97
|
+
]
|
|
98
|
+
return <ResourceWizardCreatePage resourceId="products" steps={steps} />
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const resourceNewRoute = createRoute({
|
|
102
|
+
getParentRoute: () => rootRoute,
|
|
103
|
+
path: '/resources/$resourceId/new',
|
|
104
|
+
component: function ResourceNewRouteComponent() {
|
|
105
|
+
const { resourceId } = resourceNewRoute.useParams()
|
|
106
|
+
if (resourceId === 'products') return <ProductsNewPage />
|
|
107
|
+
return <ResourceEditPage resourceId={resourceId} />
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const resourceShowRoute = createRoute({
|
|
112
|
+
getParentRoute: () => rootRoute,
|
|
113
|
+
path: '/resources/$resourceId/$recordId',
|
|
114
|
+
component: function ResourceShowRouteComponent() {
|
|
115
|
+
const { resourceId, recordId } = resourceShowRoute.useParams()
|
|
116
|
+
return <ResourceShowPage resourceId={resourceId} recordId={recordId} />
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const resourceEditRoute = createRoute({
|
|
121
|
+
getParentRoute: () => rootRoute,
|
|
122
|
+
path: '/resources/$resourceId/$recordId/edit',
|
|
123
|
+
component: function ResourceEditRouteComponent() {
|
|
124
|
+
const { resourceId, recordId } = resourceEditRoute.useParams()
|
|
125
|
+
return <ResourceEditPage resourceId={resourceId} recordId={recordId} />
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const settingsIndexRoute = createRoute({
|
|
130
|
+
getParentRoute: () => rootRoute,
|
|
131
|
+
path: '/settings',
|
|
132
|
+
component: function SettingsIndexRouteComponent() {
|
|
133
|
+
return <SettingsPage />
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const settingsSectionRoute = createRoute({
|
|
138
|
+
getParentRoute: () => rootRoute,
|
|
139
|
+
path: '/settings/$section',
|
|
140
|
+
component: function SettingsSectionRouteComponent() {
|
|
141
|
+
const { section } = settingsSectionRoute.useParams()
|
|
142
|
+
return <SettingsPage section={section} />
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// Route for Pro extension pages registered via `registerExtensionRoute`.
|
|
147
|
+
// The component reads `key` from params and looks up the registered
|
|
148
|
+
// extension at render time — so it works even if extensions are registered
|
|
149
|
+
// after module init but before the user navigates to the route.
|
|
150
|
+
const extensionRoute = createRoute({
|
|
151
|
+
getParentRoute: () => rootRoute,
|
|
152
|
+
path: '/ext/$extKey',
|
|
153
|
+
component: function ExtensionRouteComponent() {
|
|
154
|
+
const { extKey } = extensionRoute.useParams()
|
|
155
|
+
const ext = getRouteExtension(extKey)
|
|
156
|
+
if (!ext) {
|
|
157
|
+
// Extension was registered for this key in the sidebar but no component
|
|
158
|
+
// was provided. Render a minimal placeholder rather than a blank screen.
|
|
159
|
+
return (
|
|
160
|
+
<div className="flex flex-col items-center justify-center py-24 text-muted-foreground">
|
|
161
|
+
<p className="text-sm">Extension "{extKey}" not found.</p>
|
|
162
|
+
</div>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
return <ext.component />
|
|
166
|
+
},
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const routeTree = rootRoute.addChildren([
|
|
170
|
+
homeRoute,
|
|
171
|
+
auditLogRoute,
|
|
172
|
+
extensionRoute,
|
|
173
|
+
resourceNewRoute,
|
|
174
|
+
resourceEditRoute,
|
|
175
|
+
resourceShowRoute,
|
|
176
|
+
resourceListRoute,
|
|
177
|
+
settingsSectionRoute,
|
|
178
|
+
settingsIndexRoute,
|
|
179
|
+
])
|
|
180
|
+
|
|
181
|
+
// ─── Router instance ──────────────────────────────────────────────────────────
|
|
182
|
+
//
|
|
183
|
+
// Search params: in TSR v1.169 `location.searchStr` is computed as
|
|
184
|
+
// `stringifySearch(parseSearch(rawUrlSearch))` — it's NOT the raw URL search
|
|
185
|
+
// string. Our list page reads `searchStr` to extract `filters[<path>]=<value>`
|
|
186
|
+
// pairs (see `parseLocation` in `router.tsx`), so the parse/stringify pair
|
|
187
|
+
// must be a flat-key identity round-trip rather than TSR's default
|
|
188
|
+
// JSON-encoded values (which would mangle the bracket notation) or no-ops
|
|
189
|
+
// (which would drop the search entirely — filters/sort never reach the API).
|
|
190
|
+
|
|
191
|
+
const flatParseSearch = (search: string): Record<string, string> => {
|
|
192
|
+
const str = search.startsWith('?') ? search.slice(1) : search
|
|
193
|
+
if (!str) return {}
|
|
194
|
+
const params = new URLSearchParams(str)
|
|
195
|
+
const out: Record<string, string> = {}
|
|
196
|
+
params.forEach((value, key) => {
|
|
197
|
+
out[key] = value
|
|
198
|
+
})
|
|
199
|
+
return out
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const flatStringifySearch = (search: Record<string, unknown>): string => {
|
|
203
|
+
if (!search) return ''
|
|
204
|
+
const params = new URLSearchParams()
|
|
205
|
+
for (const [k, v] of Object.entries(search)) {
|
|
206
|
+
if (v == null || v === '') continue
|
|
207
|
+
params.set(k, typeof v === 'string' ? v : String(v))
|
|
208
|
+
}
|
|
209
|
+
const s = params.toString()
|
|
210
|
+
return s ? `?${s}` : ''
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const noopRouterContext: RouterContext = {
|
|
214
|
+
// Replaced at provider mount time via the `context` prop on RouterProvider.
|
|
215
|
+
ShellLayout: () => null,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function makeRouter(basepath: string) {
|
|
219
|
+
return createRouter({
|
|
220
|
+
routeTree,
|
|
221
|
+
history: createBrowserHistory(),
|
|
222
|
+
basepath: basepath || '/',
|
|
223
|
+
parseSearch: flatParseSearch,
|
|
224
|
+
stringifySearch: flatStringifySearch,
|
|
225
|
+
context: noopRouterContext,
|
|
226
|
+
defaultPreload: false,
|
|
227
|
+
scrollRestoration: false,
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Type registration — gives typed `useParams()`/`useSearch()` and link
|
|
232
|
+
// validation across the package. The concrete router type uses the root
|
|
233
|
+
// router shape (basepath doesn't affect the generic types).
|
|
234
|
+
declare module '@tanstack/react-router' {
|
|
235
|
+
interface Register {
|
|
236
|
+
router: ReturnType<typeof makeRouter>
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ─── Provider ─────────────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
export interface AdminRouterProviderProps {
|
|
243
|
+
/** Component rendered as the authenticated shell. Receives `children` —
|
|
244
|
+
* must include an `<Outlet/>` slot (or a child element wrapping it) so
|
|
245
|
+
* routed page components can mount inside it. */
|
|
246
|
+
ShellLayout: React.ComponentType<{ children: React.ReactNode }>
|
|
247
|
+
/** URL prefix where the SPA is mounted (e.g. `/admin`). Defaults to `''`
|
|
248
|
+
* (root mount). Passed to TSR as the router `basepath` AND exposed to
|
|
249
|
+
* `Link`/`useNavigate` via `BasepathContext` so they push full paths. */
|
|
250
|
+
basepath?: string
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Mounts the admin's route tree. Must be rendered only after the user is
|
|
254
|
+
* authenticated — login flow happens upstream in `AdminApp`. */
|
|
255
|
+
export function AdminRouterProvider({
|
|
256
|
+
ShellLayout,
|
|
257
|
+
basepath = '',
|
|
258
|
+
}: AdminRouterProviderProps): React.ReactElement {
|
|
259
|
+
// Normalise: strip trailing slash, treat '/' as 'no basepath' (root mount).
|
|
260
|
+
const normalised = React.useMemo(() => {
|
|
261
|
+
if (!basepath || basepath === '/') return ''
|
|
262
|
+
return basepath.endsWith('/') ? basepath.slice(0, -1) : basepath
|
|
263
|
+
}, [basepath])
|
|
264
|
+
// Create router lazily per basepath. Runtime config is stable across the
|
|
265
|
+
// app's lifetime, so in practice this runs exactly once per mount.
|
|
266
|
+
const router = React.useMemo(() => makeRouter(normalised), [normalised])
|
|
267
|
+
const context = React.useMemo<RouterContext>(() => ({ ShellLayout }), [ShellLayout])
|
|
268
|
+
return (
|
|
269
|
+
<BasepathContext.Provider value={normalised}>
|
|
270
|
+
<RouterProvider router={router} context={context} />
|
|
271
|
+
</BasepathContext.Provider>
|
|
272
|
+
)
|
|
273
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Page breadcrumb helper. Each page builds its own crumb chain (home →
|
|
2
|
+
// resource → record → action) and hands it to <PageBreadcrumbs />, which
|
|
3
|
+
// renders the shadcn primitives with separators, hash-router-aware Links
|
|
4
|
+
// and a Home icon on the root crumb.
|
|
5
|
+
|
|
6
|
+
import * as React from 'react'
|
|
7
|
+
import {
|
|
8
|
+
Breadcrumb,
|
|
9
|
+
BreadcrumbItem,
|
|
10
|
+
BreadcrumbLink,
|
|
11
|
+
BreadcrumbList,
|
|
12
|
+
BreadcrumbPage,
|
|
13
|
+
BreadcrumbSeparator,
|
|
14
|
+
} from '@modern-admin/ui'
|
|
15
|
+
import { Home } from 'lucide-react'
|
|
16
|
+
import { Link } from './router.js'
|
|
17
|
+
import type { Route } from './router.js'
|
|
18
|
+
|
|
19
|
+
export interface BreadcrumbItemSpec {
|
|
20
|
+
label: string
|
|
21
|
+
/** When set, renders a hash-router link; otherwise the crumb is plain text. */
|
|
22
|
+
to?: Route
|
|
23
|
+
/** Optional leading icon. The home crumb gets one by default. */
|
|
24
|
+
icon?: React.ReactNode
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PageBreadcrumbsProps {
|
|
28
|
+
items: BreadcrumbItemSpec[]
|
|
29
|
+
className?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function PageBreadcrumbs({
|
|
33
|
+
items,
|
|
34
|
+
className,
|
|
35
|
+
}: PageBreadcrumbsProps): React.ReactElement | null {
|
|
36
|
+
if (items.length === 0) return null
|
|
37
|
+
return (
|
|
38
|
+
<Breadcrumb className={className}>
|
|
39
|
+
<BreadcrumbList>
|
|
40
|
+
{items.map((item, i) => {
|
|
41
|
+
const isLast = i === items.length - 1
|
|
42
|
+
const content = (
|
|
43
|
+
<span className="inline-flex items-center gap-1.5">
|
|
44
|
+
{item.icon}
|
|
45
|
+
<span className="truncate max-w-[8rem] sm:max-w-[16rem]">{item.label}</span>
|
|
46
|
+
</span>
|
|
47
|
+
)
|
|
48
|
+
return (
|
|
49
|
+
<React.Fragment key={`${i}-${item.label}`}>
|
|
50
|
+
<BreadcrumbItem>
|
|
51
|
+
{isLast || !item.to ? (
|
|
52
|
+
<BreadcrumbPage>{content}</BreadcrumbPage>
|
|
53
|
+
) : (
|
|
54
|
+
<BreadcrumbLink asChild>
|
|
55
|
+
<Link to={item.to}>{content}</Link>
|
|
56
|
+
</BreadcrumbLink>
|
|
57
|
+
)}
|
|
58
|
+
</BreadcrumbItem>
|
|
59
|
+
{!isLast && <BreadcrumbSeparator />}
|
|
60
|
+
</React.Fragment>
|
|
61
|
+
)
|
|
62
|
+
})}
|
|
63
|
+
</BreadcrumbList>
|
|
64
|
+
</Breadcrumb>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Default home crumb — used by every page; pass `homeLabel` to localize. */
|
|
69
|
+
export function homeCrumb(label: string): BreadcrumbItemSpec {
|
|
70
|
+
return {
|
|
71
|
+
label,
|
|
72
|
+
to: { name: 'home' },
|
|
73
|
+
icon: <Home className="size-3.5" />,
|
|
74
|
+
}
|
|
75
|
+
}
|