@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,154 @@
|
|
|
1
|
+
// Export dialog: lets the user download all records matching the current
|
|
2
|
+
// list filters/sorting as CSV or JSON. Rendered inside a generic <Dialog>
|
|
3
|
+
// via useDialogs().open() — the parent passes resourceId + visible
|
|
4
|
+
// properties + the active ListQuery.
|
|
5
|
+
|
|
6
|
+
import * as React from 'react'
|
|
7
|
+
import {
|
|
8
|
+
Button,
|
|
9
|
+
DialogDescription,
|
|
10
|
+
DialogFooter,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
} from '@modern-admin/ui'
|
|
14
|
+
import { Download, FileJson, FileSpreadsheet, X } from 'lucide-react'
|
|
15
|
+
import { useAdminClient } from '../provider.js'
|
|
16
|
+
import { useI18n } from '../i18n.js'
|
|
17
|
+
import { useNotify } from '../notify.js'
|
|
18
|
+
import {
|
|
19
|
+
downloadText,
|
|
20
|
+
exportFilename,
|
|
21
|
+
fetchAllRecords,
|
|
22
|
+
recordsToCsv,
|
|
23
|
+
recordsToJson,
|
|
24
|
+
type ExportFormat,
|
|
25
|
+
} from '../export.js'
|
|
26
|
+
import type { ListQuery, PropertyJSON } from '../types.js'
|
|
27
|
+
|
|
28
|
+
export interface ExportDialogProps {
|
|
29
|
+
resourceId: string
|
|
30
|
+
resourceLabel: string
|
|
31
|
+
properties: PropertyJSON[]
|
|
32
|
+
query: ListQuery | undefined
|
|
33
|
+
onClose(): void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function ExportDialog({
|
|
37
|
+
resourceId,
|
|
38
|
+
resourceLabel,
|
|
39
|
+
properties,
|
|
40
|
+
query,
|
|
41
|
+
onClose,
|
|
42
|
+
}: ExportDialogProps): React.ReactElement {
|
|
43
|
+
const client = useAdminClient()
|
|
44
|
+
const { t } = useI18n()
|
|
45
|
+
const notify = useNotify()
|
|
46
|
+
const [busy, setBusy] = React.useState<ExportFormat | null>(null)
|
|
47
|
+
const [progress, setProgress] = React.useState<{ loaded: number; total: number } | null>(null)
|
|
48
|
+
const abortRef = React.useRef<AbortController | null>(null)
|
|
49
|
+
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
return () => abortRef.current?.abort()
|
|
52
|
+
}, [])
|
|
53
|
+
|
|
54
|
+
const run = async (format: ExportFormat): Promise<void> => {
|
|
55
|
+
if (busy) return
|
|
56
|
+
setBusy(format)
|
|
57
|
+
setProgress({ loaded: 0, total: 0 })
|
|
58
|
+
const ctrl = new AbortController()
|
|
59
|
+
abortRef.current = ctrl
|
|
60
|
+
try {
|
|
61
|
+
const records = await fetchAllRecords(client, resourceId, query, {
|
|
62
|
+
signal: ctrl.signal,
|
|
63
|
+
onProgress: (loaded, total) => setProgress({ loaded, total }),
|
|
64
|
+
})
|
|
65
|
+
const body =
|
|
66
|
+
format === 'csv'
|
|
67
|
+
? recordsToCsv(records, { properties, query })
|
|
68
|
+
: recordsToJson(records, { properties, query })
|
|
69
|
+
const mime = format === 'csv' ? 'text/csv' : 'application/json'
|
|
70
|
+
downloadText(exportFilename(resourceId, format), mime, body)
|
|
71
|
+
notify.success({ key: 'export:exported', params: { count: records.length } })
|
|
72
|
+
onClose()
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if ((err as { name?: string }).name === 'AbortError') return
|
|
75
|
+
notify.error(
|
|
76
|
+
{ key: 'export:exportFailed' },
|
|
77
|
+
{ description: err instanceof Error ? err.message : String(err) },
|
|
78
|
+
)
|
|
79
|
+
} finally {
|
|
80
|
+
setBusy(null)
|
|
81
|
+
setProgress(null)
|
|
82
|
+
abortRef.current = null
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<>
|
|
88
|
+
<DialogHeader>
|
|
89
|
+
<DialogTitle>{t('export:title')}</DialogTitle>
|
|
90
|
+
<DialogDescription>
|
|
91
|
+
{t('export:description', { resource: resourceLabel })}
|
|
92
|
+
</DialogDescription>
|
|
93
|
+
</DialogHeader>
|
|
94
|
+
|
|
95
|
+
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
96
|
+
<Button
|
|
97
|
+
type="button"
|
|
98
|
+
variant="outline"
|
|
99
|
+
className="h-auto justify-start gap-3 py-3"
|
|
100
|
+
disabled={!!busy}
|
|
101
|
+
onClick={() => run('csv')}
|
|
102
|
+
>
|
|
103
|
+
<FileSpreadsheet className="size-5 shrink-0" />
|
|
104
|
+
<div className="min-w-0 flex flex-col items-start text-left overflow-hidden">
|
|
105
|
+
<span className="font-medium">CSV</span>
|
|
106
|
+
<span className="text-xs text-muted-foreground truncate w-full">
|
|
107
|
+
{t('export:csvHint')}
|
|
108
|
+
</span>
|
|
109
|
+
</div>
|
|
110
|
+
</Button>
|
|
111
|
+
<Button
|
|
112
|
+
type="button"
|
|
113
|
+
variant="outline"
|
|
114
|
+
className="h-auto justify-start gap-3 py-3"
|
|
115
|
+
disabled={!!busy}
|
|
116
|
+
onClick={() => run('json')}
|
|
117
|
+
>
|
|
118
|
+
<FileJson className="size-5 shrink-0" />
|
|
119
|
+
<div className="min-w-0 flex flex-col items-start text-left overflow-hidden">
|
|
120
|
+
<span className="font-medium">JSON</span>
|
|
121
|
+
<span className="text-xs text-muted-foreground truncate w-full">
|
|
122
|
+
{t('export:jsonHint')}
|
|
123
|
+
</span>
|
|
124
|
+
</div>
|
|
125
|
+
</Button>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{busy && (
|
|
129
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
130
|
+
<Download className="size-4 animate-pulse" />
|
|
131
|
+
<span>
|
|
132
|
+
{progress && progress.total > 0
|
|
133
|
+
? t('export:progress', { loaded: progress.loaded, total: progress.total })
|
|
134
|
+
: t('export:downloading')}
|
|
135
|
+
</span>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
<DialogFooter>
|
|
140
|
+
<Button
|
|
141
|
+
type="button"
|
|
142
|
+
variant="ghost"
|
|
143
|
+
onClick={() => {
|
|
144
|
+
abortRef.current?.abort()
|
|
145
|
+
onClose()
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
<X className="size-4" />
|
|
149
|
+
{busy ? t('common:cancel') : t('common:close')}
|
|
150
|
+
</Button>
|
|
151
|
+
</DialogFooter>
|
|
152
|
+
</>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { Plus, Database, BarChart2, FolderPlus, Pencil, Trash2 } from 'lucide-react'
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
CardHeader,
|
|
8
|
+
CardTitle,
|
|
9
|
+
Empty,
|
|
10
|
+
EmptyDescription,
|
|
11
|
+
EmptyHeader,
|
|
12
|
+
EmptyMedia,
|
|
13
|
+
EmptyTitle,
|
|
14
|
+
Tabs,
|
|
15
|
+
TabsList,
|
|
16
|
+
TabsTrigger,
|
|
17
|
+
} from '@modern-admin/ui'
|
|
18
|
+
import { useResources, useCurrentUser } from '../hooks.js'
|
|
19
|
+
import { useI18n } from '../i18n.js'
|
|
20
|
+
import { Link } from '../router.js'
|
|
21
|
+
import { useDashboardCharts, ServerDashboardStore } from '../use-dashboard-charts.js'
|
|
22
|
+
import { useAdminClient } from '../provider.js'
|
|
23
|
+
import { useDialogs } from '../dialogs.js'
|
|
24
|
+
import { ChartWidget } from '../components/chart-widget.js'
|
|
25
|
+
import { ChartBuilderDialog } from '../components/chart-builder-dialog.js'
|
|
26
|
+
import { GroupSettingsDialog } from '../components/group-settings-dialog.js'
|
|
27
|
+
import { MoveChartDialog } from '../components/move-chart-dialog.js'
|
|
28
|
+
import type { ChartDef, ChartGroup } from '@modern-admin/core'
|
|
29
|
+
|
|
30
|
+
export function HomePage(): React.ReactElement {
|
|
31
|
+
const { t } = useI18n()
|
|
32
|
+
const resources = useResources()
|
|
33
|
+
const { user } = useCurrentUser()
|
|
34
|
+
const adminClient = useAdminClient()
|
|
35
|
+
const dialogs = useDialogs()
|
|
36
|
+
// Use server-backed store so charts persist across devices/browsers.
|
|
37
|
+
// Falls back gracefully when configStore is not configured server-side.
|
|
38
|
+
const serverStore = React.useMemo(() => new ServerDashboardStore(adminClient), [adminClient])
|
|
39
|
+
const {
|
|
40
|
+
charts,
|
|
41
|
+
groups,
|
|
42
|
+
addChart,
|
|
43
|
+
updateChart,
|
|
44
|
+
removeChart,
|
|
45
|
+
addGroup,
|
|
46
|
+
updateGroup,
|
|
47
|
+
removeGroup,
|
|
48
|
+
} = useDashboardCharts({
|
|
49
|
+
userId: user?.id ?? null,
|
|
50
|
+
store: serverStore,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Only show resources explicitly visible in navigation (same rule as sidebar).
|
|
54
|
+
const navResources = React.useMemo(
|
|
55
|
+
() => resources.filter((r) => r.navigation !== null),
|
|
56
|
+
[resources],
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const [building, setBuilding] = React.useState(false)
|
|
60
|
+
const [editingId, setEditingId] = React.useState<string | null>(null)
|
|
61
|
+
// null = no dialog; 'new' = create; ChartGroup = edit-existing.
|
|
62
|
+
const [groupDialog, setGroupDialog] = React.useState<ChartGroup | 'new' | null>(null)
|
|
63
|
+
const [activeGroupId, setActiveGroupId] = React.useState<string | null>(null)
|
|
64
|
+
// Chart being moved — null when dialog is closed.
|
|
65
|
+
const [movingChart, setMovingChart] = React.useState<ChartDef | null>(null)
|
|
66
|
+
|
|
67
|
+
// Keep `activeGroupId` valid when groups change (group removed, list reloaded
|
|
68
|
+
// from store, etc.). When at least one group exists we always render charts
|
|
69
|
+
// bucketed by group, so we need a definite active id.
|
|
70
|
+
React.useEffect(() => {
|
|
71
|
+
if (groups.length === 0) {
|
|
72
|
+
setActiveGroupId(null)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
if (!activeGroupId || !groups.some((g) => g.id === activeGroupId)) {
|
|
76
|
+
setActiveGroupId(groups[0]!.id)
|
|
77
|
+
}
|
|
78
|
+
}, [groups, activeGroupId])
|
|
79
|
+
|
|
80
|
+
const editingChart = editingId ? charts.find((c) => c.id === editingId) : undefined
|
|
81
|
+
|
|
82
|
+
// Build the chart bucket for the active group: charts whose `groupId`
|
|
83
|
+
// matches OR (fallback) ungrouped charts when the active group is the
|
|
84
|
+
// first one. Sorted by `order` then `createdAt`.
|
|
85
|
+
const firstGroupId = groups[0]?.id ?? null
|
|
86
|
+
const visibleCharts = React.useMemo(() => {
|
|
87
|
+
if (groups.length === 0) return [...charts].sort(byChartOrder)
|
|
88
|
+
if (!activeGroupId) return []
|
|
89
|
+
const filtered = charts.filter((c) => {
|
|
90
|
+
if (c.groupId) return c.groupId === activeGroupId
|
|
91
|
+
// Ungrouped charts default into the first group so they remain visible.
|
|
92
|
+
return activeGroupId === firstGroupId
|
|
93
|
+
})
|
|
94
|
+
return filtered.sort(byChartOrder)
|
|
95
|
+
}, [charts, groups.length, activeGroupId, firstGroupId])
|
|
96
|
+
|
|
97
|
+
const activeGroup = activeGroupId ? groups.find((g) => g.id === activeGroupId) ?? null : null
|
|
98
|
+
|
|
99
|
+
const handleDeleteChart = async (chart: ChartDef): Promise<void> => {
|
|
100
|
+
const ok = await dialogs.confirm({
|
|
101
|
+
title: t('chart:deleteChartConfirm'),
|
|
102
|
+
description: t('chart:deleteChartConfirmHint'),
|
|
103
|
+
confirmLabel: t('common:delete'),
|
|
104
|
+
destructive: true,
|
|
105
|
+
})
|
|
106
|
+
if (ok) removeChart(chart.id)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const handleDeleteGroup = async (group: ChartGroup): Promise<void> => {
|
|
110
|
+
const count = charts.filter((c) => {
|
|
111
|
+
if (c.groupId) return c.groupId === group.id
|
|
112
|
+
// Ungrouped charts also belong to the first group at display time.
|
|
113
|
+
return group.id === firstGroupId
|
|
114
|
+
}).length
|
|
115
|
+
const ok = await dialogs.confirm({
|
|
116
|
+
title: t('chart:deleteGroupConfirm'),
|
|
117
|
+
description: t('chart:deleteGroupConfirmHint').replace('{count}', String(count)),
|
|
118
|
+
confirmLabel: t('common:delete'),
|
|
119
|
+
destructive: true,
|
|
120
|
+
})
|
|
121
|
+
if (ok) removeGroup(group.id)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div className="space-y-3 sm:space-y-6">
|
|
126
|
+
{/* ── Dashboard charts ──────────────────────────────────────── */}
|
|
127
|
+
<Card>
|
|
128
|
+
<CardHeader className="flex flex-row items-center justify-between p-3 pb-2 space-y-0 gap-2 sm:p-6 sm:pb-2">
|
|
129
|
+
<CardTitle>{t('chart:dashboard')}</CardTitle>
|
|
130
|
+
<div className="flex items-center gap-2">
|
|
131
|
+
<Button
|
|
132
|
+
size="sm"
|
|
133
|
+
variant="outline"
|
|
134
|
+
onClick={() => setGroupDialog('new')}
|
|
135
|
+
>
|
|
136
|
+
<FolderPlus className="size-4" />
|
|
137
|
+
<span className="hidden sm:inline ml-1.5">{t('chart:addGroup')}</span>
|
|
138
|
+
</Button>
|
|
139
|
+
<Button size="sm" onClick={() => setBuilding(true)} disabled={navResources.length === 0}>
|
|
140
|
+
<Plus className="size-4" />
|
|
141
|
+
<span className="hidden sm:inline ml-1.5">{t('chart:addChart')}</span>
|
|
142
|
+
</Button>
|
|
143
|
+
</div>
|
|
144
|
+
</CardHeader>
|
|
145
|
+
<CardContent className="p-3 pt-0 sm:p-6 sm:pt-0">
|
|
146
|
+
{groups.length > 0 && activeGroupId && (
|
|
147
|
+
<Tabs
|
|
148
|
+
value={activeGroupId}
|
|
149
|
+
onValueChange={(v) => setActiveGroupId(v)}
|
|
150
|
+
className="mb-4"
|
|
151
|
+
>
|
|
152
|
+
<div className="flex items-end justify-between gap-2">
|
|
153
|
+
<TabsList className="flex-1">
|
|
154
|
+
{groups.map((g) => (
|
|
155
|
+
<TabsTrigger key={g.id} value={g.id}>
|
|
156
|
+
{g.name}
|
|
157
|
+
</TabsTrigger>
|
|
158
|
+
))}
|
|
159
|
+
</TabsList>
|
|
160
|
+
{activeGroup && (
|
|
161
|
+
<div className="mb-1 flex shrink-0 items-center gap-1">
|
|
162
|
+
<Button
|
|
163
|
+
size="sm"
|
|
164
|
+
variant="ghost"
|
|
165
|
+
onClick={() => setGroupDialog(activeGroup)}
|
|
166
|
+
aria-label={t('chart:editGroup')}
|
|
167
|
+
>
|
|
168
|
+
<Pencil className="size-3.5" />
|
|
169
|
+
<span className="hidden sm:inline ml-1.5">{t('chart:editGroup')}</span>
|
|
170
|
+
</Button>
|
|
171
|
+
<Button
|
|
172
|
+
size="sm"
|
|
173
|
+
variant="ghost"
|
|
174
|
+
onClick={() => void handleDeleteGroup(activeGroup)}
|
|
175
|
+
aria-label={t('chart:deleteGroup')}
|
|
176
|
+
>
|
|
177
|
+
<Trash2 className="size-3.5" />
|
|
178
|
+
<span className="hidden sm:inline ml-1.5">{t('chart:deleteGroup')}</span>
|
|
179
|
+
</Button>
|
|
180
|
+
</div>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
</Tabs>
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
{visibleCharts.length === 0 ? (
|
|
187
|
+
<Empty className="border-0 py-4">
|
|
188
|
+
<EmptyHeader>
|
|
189
|
+
<EmptyMedia>
|
|
190
|
+
<BarChart2 />
|
|
191
|
+
</EmptyMedia>
|
|
192
|
+
<EmptyTitle>{t('chart:noCharts')}</EmptyTitle>
|
|
193
|
+
<EmptyDescription>{t('chart:noChartsHint')}</EmptyDescription>
|
|
194
|
+
</EmptyHeader>
|
|
195
|
+
</Empty>
|
|
196
|
+
) : (
|
|
197
|
+
<div className="grid gap-3 grid-cols-1 sm:gap-4 md:grid-cols-2">
|
|
198
|
+
{visibleCharts.map((c) => (
|
|
199
|
+
<div
|
|
200
|
+
key={c.id}
|
|
201
|
+
className={c.width === 'full' ? 'md:col-span-2' : undefined}
|
|
202
|
+
>
|
|
203
|
+
<ChartWidget
|
|
204
|
+
config={c}
|
|
205
|
+
onEdit={() => setEditingId(c.id)}
|
|
206
|
+
onDelete={() => void handleDeleteChart(c)}
|
|
207
|
+
onMove={() => setMovingChart(c)}
|
|
208
|
+
onUpdate={(input) => updateChart(c.id, input)}
|
|
209
|
+
/>
|
|
210
|
+
</div>
|
|
211
|
+
))}
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
</CardContent>
|
|
215
|
+
</Card>
|
|
216
|
+
|
|
217
|
+
{/* ── Resources list ────────────────────────────────────────── */}
|
|
218
|
+
<Card>
|
|
219
|
+
<CardHeader>
|
|
220
|
+
<CardTitle>{t('common:resources')}</CardTitle>
|
|
221
|
+
</CardHeader>
|
|
222
|
+
<CardContent>
|
|
223
|
+
{navResources.length === 0 ? (
|
|
224
|
+
<Empty className="border-0">
|
|
225
|
+
<EmptyHeader>
|
|
226
|
+
<EmptyMedia>
|
|
227
|
+
<Database />
|
|
228
|
+
</EmptyMedia>
|
|
229
|
+
<EmptyTitle>{t('common:noRecords')}</EmptyTitle>
|
|
230
|
+
<EmptyDescription>
|
|
231
|
+
{t('common:noRecordsHint', { resource: 'resource' })}
|
|
232
|
+
</EmptyDescription>
|
|
233
|
+
</EmptyHeader>
|
|
234
|
+
</Empty>
|
|
235
|
+
) : (
|
|
236
|
+
<ul className="grid gap-2 sm:grid-cols-2 md:grid-cols-3">
|
|
237
|
+
{navResources.map((r) => (
|
|
238
|
+
<li key={r.id}>
|
|
239
|
+
<Link
|
|
240
|
+
to={{ name: 'list', resourceId: r.id }}
|
|
241
|
+
className="block rounded-md border border-border bg-card p-4 hover:border-primary/40 hover:shadow-sm transition-shadow"
|
|
242
|
+
>
|
|
243
|
+
<div className="font-semibold">{r.name}</div>
|
|
244
|
+
{r.name !== r.id && (
|
|
245
|
+
<div className="text-xs text-muted-foreground">{r.id}</div>
|
|
246
|
+
)}
|
|
247
|
+
</Link>
|
|
248
|
+
</li>
|
|
249
|
+
))}
|
|
250
|
+
</ul>
|
|
251
|
+
)}
|
|
252
|
+
</CardContent>
|
|
253
|
+
</Card>
|
|
254
|
+
|
|
255
|
+
{/* ── Chart builder dialog ──────────────────────────────────── */}
|
|
256
|
+
{building && (
|
|
257
|
+
<ChartBuilderDialog
|
|
258
|
+
onSave={(input) => {
|
|
259
|
+
// New charts inherit the active group so the user sees them in
|
|
260
|
+
// the tab they were on. The hook also auto-falls back to the
|
|
261
|
+
// first group if `groupId` is undefined.
|
|
262
|
+
addChart(activeGroupId ? { ...input, groupId: activeGroupId } : input)
|
|
263
|
+
setBuilding(false)
|
|
264
|
+
}}
|
|
265
|
+
onClose={() => setBuilding(false)}
|
|
266
|
+
/>
|
|
267
|
+
)}
|
|
268
|
+
{editingChart && (
|
|
269
|
+
<ChartBuilderDialog
|
|
270
|
+
initial={editingChart}
|
|
271
|
+
onSave={(input) => { updateChart(editingChart.id, input); setEditingId(null) }}
|
|
272
|
+
onClose={() => setEditingId(null)}
|
|
273
|
+
/>
|
|
274
|
+
)}
|
|
275
|
+
|
|
276
|
+
{/* ── Move chart dialog ─────────────────────────────────────── */}
|
|
277
|
+
{movingChart && (
|
|
278
|
+
<MoveChartDialog
|
|
279
|
+
groups={groups}
|
|
280
|
+
initialGroupId={movingChart.groupId}
|
|
281
|
+
initialOrder={movingChart.order}
|
|
282
|
+
onSave={({ groupId, order }) => {
|
|
283
|
+
updateChart(movingChart.id, { ...movingChart, groupId, order })
|
|
284
|
+
setMovingChart(null)
|
|
285
|
+
}}
|
|
286
|
+
onClose={() => setMovingChart(null)}
|
|
287
|
+
onCreateGroup={() => {
|
|
288
|
+
setMovingChart(null)
|
|
289
|
+
setGroupDialog('new')
|
|
290
|
+
}}
|
|
291
|
+
/>
|
|
292
|
+
)}
|
|
293
|
+
|
|
294
|
+
{/* ── Group settings dialog ─────────────────────────────────── */}
|
|
295
|
+
{groupDialog && (
|
|
296
|
+
<GroupSettingsDialog
|
|
297
|
+
initial={groupDialog === 'new' ? undefined : groupDialog}
|
|
298
|
+
onSave={(input) => {
|
|
299
|
+
if (groupDialog === 'new') {
|
|
300
|
+
const newId = addGroup(input)
|
|
301
|
+
setActiveGroupId(newId)
|
|
302
|
+
} else {
|
|
303
|
+
updateGroup(groupDialog.id, input)
|
|
304
|
+
}
|
|
305
|
+
setGroupDialog(null)
|
|
306
|
+
}}
|
|
307
|
+
onClose={() => setGroupDialog(null)}
|
|
308
|
+
/>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Sort charts within a group: by `order` asc, ties broken by `createdAt`. */
|
|
315
|
+
function byChartOrder(a: ChartDef, b: ChartDef): number {
|
|
316
|
+
if (a.order !== b.order) return a.order - b.order
|
|
317
|
+
return a.createdAt.localeCompare(b.createdAt)
|
|
318
|
+
}
|