@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,134 @@
|
|
|
1
|
+
// Shared layout primitives for the Settings hub. Every section in
|
|
2
|
+
// `settings-page.tsx` (API keys, webhooks, AI assistant, ...) should use
|
|
3
|
+
// these so the look, spacing, and mobile behavior stays uniform.
|
|
4
|
+
|
|
5
|
+
import * as React from 'react'
|
|
6
|
+
import {
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
CardDescription,
|
|
10
|
+
CardHeader,
|
|
11
|
+
CardTitle,
|
|
12
|
+
Empty,
|
|
13
|
+
EmptyDescription,
|
|
14
|
+
EmptyHeader,
|
|
15
|
+
EmptyMedia,
|
|
16
|
+
EmptyTitle,
|
|
17
|
+
cn,
|
|
18
|
+
} from '@modern-admin/ui'
|
|
19
|
+
|
|
20
|
+
type IconComponent = React.ComponentType<{ className?: string }>
|
|
21
|
+
|
|
22
|
+
interface SettingsCardProps {
|
|
23
|
+
icon: IconComponent
|
|
24
|
+
title: React.ReactNode
|
|
25
|
+
description?: React.ReactNode
|
|
26
|
+
action?: React.ReactNode
|
|
27
|
+
/** Removes default padding/header so the card body can fill edge-to-edge. */
|
|
28
|
+
bodyClassName?: string
|
|
29
|
+
children: React.ReactNode
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Unified card wrapper for a settings section. Header collapses
|
|
34
|
+
* vertically on mobile and lays the action button to the right on `sm+`.
|
|
35
|
+
* `CardContent` gets `min-w-0` so tables/grids inside can shrink and
|
|
36
|
+
* scroll horizontally instead of forcing the parent grid wider.
|
|
37
|
+
*/
|
|
38
|
+
export function SettingsCard({
|
|
39
|
+
icon: Icon,
|
|
40
|
+
title,
|
|
41
|
+
description,
|
|
42
|
+
action,
|
|
43
|
+
bodyClassName,
|
|
44
|
+
children,
|
|
45
|
+
}: SettingsCardProps): React.ReactElement {
|
|
46
|
+
return (
|
|
47
|
+
<Card>
|
|
48
|
+
<CardHeader
|
|
49
|
+
className={cn(
|
|
50
|
+
'flex-col items-start gap-2',
|
|
51
|
+
action && 'sm:flex-row sm:items-center sm:justify-between',
|
|
52
|
+
)}
|
|
53
|
+
>
|
|
54
|
+
<div className="flex min-w-0 flex-col gap-1.5">
|
|
55
|
+
<CardTitle className="flex items-center gap-2">
|
|
56
|
+
<Icon className="size-5" />
|
|
57
|
+
{title}
|
|
58
|
+
</CardTitle>
|
|
59
|
+
{description ? <CardDescription>{description}</CardDescription> : null}
|
|
60
|
+
</div>
|
|
61
|
+
{action}
|
|
62
|
+
</CardHeader>
|
|
63
|
+
<CardContent className={cn('min-w-0', bodyClassName)}>{children}</CardContent>
|
|
64
|
+
</Card>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Horizontal-scroll wrapper for tables inside `SettingsCard`. On mobile
|
|
70
|
+
* the content extends to the card edges (`-mx-6`) so the user sees the
|
|
71
|
+
* left edge of the table flush with the card; from `sm+` the negative
|
|
72
|
+
* margin is removed.
|
|
73
|
+
*/
|
|
74
|
+
export function SettingsTableScroll({ children }: { children: React.ReactNode }): React.ReactElement {
|
|
75
|
+
return <div className="-mx-6 overflow-x-auto sm:mx-0">{children}</div>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface SettingsEmptyProps {
|
|
79
|
+
icon: IconComponent
|
|
80
|
+
title: React.ReactNode
|
|
81
|
+
description?: React.ReactNode
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function SettingsEmpty({ icon: Icon, title, description }: SettingsEmptyProps): React.ReactElement {
|
|
85
|
+
return (
|
|
86
|
+
<Empty className="border-0">
|
|
87
|
+
<EmptyHeader>
|
|
88
|
+
<EmptyMedia>
|
|
89
|
+
<Icon />
|
|
90
|
+
</EmptyMedia>
|
|
91
|
+
<EmptyTitle>{title}</EmptyTitle>
|
|
92
|
+
{description ? <EmptyDescription>{description}</EmptyDescription> : null}
|
|
93
|
+
</EmptyHeader>
|
|
94
|
+
</Empty>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface SettingsListStateProps {
|
|
99
|
+
isLoading: boolean
|
|
100
|
+
error: unknown
|
|
101
|
+
isEmpty: boolean
|
|
102
|
+
loadingLabel: React.ReactNode
|
|
103
|
+
empty: SettingsEmptyProps
|
|
104
|
+
children: React.ReactNode
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Renders one of: loading row, destructive error banner, empty state, or
|
|
109
|
+
* the actual list `children`. Keeps every section's "list with status"
|
|
110
|
+
* surface identical.
|
|
111
|
+
*/
|
|
112
|
+
export function SettingsListState({
|
|
113
|
+
isLoading,
|
|
114
|
+
error,
|
|
115
|
+
isEmpty,
|
|
116
|
+
loadingLabel,
|
|
117
|
+
empty,
|
|
118
|
+
children,
|
|
119
|
+
}: SettingsListStateProps): React.ReactElement {
|
|
120
|
+
if (isLoading) {
|
|
121
|
+
return <div className="py-8 text-center text-sm text-muted-foreground">{loadingLabel}</div>
|
|
122
|
+
}
|
|
123
|
+
if (error) {
|
|
124
|
+
return (
|
|
125
|
+
<div className="rounded-md border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive">
|
|
126
|
+
{error instanceof Error ? error.message : String(error)}
|
|
127
|
+
</div>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
if (isEmpty) {
|
|
131
|
+
return <SettingsEmpty {...empty} />
|
|
132
|
+
}
|
|
133
|
+
return <>{children}</>
|
|
134
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardHeader,
|
|
7
|
+
CardTitle,
|
|
8
|
+
Kbd,
|
|
9
|
+
Tooltip,
|
|
10
|
+
TooltipContent,
|
|
11
|
+
TooltipTrigger,
|
|
12
|
+
getModKeyLabel,
|
|
13
|
+
} from '@modern-admin/ui'
|
|
14
|
+
import { AlertCircle, Pencil, Trash2, Zap } from 'lucide-react'
|
|
15
|
+
import { useDeleteRecord, useFeatures, useInvokeRecordAction, useRecord, useResource } from '../hooks.js'
|
|
16
|
+
import { confirmGuard } from '../action-guard.js'
|
|
17
|
+
import { parseApiError } from '../client.js'
|
|
18
|
+
import { PropertyDisplay } from '../property-renderer.js'
|
|
19
|
+
import { Link, useNavigate } from '../router.js'
|
|
20
|
+
import { useI18n } from '../i18n.js'
|
|
21
|
+
import { useHotkey } from '../use-hotkey.js'
|
|
22
|
+
import { PageBreadcrumbs, homeCrumb } from '../breadcrumbs.js'
|
|
23
|
+
import { RelatedRecordsTabs } from '../components/related-records-tabs.js'
|
|
24
|
+
import { useDialogs } from '../dialogs.js'
|
|
25
|
+
import { useNotify } from '../notify.js'
|
|
26
|
+
import { ActionMenu } from '../action-menu.js'
|
|
27
|
+
import { RevisionsButton } from '../components/revisions-button.js'
|
|
28
|
+
import { visibleRecordProperties } from '../relations.js'
|
|
29
|
+
|
|
30
|
+
function PageError({
|
|
31
|
+
error,
|
|
32
|
+
t,
|
|
33
|
+
}: {
|
|
34
|
+
error: unknown
|
|
35
|
+
t: (key: string) => string
|
|
36
|
+
}): React.ReactElement {
|
|
37
|
+
const { status, message } = parseApiError(error)
|
|
38
|
+
const title =
|
|
39
|
+
status === 404
|
|
40
|
+
? t('errors:notFound')
|
|
41
|
+
: status === 403
|
|
42
|
+
? t('errors:forbidden')
|
|
43
|
+
: t('errors:server')
|
|
44
|
+
return (
|
|
45
|
+
<div className="flex items-start gap-3 rounded-lg border border-destructive/50 bg-destructive/10 p-4 dark:bg-destructive/15">
|
|
46
|
+
<AlertCircle className="mt-0.5 size-5 shrink-0 text-destructive" />
|
|
47
|
+
<div className="space-y-1 text-sm">
|
|
48
|
+
<p className="font-semibold text-destructive">{title}</p>
|
|
49
|
+
<p className="text-destructive/90">{message}</p>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ResourceShowPageProps {
|
|
56
|
+
resourceId: string
|
|
57
|
+
recordId: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function ResourceShowPage({
|
|
61
|
+
resourceId,
|
|
62
|
+
recordId,
|
|
63
|
+
}: ResourceShowPageProps): React.ReactElement {
|
|
64
|
+
const resource = useResource(resourceId)
|
|
65
|
+
const record = useRecord(resourceId, recordId)
|
|
66
|
+
const remove = useDeleteRecord(resourceId)
|
|
67
|
+
const invokeRecord = useInvokeRecordAction(resourceId)
|
|
68
|
+
const features = useFeatures()
|
|
69
|
+
const { t } = useI18n()
|
|
70
|
+
const navigate = useNavigate()
|
|
71
|
+
const dialogs = useDialogs()
|
|
72
|
+
const notify = useNotify()
|
|
73
|
+
|
|
74
|
+
const customRecordActions = (resource?.actions ?? []).filter(
|
|
75
|
+
(a) => a.actionType === 'record' && !['show', 'edit', 'delete'].includes(a.name),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
// ── Keyboard shortcuts ──
|
|
79
|
+
// Ctrl/Cmd+E jumps into edit. Discoverable via the action-button tooltip.
|
|
80
|
+
useHotkey(
|
|
81
|
+
'mod+e',
|
|
82
|
+
() => {
|
|
83
|
+
if (!record.data) return
|
|
84
|
+
navigate({ name: 'edit', resourceId, recordId })
|
|
85
|
+
},
|
|
86
|
+
{ description: t('common:edit') },
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
const handleDelete = async (): Promise<void> => {
|
|
90
|
+
const ok = await dialogs.confirm({
|
|
91
|
+
title: t('common:confirmDelete'),
|
|
92
|
+
confirmLabel: t('common:delete'),
|
|
93
|
+
destructive: true,
|
|
94
|
+
})
|
|
95
|
+
if (!ok) return
|
|
96
|
+
await remove.mutateAsync(recordId)
|
|
97
|
+
navigate({ name: 'list', resourceId })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!resource) return <div className="p-6">{t('common:loading')}</div>
|
|
101
|
+
|
|
102
|
+
const modLabel = getModKeyLabel()
|
|
103
|
+
const recordLabel = record.data?.record?.title || recordId
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div className="space-y-2 sm:space-y-4">
|
|
107
|
+
<PageBreadcrumbs
|
|
108
|
+
items={[
|
|
109
|
+
homeCrumb(t('common:home')),
|
|
110
|
+
{ label: resource.name, to: { name: 'list', resourceId } },
|
|
111
|
+
{ label: recordLabel },
|
|
112
|
+
]}
|
|
113
|
+
/>
|
|
114
|
+
<Card>
|
|
115
|
+
<CardHeader className="flex flex-row items-center justify-between gap-3">
|
|
116
|
+
<CardTitle className="truncate">
|
|
117
|
+
{resource.name} #{recordId}
|
|
118
|
+
</CardTitle>
|
|
119
|
+
{record.data && (
|
|
120
|
+
<div className="flex shrink-0 flex-wrap gap-2">
|
|
121
|
+
{features.history && (
|
|
122
|
+
<RevisionsButton resourceId={resourceId} recordId={recordId} />
|
|
123
|
+
)}
|
|
124
|
+
<Tooltip>
|
|
125
|
+
<TooltipTrigger asChild>
|
|
126
|
+
{/* `asChild` + Link-as-Button keeps the rendered DOM a
|
|
127
|
+
* single `<a>` so it picks up the Button's `h-8` from
|
|
128
|
+
* `size="sm"` instead of stacking a Link wrapper that
|
|
129
|
+
* collapses to its anchor default height. */}
|
|
130
|
+
<Button variant="outline-primary" size="sm" asChild>
|
|
131
|
+
<Link to={{ name: 'edit', resourceId, recordId }} aria-label={t('common:edit')}>
|
|
132
|
+
<Pencil className="size-4" />
|
|
133
|
+
<span className="hidden sm:inline">{t('common:edit')}</span>
|
|
134
|
+
</Link>
|
|
135
|
+
</Button>
|
|
136
|
+
</TooltipTrigger>
|
|
137
|
+
<TooltipContent className="flex items-center gap-1.5">
|
|
138
|
+
<span>{t('common:edit')}</span>
|
|
139
|
+
<span className="inline-flex items-center gap-0.5">
|
|
140
|
+
<Kbd>{modLabel}</Kbd>
|
|
141
|
+
<span className="text-muted-foreground">+</span>
|
|
142
|
+
<Kbd>E</Kbd>
|
|
143
|
+
</span>
|
|
144
|
+
</TooltipContent>
|
|
145
|
+
</Tooltip>
|
|
146
|
+
<Button
|
|
147
|
+
variant="outline-destructive"
|
|
148
|
+
size="sm"
|
|
149
|
+
disabled={remove.isPending}
|
|
150
|
+
onClick={() => void handleDelete()}
|
|
151
|
+
aria-label={t('common:delete')}
|
|
152
|
+
>
|
|
153
|
+
<Trash2 className="size-4" />
|
|
154
|
+
<span className="hidden sm:inline">{t('common:delete')}</span>
|
|
155
|
+
</Button>
|
|
156
|
+
{customRecordActions.length > 0 && (
|
|
157
|
+
<ActionMenu
|
|
158
|
+
actions={customRecordActions}
|
|
159
|
+
onAction={async (action) => {
|
|
160
|
+
if (!await confirmGuard(action, dialogs)) return
|
|
161
|
+
void invokeRecord
|
|
162
|
+
.mutateAsync({ recordId, actionName: action.name })
|
|
163
|
+
.then((res) => {
|
|
164
|
+
if (res.notice) {
|
|
165
|
+
const type = res.notice.type === 'error' ? 'error'
|
|
166
|
+
: res.notice.type === 'warning' ? 'warning'
|
|
167
|
+
: res.notice.type === 'info' ? 'info'
|
|
168
|
+
: 'success'
|
|
169
|
+
notify[type]({ message: res.notice.message })
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
.catch((err: Error) =>
|
|
173
|
+
notify.error({ message: err.message }),
|
|
174
|
+
)
|
|
175
|
+
}}
|
|
176
|
+
t={t}
|
|
177
|
+
trigger={(
|
|
178
|
+
<Button
|
|
179
|
+
variant="outline"
|
|
180
|
+
size="sm"
|
|
181
|
+
disabled={invokeRecord.isPending}
|
|
182
|
+
aria-label={t('common:actions')}
|
|
183
|
+
>
|
|
184
|
+
<Zap className="size-4" />
|
|
185
|
+
<span className="hidden sm:inline">{t('common:actions')}</span>
|
|
186
|
+
</Button>
|
|
187
|
+
)}
|
|
188
|
+
/>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
</CardHeader>
|
|
193
|
+
<CardContent>
|
|
194
|
+
<div className="space-y-4">
|
|
195
|
+
{record.isLoading && <p className="text-muted-foreground">{t('common:loading')}</p>}
|
|
196
|
+
{record.isError && <PageError error={record.error} t={t} />}
|
|
197
|
+
{record.data && (
|
|
198
|
+
<dl className="[column-fill:_balance] md:columns-2">
|
|
199
|
+
{visibleRecordProperties(resource.properties, 'show')
|
|
200
|
+
.map((p) => (
|
|
201
|
+
<div key={p.path} className="mb-8 break-inside-avoid">
|
|
202
|
+
<dt className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
203
|
+
{p.label}
|
|
204
|
+
</dt>
|
|
205
|
+
<dd className="mt-1">
|
|
206
|
+
<PropertyDisplay
|
|
207
|
+
property={p}
|
|
208
|
+
value={record.data!.record.params[p.path]}
|
|
209
|
+
view="show"
|
|
210
|
+
populated={record.data!.record.populated as Record<string, unknown> | undefined}
|
|
211
|
+
/>
|
|
212
|
+
</dd>
|
|
213
|
+
</div>
|
|
214
|
+
))}
|
|
215
|
+
</dl>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
</CardContent>
|
|
219
|
+
</Card>
|
|
220
|
+
{record.data && <RelatedRecordsTabs resource={resource} recordId={recordId} />}
|
|
221
|
+
</div>
|
|
222
|
+
)
|
|
223
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// ResourceWizardCreatePage — multi-step creation form for a resource.
|
|
2
|
+
// Analogous to ResourceEditPage but splits the fields across declarative
|
|
3
|
+
// `steps`, each validated independently before advancing. Submit on the final
|
|
4
|
+
// step creates the record and navigates to its show page.
|
|
5
|
+
|
|
6
|
+
import * as React from 'react'
|
|
7
|
+
import { useForm, type SubmitHandler } from 'react-hook-form'
|
|
8
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
9
|
+
import { Card, CardHeader, CardTitle, Form } from '@modern-admin/ui'
|
|
10
|
+
import { useCreateRecord, useResource } from '../hooks.js'
|
|
11
|
+
import { useNavigate } from '../router.js'
|
|
12
|
+
import { useI18n } from '../i18n.js'
|
|
13
|
+
import { useNotify } from '../notify.js'
|
|
14
|
+
import { PageBreadcrumbs, homeCrumb } from '../breadcrumbs.js'
|
|
15
|
+
import { buildValidationSchema, defaultValueFor } from '../validation.js'
|
|
16
|
+
import type { PropertyJSON } from '../types.js'
|
|
17
|
+
import { visibleRecordProperties } from '../relations.js'
|
|
18
|
+
import {
|
|
19
|
+
WizardForm,
|
|
20
|
+
type WizardStep,
|
|
21
|
+
type WizardFormLabels,
|
|
22
|
+
} from '../components/wizard-form.js'
|
|
23
|
+
|
|
24
|
+
export interface ResourceWizardCreatePageProps {
|
|
25
|
+
resourceId: string
|
|
26
|
+
/** Step definitions. Each step declares which property paths it shows. */
|
|
27
|
+
steps: WizardStep[]
|
|
28
|
+
/**
|
|
29
|
+
* Override wizard button labels. Falls back to locale translations.
|
|
30
|
+
* Useful when embedding the page with a custom i18n setup.
|
|
31
|
+
*/
|
|
32
|
+
labels?: WizardFormLabels
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type FormValues = Record<string, unknown>
|
|
36
|
+
|
|
37
|
+
export function ResourceWizardCreatePage({
|
|
38
|
+
resourceId,
|
|
39
|
+
steps,
|
|
40
|
+
labels: labelsProp,
|
|
41
|
+
}: ResourceWizardCreatePageProps): React.ReactElement {
|
|
42
|
+
const resource = useResource(resourceId)
|
|
43
|
+
const create = useCreateRecord(resourceId)
|
|
44
|
+
const navigate = useNavigate()
|
|
45
|
+
const { t, locale } = useI18n()
|
|
46
|
+
const notify = useNotify()
|
|
47
|
+
|
|
48
|
+
const editable = React.useMemo<PropertyJSON[]>(
|
|
49
|
+
() =>
|
|
50
|
+
resource
|
|
51
|
+
? visibleRecordProperties(resource.properties, 'edit').filter(
|
|
52
|
+
(p) => !p.isDisabled,
|
|
53
|
+
)
|
|
54
|
+
: [],
|
|
55
|
+
[resource],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
// Route live form values into the validation schema (same pattern as edit-page).
|
|
59
|
+
const getValuesRef = React.useRef<() => FormValues>(() => ({}))
|
|
60
|
+
|
|
61
|
+
const schema = React.useMemo(
|
|
62
|
+
() => buildValidationSchema(editable, t, () => getValuesRef.current()),
|
|
63
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
64
|
+
[editable, locale],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const defaults = React.useMemo<FormValues>(() => {
|
|
68
|
+
const out: FormValues = {}
|
|
69
|
+
for (const p of editable) out[p.path] = defaultValueFor(p)
|
|
70
|
+
return out
|
|
71
|
+
}, [editable])
|
|
72
|
+
|
|
73
|
+
const form = useForm<FormValues>({
|
|
74
|
+
resolver: zodResolver(schema),
|
|
75
|
+
defaultValues: defaults,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
getValuesRef.current = form.getValues
|
|
79
|
+
|
|
80
|
+
// Reset when the resource schema arrives (resource loaded after mount).
|
|
81
|
+
React.useEffect(() => {
|
|
82
|
+
form.reset(defaults)
|
|
83
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
84
|
+
}, [defaults])
|
|
85
|
+
|
|
86
|
+
const [submitError, setSubmitError] = React.useState<string | null>(null)
|
|
87
|
+
|
|
88
|
+
const onSubmit: SubmitHandler<FormValues> = async (values) => {
|
|
89
|
+
setSubmitError(null)
|
|
90
|
+
try {
|
|
91
|
+
const result = await create.mutateAsync(values)
|
|
92
|
+
const errors = result.record.errors as Record<
|
|
93
|
+
string,
|
|
94
|
+
{ message?: string } | string
|
|
95
|
+
>
|
|
96
|
+
if (errors && Object.keys(errors).length > 0) {
|
|
97
|
+
for (const [path, err] of Object.entries(errors)) {
|
|
98
|
+
const message =
|
|
99
|
+
typeof err === 'string' ? err : (err?.message ?? 'Invalid value')
|
|
100
|
+
form.setError(path, { type: 'server', message })
|
|
101
|
+
}
|
|
102
|
+
if (result.record.baseError) setSubmitError(String(result.record.baseError))
|
|
103
|
+
notify.error({ key: 'toast:validationFailed' })
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
notify.success({ key: 'toast:created' })
|
|
107
|
+
navigate({ name: 'show', resourceId, recordId: String(result.record.id) })
|
|
108
|
+
} catch (err) {
|
|
109
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
110
|
+
setSubmitError(message)
|
|
111
|
+
notify.error({ key: 'toast:createFailed' }, { description: message })
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const handleSubmit = (): void => {
|
|
116
|
+
void form.handleSubmit(onSubmit, () => {
|
|
117
|
+
notify.error({ key: 'toast:validationFailed' })
|
|
118
|
+
})()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!resource) return <div className="p-6">{t('common:loading')}</div>
|
|
122
|
+
|
|
123
|
+
const labels: WizardFormLabels = {
|
|
124
|
+
back: t('common:back'),
|
|
125
|
+
next: t('common:next'),
|
|
126
|
+
submit: t('common:create'),
|
|
127
|
+
cancel: t('common:cancel'),
|
|
128
|
+
stepOf: t('wizard:stepOf'),
|
|
129
|
+
...labelsProp,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const crumbs = [
|
|
133
|
+
homeCrumb(t('common:home')),
|
|
134
|
+
{ label: resource.name, to: { name: 'list' as const, resourceId } },
|
|
135
|
+
{ label: t('common:new') },
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div className="flex min-h-full flex-col gap-4">
|
|
140
|
+
<PageBreadcrumbs items={crumbs} />
|
|
141
|
+
<Card className="flex-1">
|
|
142
|
+
<CardHeader>
|
|
143
|
+
<CardTitle className="truncate">
|
|
144
|
+
{t('common:newRecord', { name: resource.name })}
|
|
145
|
+
</CardTitle>
|
|
146
|
+
</CardHeader>
|
|
147
|
+
<Form {...form}>
|
|
148
|
+
<WizardForm
|
|
149
|
+
steps={steps}
|
|
150
|
+
properties={editable}
|
|
151
|
+
resourceId={resourceId}
|
|
152
|
+
control={form.control}
|
|
153
|
+
trigger={form.trigger}
|
|
154
|
+
onSubmit={handleSubmit}
|
|
155
|
+
onCancel={() => navigate({ name: 'list', resourceId })}
|
|
156
|
+
isSubmitting={form.formState.isSubmitting}
|
|
157
|
+
submitError={submitError}
|
|
158
|
+
labels={labels}
|
|
159
|
+
/>
|
|
160
|
+
</Form>
|
|
161
|
+
</Card>
|
|
162
|
+
</div>
|
|
163
|
+
)
|
|
164
|
+
}
|