@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,272 @@
|
|
|
1
|
+
// Cross-resource command palette. Backed by `useGlobalSearch`, which fans
|
|
2
|
+
// the query out to every registered resource's `search` action server-side.
|
|
3
|
+
// Results are grouped by resource; selecting an entry navigates to the
|
|
4
|
+
// record's show page and closes the palette.
|
|
5
|
+
//
|
|
6
|
+
// Designed as a controlled component — the parent (typically the header)
|
|
7
|
+
// holds the `open` state so it can pair the trigger button with the
|
|
8
|
+
// `mod+k` hotkey.
|
|
9
|
+
|
|
10
|
+
import * as React from 'react'
|
|
11
|
+
import {
|
|
12
|
+
CommandDialog,
|
|
13
|
+
CommandEmpty,
|
|
14
|
+
CommandGroup,
|
|
15
|
+
CommandInput,
|
|
16
|
+
CommandItem,
|
|
17
|
+
CommandList,
|
|
18
|
+
CommandSeparator,
|
|
19
|
+
DialogDescription,
|
|
20
|
+
DialogTitle,
|
|
21
|
+
} from '@modern-admin/ui'
|
|
22
|
+
import { ArrowRight, Clock, Loader2, X } from 'lucide-react'
|
|
23
|
+
import { useGlobalSearch } from '../hooks.js'
|
|
24
|
+
import { useI18n } from '../i18n.js'
|
|
25
|
+
import { useNavigate } from '../router.js'
|
|
26
|
+
|
|
27
|
+
const DEBOUNCE_MS = 300
|
|
28
|
+
const RECENT_STORAGE_KEY = 'modern-admin:global-search:recent:v1'
|
|
29
|
+
const RECENT_MAX = 6
|
|
30
|
+
|
|
31
|
+
export interface GlobalSearchDialogProps {
|
|
32
|
+
open: boolean
|
|
33
|
+
|
|
34
|
+
onOpenChange(open: boolean): void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const readRecent = (): string[] => {
|
|
38
|
+
if (typeof window === 'undefined') return []
|
|
39
|
+
try {
|
|
40
|
+
const raw = window.localStorage.getItem(RECENT_STORAGE_KEY)
|
|
41
|
+
if (!raw) return []
|
|
42
|
+
const parsed = JSON.parse(raw) as unknown
|
|
43
|
+
if (!Array.isArray(parsed)) return []
|
|
44
|
+
return parsed.filter((v): v is string => typeof v === 'string').slice(0, RECENT_MAX)
|
|
45
|
+
} catch {
|
|
46
|
+
return []
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const writeRecent = (entries: string[]): void => {
|
|
51
|
+
if (typeof window === 'undefined') return
|
|
52
|
+
try {
|
|
53
|
+
window.localStorage.setItem(RECENT_STORAGE_KEY, JSON.stringify(entries.slice(0, RECENT_MAX)))
|
|
54
|
+
} catch {
|
|
55
|
+
/* quota exceeded — recent list is best-effort */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Highlight every case-insensitive occurrence of `needle` in `text` with
|
|
61
|
+
* `<mark>`. Returns an array of React nodes ready to render inside any
|
|
62
|
+
* inline container. Empty `needle` falls back to the raw string.
|
|
63
|
+
*/
|
|
64
|
+
const highlightMatch = (text: string, needle: string): React.ReactNode => {
|
|
65
|
+
if (!needle) return text
|
|
66
|
+
const lower = text.toLowerCase()
|
|
67
|
+
const target = needle.toLowerCase()
|
|
68
|
+
const nodes: React.ReactNode[] = []
|
|
69
|
+
let cursor = 0
|
|
70
|
+
let key = 0
|
|
71
|
+
while (cursor < text.length) {
|
|
72
|
+
const idx = lower.indexOf(target, cursor)
|
|
73
|
+
if (idx === -1) {
|
|
74
|
+
nodes.push(text.slice(cursor))
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
if (idx > cursor) nodes.push(text.slice(cursor, idx))
|
|
78
|
+
nodes.push(
|
|
79
|
+
<mark
|
|
80
|
+
key={key++}
|
|
81
|
+
className="rounded-sm bg-primary/20 px-0.5 text-foreground"
|
|
82
|
+
>
|
|
83
|
+
{text.slice(idx, idx + target.length)}
|
|
84
|
+
</mark>,
|
|
85
|
+
)
|
|
86
|
+
cursor = idx + target.length
|
|
87
|
+
}
|
|
88
|
+
return nodes
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function GlobalSearchDialog({
|
|
92
|
+
open,
|
|
93
|
+
onOpenChange,
|
|
94
|
+
}: GlobalSearchDialogProps): React.ReactElement {
|
|
95
|
+
const { t } = useI18n()
|
|
96
|
+
const navigate = useNavigate()
|
|
97
|
+
const [query, setQuery] = React.useState('')
|
|
98
|
+
const [debounced, setDebounced] = React.useState('')
|
|
99
|
+
const [recent, setRecent] = React.useState<string[]>(() => readRecent())
|
|
100
|
+
|
|
101
|
+
// Reset query each time the dialog opens so it starts empty, and rehydrate
|
|
102
|
+
// the recent list (other tabs may have appended entries while we were idle).
|
|
103
|
+
React.useEffect(() => {
|
|
104
|
+
if (open) {
|
|
105
|
+
setQuery('')
|
|
106
|
+
setDebounced('')
|
|
107
|
+
setRecent(readRecent())
|
|
108
|
+
}
|
|
109
|
+
}, [open])
|
|
110
|
+
|
|
111
|
+
React.useEffect(() => {
|
|
112
|
+
const timer = window.setTimeout(() => setDebounced(query.trim()), DEBOUNCE_MS)
|
|
113
|
+
return () => window.clearTimeout(timer)
|
|
114
|
+
}, [query])
|
|
115
|
+
|
|
116
|
+
const { data, isFetching } = useGlobalSearch(debounced, open)
|
|
117
|
+
|
|
118
|
+
// Capture the most recent successful query so it's available for the
|
|
119
|
+
// "recent" list. We only persist on user-driven navigation (not every
|
|
120
|
+
// keystroke) to keep the list signal-to-noise high.
|
|
121
|
+
const persistRecent = React.useCallback((value: string): void => {
|
|
122
|
+
if (!value) return
|
|
123
|
+
setRecent((prev) => {
|
|
124
|
+
const next = [value, ...prev.filter((q) => q !== value)].slice(0, RECENT_MAX)
|
|
125
|
+
writeRecent(next)
|
|
126
|
+
return next
|
|
127
|
+
})
|
|
128
|
+
}, [])
|
|
129
|
+
|
|
130
|
+
const groups = data?.groups ?? []
|
|
131
|
+
const hasQuery = debounced.length > 0
|
|
132
|
+
const showEmpty = hasQuery && !isFetching && groups.length === 0
|
|
133
|
+
|
|
134
|
+
const handleSelect = React.useCallback(
|
|
135
|
+
(resourceId: string, recordId: string): void => {
|
|
136
|
+
persistRecent(debounced)
|
|
137
|
+
onOpenChange(false)
|
|
138
|
+
navigate({ name: 'show', resourceId, recordId })
|
|
139
|
+
},
|
|
140
|
+
[debounced, navigate, onOpenChange, persistRecent],
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const handleShowAll = React.useCallback(
|
|
144
|
+
(resourceId: string): void => {
|
|
145
|
+
persistRecent(debounced)
|
|
146
|
+
onOpenChange(false)
|
|
147
|
+
navigate({ name: 'list', resourceId })
|
|
148
|
+
},
|
|
149
|
+
[debounced, navigate, onOpenChange, persistRecent],
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
const handlePickRecent = React.useCallback((value: string): void => {
|
|
153
|
+
setQuery(value)
|
|
154
|
+
setDebounced(value)
|
|
155
|
+
}, [])
|
|
156
|
+
|
|
157
|
+
const handleClearRecent = React.useCallback((): void => {
|
|
158
|
+
setRecent([])
|
|
159
|
+
writeRecent([])
|
|
160
|
+
}, [])
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<CommandDialog open={open} onOpenChange={onOpenChange}>
|
|
164
|
+
{/* Visually-hidden title + description keep Radix Dialog accessibility
|
|
165
|
+
warnings quiet and provide a label for screen readers. */}
|
|
166
|
+
<DialogTitle className="sr-only">{t('globalSearch:title')}</DialogTitle>
|
|
167
|
+
<DialogDescription className="sr-only">
|
|
168
|
+
{t('globalSearch:description')}
|
|
169
|
+
</DialogDescription>
|
|
170
|
+
<CommandInput
|
|
171
|
+
placeholder={t('globalSearch:placeholder')}
|
|
172
|
+
value={query}
|
|
173
|
+
onValueChange={setQuery}
|
|
174
|
+
/>
|
|
175
|
+
{/* cmdk dedupes by item value — prefix each value with `resourceId:recordId`
|
|
176
|
+
so identical record ids across resources keep distinct entries.
|
|
177
|
+
shouldFilter is left on (default) so cmdk re-orders by relevance against
|
|
178
|
+
the typed query — server already narrowed the set down. */}
|
|
179
|
+
<CommandList>
|
|
180
|
+
{!hasQuery && recent.length === 0 && (
|
|
181
|
+
<div className="py-6 text-center text-sm text-muted-foreground">
|
|
182
|
+
{t('globalSearch:hint')}
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
{!hasQuery && recent.length > 0 && (
|
|
186
|
+
<CommandGroup
|
|
187
|
+
heading={
|
|
188
|
+
<span className="flex items-center justify-between gap-2">
|
|
189
|
+
<span>{t('globalSearch:recent')}</span>
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
className="text-xs text-muted-foreground hover:text-foreground"
|
|
193
|
+
onClick={handleClearRecent}
|
|
194
|
+
>
|
|
195
|
+
<X className="mr-1 inline size-3" aria-hidden="true" />
|
|
196
|
+
{t('globalSearch:clearRecent')}
|
|
197
|
+
</button>
|
|
198
|
+
</span>
|
|
199
|
+
}
|
|
200
|
+
>
|
|
201
|
+
{recent.map((entry) => (
|
|
202
|
+
<CommandItem
|
|
203
|
+
key={`recent:${entry}`}
|
|
204
|
+
value={`recent:${entry}`}
|
|
205
|
+
onSelect={() => handlePickRecent(entry)}
|
|
206
|
+
>
|
|
207
|
+
<Clock className="mr-2 size-4 text-muted-foreground" aria-hidden="true" />
|
|
208
|
+
<span className="flex-1 truncate">{entry}</span>
|
|
209
|
+
</CommandItem>
|
|
210
|
+
))}
|
|
211
|
+
</CommandGroup>
|
|
212
|
+
)}
|
|
213
|
+
{showEmpty && <CommandEmpty>{t('globalSearch:noResults')}</CommandEmpty>}
|
|
214
|
+
{hasQuery && isFetching && groups.length === 0 && (
|
|
215
|
+
<div
|
|
216
|
+
className="flex items-center justify-center gap-2 py-6 text-sm text-muted-foreground"
|
|
217
|
+
role="status"
|
|
218
|
+
aria-live="polite"
|
|
219
|
+
>
|
|
220
|
+
<Loader2 className="size-4 animate-spin" aria-hidden="true"/>
|
|
221
|
+
<span>{t('common:loading')}</span>
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
{groups.map((group, idx) => (
|
|
225
|
+
<React.Fragment key={group.resourceId}>
|
|
226
|
+
{idx > 0 && <CommandSeparator/>}
|
|
227
|
+
<CommandGroup heading={group.resourceName}>
|
|
228
|
+
{group.records.map((hit) => (
|
|
229
|
+
<CommandItem
|
|
230
|
+
key={`${hit.resourceId}:${hit.recordId}`}
|
|
231
|
+
value={`${hit.resourceId}:${hit.recordId} ${hit.title} ${hit.snippet ?? ''}`}
|
|
232
|
+
onSelect={() => handleSelect(hit.resourceId, hit.recordId)}
|
|
233
|
+
className="flex-col items-start gap-0.5"
|
|
234
|
+
>
|
|
235
|
+
<div className="flex w-full items-baseline gap-2">
|
|
236
|
+
<span className="flex-1 truncate font-medium">
|
|
237
|
+
{highlightMatch(hit.title, debounced)}
|
|
238
|
+
</span>
|
|
239
|
+
{hit.matchedField && (
|
|
240
|
+
<span className="shrink-0 text-xs text-muted-foreground">
|
|
241
|
+
{t('globalSearch:matchedIn').replace('{field}', hit.matchedField)}
|
|
242
|
+
</span>
|
|
243
|
+
)}
|
|
244
|
+
</div>
|
|
245
|
+
{hit.snippet && (
|
|
246
|
+
<div className="line-clamp-1 text-xs text-muted-foreground">
|
|
247
|
+
{highlightMatch(hit.snippet, debounced)}
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
250
|
+
</CommandItem>
|
|
251
|
+
))}
|
|
252
|
+
{/* `forceMount` keeps this row visible regardless of the current
|
|
253
|
+
query — cmdk's default fuzzy filter would otherwise drop it
|
|
254
|
+
because the value `${id}:show-all` rarely contains the typed
|
|
255
|
+
needle. */}
|
|
256
|
+
<CommandItem
|
|
257
|
+
key={`${group.resourceId}:show-all`}
|
|
258
|
+
value={`${group.resourceId}:show-all`}
|
|
259
|
+
forceMount
|
|
260
|
+
onSelect={() => handleShowAll(group.resourceId)}
|
|
261
|
+
className="text-xs text-muted-foreground"
|
|
262
|
+
>
|
|
263
|
+
<ArrowRight className="mr-2 size-3.5" aria-hidden="true" />
|
|
264
|
+
{t('globalSearch:showAllIn').replace('{resource}', group.resourceName)}
|
|
265
|
+
</CommandItem>
|
|
266
|
+
</CommandGroup>
|
|
267
|
+
</React.Fragment>
|
|
268
|
+
))}
|
|
269
|
+
</CommandList>
|
|
270
|
+
</CommandDialog>
|
|
271
|
+
)
|
|
272
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Create / edit dialog for a dashboard chart group. Lives in the react
|
|
2
|
+
// package because it is i18n-aware; the actual primitives come from
|
|
3
|
+
// @modern-admin/ui.
|
|
4
|
+
|
|
5
|
+
import * as React from 'react'
|
|
6
|
+
import {
|
|
7
|
+
Button,
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogFooter,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
InfoTooltip,
|
|
14
|
+
Input,
|
|
15
|
+
Label,
|
|
16
|
+
} from '@modern-admin/ui'
|
|
17
|
+
import type { ChartGroup } from '@modern-admin/core'
|
|
18
|
+
import { useI18n } from '../i18n.js'
|
|
19
|
+
|
|
20
|
+
export interface GroupSettingsDialogProps {
|
|
21
|
+
/** When set, the dialog is in edit mode and pre-populates from this group. */
|
|
22
|
+
initial?: ChartGroup
|
|
23
|
+
onSave(input: { name: string; order: number }): void
|
|
24
|
+
onClose(): void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function GroupSettingsDialog({
|
|
28
|
+
initial,
|
|
29
|
+
onSave,
|
|
30
|
+
onClose,
|
|
31
|
+
}: GroupSettingsDialogProps): React.ReactElement {
|
|
32
|
+
const { t } = useI18n()
|
|
33
|
+
const [name, setName] = React.useState(initial?.name ?? '')
|
|
34
|
+
const [order, setOrder] = React.useState<number>(initial?.order ?? 0)
|
|
35
|
+
const [error, setError] = React.useState<string>('')
|
|
36
|
+
|
|
37
|
+
const handleSave = (): void => {
|
|
38
|
+
const trimmed = name.trim()
|
|
39
|
+
if (!trimmed) {
|
|
40
|
+
setError(t('chart:groupNameRequired'))
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
setError('')
|
|
44
|
+
onSave({ name: trimmed, order: Number.isFinite(order) ? Math.trunc(order) : 0 })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Dialog open onOpenChange={(open) => { if (!open) onClose() }}>
|
|
49
|
+
<DialogContent className="w-full max-w-md">
|
|
50
|
+
<DialogHeader>
|
|
51
|
+
<DialogTitle>
|
|
52
|
+
{initial ? t('chart:editGroup') : t('chart:newGroup')}
|
|
53
|
+
</DialogTitle>
|
|
54
|
+
</DialogHeader>
|
|
55
|
+
|
|
56
|
+
<div className="space-y-4 py-1">
|
|
57
|
+
<div className="space-y-1.5">
|
|
58
|
+
<Label htmlFor="group-name">{t('chart:groupName')}</Label>
|
|
59
|
+
<Input
|
|
60
|
+
id="group-name"
|
|
61
|
+
placeholder={t('chart:groupNamePlaceholder')}
|
|
62
|
+
value={name}
|
|
63
|
+
onChange={(e) => setName(e.target.value)}
|
|
64
|
+
autoFocus
|
|
65
|
+
/>
|
|
66
|
+
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div className="space-y-1.5">
|
|
70
|
+
<div className="flex items-center gap-1.5">
|
|
71
|
+
<Label htmlFor="group-order">{t('chart:groupOrder')}</Label>
|
|
72
|
+
<InfoTooltip content={t('chart:orderHint')} />
|
|
73
|
+
</div>
|
|
74
|
+
<Input
|
|
75
|
+
id="group-order"
|
|
76
|
+
type="number"
|
|
77
|
+
step={1}
|
|
78
|
+
value={order}
|
|
79
|
+
onChange={(e) =>
|
|
80
|
+
setOrder(Number.isFinite(Number(e.target.value)) ? Math.trunc(Number(e.target.value)) : 0)
|
|
81
|
+
}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<DialogFooter>
|
|
87
|
+
<Button variant="outline" onClick={onClose}>{t('common:cancel')}</Button>
|
|
88
|
+
<Button onClick={handleSave}>{t('chart:saveGroup')}</Button>
|
|
89
|
+
</DialogFooter>
|
|
90
|
+
</DialogContent>
|
|
91
|
+
</Dialog>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Dialog for moving a chart to a different group and adjusting its order.
|
|
2
|
+
// Opened from the chart widget's "…" dropdown menu.
|
|
3
|
+
|
|
4
|
+
import * as React from 'react'
|
|
5
|
+
import { FolderPlus } from 'lucide-react'
|
|
6
|
+
import {
|
|
7
|
+
Button,
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogFooter,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
InfoTooltip,
|
|
14
|
+
Input,
|
|
15
|
+
Label,
|
|
16
|
+
Select,
|
|
17
|
+
SelectContent,
|
|
18
|
+
SelectItem,
|
|
19
|
+
SelectTrigger,
|
|
20
|
+
SelectValue,
|
|
21
|
+
} from '@modern-admin/ui'
|
|
22
|
+
import type { ChartGroup } from '@modern-admin/core'
|
|
23
|
+
import { useI18n } from '../i18n.js'
|
|
24
|
+
|
|
25
|
+
export interface MoveChartDialogProps {
|
|
26
|
+
groups: ChartGroup[]
|
|
27
|
+
/** Current group id of the chart being moved. */
|
|
28
|
+
initialGroupId?: string
|
|
29
|
+
/** Current order value of the chart being moved. */
|
|
30
|
+
initialOrder?: number
|
|
31
|
+
onSave(input: { groupId: string; order: number }): void
|
|
32
|
+
onClose(): void
|
|
33
|
+
/** Called when the user wants to create a group first (no groups exist). */
|
|
34
|
+
onCreateGroup(): void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function MoveChartDialog({
|
|
38
|
+
groups,
|
|
39
|
+
initialGroupId,
|
|
40
|
+
initialOrder,
|
|
41
|
+
onSave,
|
|
42
|
+
onClose,
|
|
43
|
+
onCreateGroup,
|
|
44
|
+
}: MoveChartDialogProps): React.ReactElement {
|
|
45
|
+
const { t } = useI18n()
|
|
46
|
+
|
|
47
|
+
const sorted = React.useMemo(
|
|
48
|
+
() => [...groups].sort((a, b) => a.order - b.order),
|
|
49
|
+
[groups],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
// Seed from the chart's current group, or the first group if unset.
|
|
53
|
+
const defaultGroupId = initialGroupId ?? sorted[0]?.id ?? ''
|
|
54
|
+
const [groupId, setGroupId] = React.useState(defaultGroupId)
|
|
55
|
+
const [order, setOrder] = React.useState<number>(initialOrder ?? 0)
|
|
56
|
+
|
|
57
|
+
const handleSave = (): void => {
|
|
58
|
+
if (!groupId) return
|
|
59
|
+
onSave({ groupId, order: Number.isFinite(order) ? Math.trunc(order) : 0 })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Dialog open onOpenChange={(open) => { if (!open) onClose() }}>
|
|
64
|
+
<DialogContent className="w-full max-w-md">
|
|
65
|
+
<DialogHeader>
|
|
66
|
+
<DialogTitle>{t('chart:moveChart')}</DialogTitle>
|
|
67
|
+
</DialogHeader>
|
|
68
|
+
|
|
69
|
+
{sorted.length === 0 ? (
|
|
70
|
+
// No groups yet — prompt to create one.
|
|
71
|
+
<div className="space-y-3 py-2">
|
|
72
|
+
<p className="text-sm font-medium">{t('chart:moveNoGroups')}</p>
|
|
73
|
+
<p className="text-sm text-muted-foreground">{t('chart:moveNoGroupsHint')}</p>
|
|
74
|
+
<Button
|
|
75
|
+
variant="outline"
|
|
76
|
+
className="w-full"
|
|
77
|
+
onClick={() => { onClose(); onCreateGroup() }}
|
|
78
|
+
>
|
|
79
|
+
<FolderPlus className="size-4 mr-2" />
|
|
80
|
+
{t('chart:addGroup')}
|
|
81
|
+
</Button>
|
|
82
|
+
</div>
|
|
83
|
+
) : (
|
|
84
|
+
<div className="space-y-4 py-1">
|
|
85
|
+
<div className="space-y-1.5">
|
|
86
|
+
<Label htmlFor="move-group">{t('chart:moveGroup')}</Label>
|
|
87
|
+
<Select value={groupId} onValueChange={setGroupId}>
|
|
88
|
+
<SelectTrigger id="move-group" className="w-full">
|
|
89
|
+
<SelectValue />
|
|
90
|
+
</SelectTrigger>
|
|
91
|
+
<SelectContent>
|
|
92
|
+
{sorted.map((g) => (
|
|
93
|
+
<SelectItem key={g.id} value={g.id}>
|
|
94
|
+
{g.name}
|
|
95
|
+
</SelectItem>
|
|
96
|
+
))}
|
|
97
|
+
</SelectContent>
|
|
98
|
+
</Select>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div className="space-y-1.5">
|
|
102
|
+
<div className="flex items-center gap-1.5">
|
|
103
|
+
<Label htmlFor="move-order">{t('chart:order')}</Label>
|
|
104
|
+
<InfoTooltip content={t('chart:orderHint')} />
|
|
105
|
+
</div>
|
|
106
|
+
<Input
|
|
107
|
+
id="move-order"
|
|
108
|
+
type="number"
|
|
109
|
+
step={1}
|
|
110
|
+
value={order}
|
|
111
|
+
onChange={(e) =>
|
|
112
|
+
setOrder(Number.isFinite(Number(e.target.value)) ? Math.trunc(Number(e.target.value)) : 0)
|
|
113
|
+
}
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
<DialogFooter>
|
|
120
|
+
<Button variant="outline" onClick={onClose}>{t('common:cancel')}</Button>
|
|
121
|
+
{sorted.length > 0 && (
|
|
122
|
+
<Button onClick={handleSave} disabled={!groupId}>
|
|
123
|
+
{t('chart:moveToGroup')}
|
|
124
|
+
</Button>
|
|
125
|
+
)}
|
|
126
|
+
</DialogFooter>
|
|
127
|
+
</DialogContent>
|
|
128
|
+
</Dialog>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// Picker dialog for many-to-many relation fields. Opens a modal containing
|
|
2
|
+
// the embedded ResourceListPage of the referenced resource in "picker" mode
|
|
3
|
+
// (controlled row selection, no row navigation, no toolbar create/export).
|
|
4
|
+
// The dialog inherits the full list UX — sorting, filtering, header
|
|
5
|
+
// filters, column visibility, pagination — so users can find records the
|
|
6
|
+
// same way they would on the main list page.
|
|
7
|
+
//
|
|
8
|
+
// Selection inside the dialog is staged locally and only committed to the
|
|
9
|
+
// outer form on Save, so the user can cancel without polluting the value.
|
|
10
|
+
//
|
|
11
|
+
// Used as an alternative to ReferenceMultiCombobox for m2m and
|
|
12
|
+
// reference-array fields. Renders existing selections as removable chips
|
|
13
|
+
// above the trigger button — same chip pattern as ReferenceMultiCombobox.
|
|
14
|
+
|
|
15
|
+
import * as React from 'react'
|
|
16
|
+
import {
|
|
17
|
+
Badge,
|
|
18
|
+
Button,
|
|
19
|
+
Dialog,
|
|
20
|
+
DialogContent,
|
|
21
|
+
DialogFooter,
|
|
22
|
+
DialogHeader,
|
|
23
|
+
DialogTitle,
|
|
24
|
+
cn,
|
|
25
|
+
} from '@modern-admin/ui'
|
|
26
|
+
import { Plus, X } from 'lucide-react'
|
|
27
|
+
import { useQueries } from '@tanstack/react-query'
|
|
28
|
+
import { useAdminClient } from '../provider.js'
|
|
29
|
+
import { useI18n } from '../i18n.js'
|
|
30
|
+
import { useResource } from '../hooks.js'
|
|
31
|
+
import { ResourceListPage } from '../pages/list-page.js'
|
|
32
|
+
import type { ListQueryState } from '../router.js'
|
|
33
|
+
|
|
34
|
+
export interface ReferenceMultiTableDialogProps {
|
|
35
|
+
referenceResourceId: string
|
|
36
|
+
value: ReadonlyArray<string | number> | null | undefined
|
|
37
|
+
onChange(next: Array<string | number>): void
|
|
38
|
+
disabled?: boolean
|
|
39
|
+
/** Label for the trigger button. Defaults to "Pick records". */
|
|
40
|
+
triggerLabel?: string
|
|
41
|
+
className?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function ReferenceMultiTableDialog({
|
|
45
|
+
referenceResourceId,
|
|
46
|
+
value,
|
|
47
|
+
onChange,
|
|
48
|
+
disabled,
|
|
49
|
+
triggerLabel,
|
|
50
|
+
className,
|
|
51
|
+
}: ReferenceMultiTableDialogProps): React.ReactElement {
|
|
52
|
+
const { t } = useI18n()
|
|
53
|
+
const client = useAdminClient()
|
|
54
|
+
const resource = useResource(referenceResourceId)
|
|
55
|
+
|
|
56
|
+
const committedIds = React.useMemo(
|
|
57
|
+
() => (value ?? []).map(String),
|
|
58
|
+
[value],
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const [open, setOpen] = React.useState(false)
|
|
62
|
+
// Staged selection inside the dialog. Reset to committed value each time
|
|
63
|
+
// the dialog opens so a previous Cancel doesn't leak state.
|
|
64
|
+
const [stagedIds, setStagedIds] = React.useState<string[]>(() => committedIds)
|
|
65
|
+
React.useEffect(() => {
|
|
66
|
+
if (open) setStagedIds(committedIds)
|
|
67
|
+
}, [open, committedIds])
|
|
68
|
+
|
|
69
|
+
// Embedded list page keeps its own page/sort/filter state. Reset to
|
|
70
|
+
// page 1 each open so each pick session starts fresh.
|
|
71
|
+
const [query, setQuery] = React.useState<ListQueryState>({ perPage: 10 })
|
|
72
|
+
React.useEffect(() => {
|
|
73
|
+
if (open) setQuery({ perPage: 10 })
|
|
74
|
+
}, [open])
|
|
75
|
+
|
|
76
|
+
// Resolve chip labels by fetching each currently-committed record. Sharing
|
|
77
|
+
// the query key with ReferenceLink keeps the cache warm — already-visible
|
|
78
|
+
// chips render instantly.
|
|
79
|
+
const titleQueries = useQueries({
|
|
80
|
+
queries: committedIds.map((id) => ({
|
|
81
|
+
queryKey: ['modern-admin', referenceResourceId, 'show', id],
|
|
82
|
+
queryFn: () => client.show(referenceResourceId, id),
|
|
83
|
+
staleTime: 30_000,
|
|
84
|
+
})),
|
|
85
|
+
})
|
|
86
|
+
const chips = React.useMemo(
|
|
87
|
+
() =>
|
|
88
|
+
committedIds.map((id, i) => {
|
|
89
|
+
const title = titleQueries[i]?.data?.record?.title
|
|
90
|
+
return { id, label: title ? `${title} <${id}>` : `#${id}` }
|
|
91
|
+
}),
|
|
92
|
+
[committedIds, titleQueries],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const remove = (id: string): void => {
|
|
96
|
+
onChange(committedIds.filter((x) => x !== id))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const handleSave = (): void => {
|
|
100
|
+
onChange(stagedIds)
|
|
101
|
+
setOpen(false)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const handleCancel = (): void => {
|
|
105
|
+
setOpen(false)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const resolvedTriggerLabel =
|
|
109
|
+
triggerLabel ??
|
|
110
|
+
(committedIds.length > 0
|
|
111
|
+
? t('common:managePickRecords', { count: committedIds.length })
|
|
112
|
+
: t('common:pickRecords'))
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div className={cn('space-y-2', className)}>
|
|
116
|
+
{chips.length > 0 && (
|
|
117
|
+
<div className="flex flex-wrap gap-1">
|
|
118
|
+
{chips.map((c) => (
|
|
119
|
+
<Badge key={c.id} variant="secondary" className="gap-1 pr-1">
|
|
120
|
+
<span className="truncate" title={c.label}>{c.label}</span>
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
aria-label={t('common:removeItem', { title: c.label })}
|
|
124
|
+
disabled={disabled}
|
|
125
|
+
onClick={() => remove(c.id)}
|
|
126
|
+
className="rounded-sm opacity-60 hover:opacity-100"
|
|
127
|
+
>
|
|
128
|
+
<X className="size-3" />
|
|
129
|
+
</button>
|
|
130
|
+
</Badge>
|
|
131
|
+
))}
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
<Button
|
|
135
|
+
type="button"
|
|
136
|
+
variant="outline"
|
|
137
|
+
disabled={disabled}
|
|
138
|
+
onClick={() => setOpen(true)}
|
|
139
|
+
className="w-full justify-start font-normal"
|
|
140
|
+
>
|
|
141
|
+
<Plus className="size-4" />
|
|
142
|
+
<span className="truncate">{resolvedTriggerLabel}</span>
|
|
143
|
+
</Button>
|
|
144
|
+
|
|
145
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
146
|
+
<DialogContent
|
|
147
|
+
// Wide layout so the embedded table has room. Cap height and let
|
|
148
|
+
// the body scroll independently of the footer.
|
|
149
|
+
className="flex max-h-[90vh] w-[95vw] max-w-5xl flex-col gap-0 p-0"
|
|
150
|
+
>
|
|
151
|
+
<DialogHeader className="border-b border-border px-6 py-4">
|
|
152
|
+
<DialogTitle>
|
|
153
|
+
{t('common:pickRecordsFrom', { name: resource?.name ?? referenceResourceId })}
|
|
154
|
+
</DialogTitle>
|
|
155
|
+
</DialogHeader>
|
|
156
|
+
{/* The list page is in embedded mode (`card: false`) so it
|
|
157
|
+
manages its own internal scroll: the table area scrolls,
|
|
158
|
+
the paginator sits below as a flush full-width bar. The
|
|
159
|
+
body wrapper itself does NOT scroll, and `overflow-hidden`
|
|
160
|
+
guarantees its children cannot bleed visually past the
|
|
161
|
+
flex-1 box (and onto the DialogFooter below). */}
|
|
162
|
+
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
163
|
+
<ResourceListPage
|
|
164
|
+
resourceId={referenceResourceId}
|
|
165
|
+
query={query}
|
|
166
|
+
onQueryChange={setQuery}
|
|
167
|
+
selectedIds={stagedIds}
|
|
168
|
+
onSelectionChange={setStagedIds}
|
|
169
|
+
disableRowNavigation
|
|
170
|
+
features={{
|
|
171
|
+
breadcrumbs: false,
|
|
172
|
+
title: false,
|
|
173
|
+
create: false,
|
|
174
|
+
export: false,
|
|
175
|
+
bulk: false,
|
|
176
|
+
actions: false,
|
|
177
|
+
card: false,
|
|
178
|
+
}}
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
<DialogFooter className="gap-2 border-t border-border px-6 py-4">
|
|
182
|
+
<div className="mr-auto text-sm text-muted-foreground self-center">
|
|
183
|
+
{t('common:selectedCount', { count: stagedIds.length })}
|
|
184
|
+
</div>
|
|
185
|
+
<Button variant="outline" type="button" onClick={handleCancel}>
|
|
186
|
+
{t('common:cancel')}
|
|
187
|
+
</Button>
|
|
188
|
+
<Button type="button" onClick={handleSave}>
|
|
189
|
+
{t('common:save')}
|
|
190
|
+
</Button>
|
|
191
|
+
</DialogFooter>
|
|
192
|
+
</DialogContent>
|
|
193
|
+
</Dialog>
|
|
194
|
+
</div>
|
|
195
|
+
)
|
|
196
|
+
}
|