@asteby/metacore-runtime-react 6.0.0 → 6.4.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/CHANGELOG.md +12 -2
- package/README.md +67 -44
- package/dist/dialogs/export.js +1 -1
- package/dist/dialogs/import.d.ts.map +1 -1
- package/dist/dialogs/import.js +1 -2
- package/dist/dynamic-columns.d.ts +29 -0
- package/dist/dynamic-columns.d.ts.map +1 -0
- package/dist/dynamic-columns.js +312 -0
- package/dist/dynamic-crud-page.d.ts +49 -0
- package/dist/dynamic-crud-page.d.ts.map +1 -0
- package/dist/dynamic-crud-page.js +106 -0
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +36 -10
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/metadata-cache.js +3 -3
- package/dist/model-extension-registry.d.ts +19 -0
- package/dist/model-extension-registry.d.ts.map +1 -0
- package/dist/model-extension-registry.js +10 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/dialogs/export.tsx +1 -1
- package/src/dialogs/import.tsx +1 -2
- package/src/dynamic-columns.tsx +531 -0
- package/src/dynamic-crud-page.tsx +275 -0
- package/src/dynamic-table.tsx +34 -11
- package/src/index.ts +18 -0
- package/src/metadata-cache.ts +3 -3
- package/src/model-extension-registry.tsx +46 -0
- package/src/types.ts +3 -1
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// DynamicCRUDPage — drop-in route component for any model that has
|
|
2
|
+
// `DefineTable` metadata on the backend. Pulls the title and CRUD flag
|
|
3
|
+
// from `/metadata/table/<model>`, mounts <DynamicTable>, wires
|
|
4
|
+
// `<DynamicRecordDialog>` for create, and exposes `<ExportDialog>` /
|
|
5
|
+
// `<ImportDialog>` from a single toolbar.
|
|
6
|
+
//
|
|
7
|
+
// The whole thing exists so apps stop reinventing the same ~150 LOC of
|
|
8
|
+
// page chrome around DynamicTable. Override anything via props:
|
|
9
|
+
//
|
|
10
|
+
// <DynamicCRUDPage
|
|
11
|
+
// model="customers"
|
|
12
|
+
// endpoint="/dynamic/customers" // default: /dynamic/<model>
|
|
13
|
+
// title="Mis clientes" // default: metadata.title
|
|
14
|
+
// newLabel="Nuevo cliente" // default: "New <singular>"
|
|
15
|
+
// hideExport hideImport hideCreate // toolbar trimming
|
|
16
|
+
// headerExtras={<MyBranchSwitcher />} // injected before the title row
|
|
17
|
+
// toolbarExtras={<MyExtraActions />} // injected before "Nuevo X"
|
|
18
|
+
// i18n={{ refresh: 'Refrescar', export: 'Exportar', ... }}
|
|
19
|
+
// onChange={() => analytics.track('table.refresh')}
|
|
20
|
+
// />
|
|
21
|
+
//
|
|
22
|
+
// Apps that only need to swap the create endpoint or hide a button keep
|
|
23
|
+
// the boilerplate to one line. Apps that need richer per-model headers
|
|
24
|
+
// register a model-extension registry and feed it via `headerExtras`.
|
|
25
|
+
import React, {
|
|
26
|
+
useCallback,
|
|
27
|
+
useEffect,
|
|
28
|
+
useMemo,
|
|
29
|
+
useState,
|
|
30
|
+
} from 'react'
|
|
31
|
+
import { Plus, Download, Upload, RefreshCw } from 'lucide-react'
|
|
32
|
+
import { useApi } from './api-context'
|
|
33
|
+
import { useMetadataCache } from './metadata-cache'
|
|
34
|
+
import { DynamicTable } from './dynamic-table'
|
|
35
|
+
import { DynamicRecordDialog } from './dialogs/dynamic-record'
|
|
36
|
+
import { ExportDialog } from './dialogs/export'
|
|
37
|
+
import { ImportDialog } from './dialogs/import'
|
|
38
|
+
import { getModelExtension } from './model-extension-registry'
|
|
39
|
+
import type { TableMetadata } from './types'
|
|
40
|
+
|
|
41
|
+
export interface DynamicCRUDPageStrings {
|
|
42
|
+
refresh?: string
|
|
43
|
+
export?: string
|
|
44
|
+
import?: string
|
|
45
|
+
/** Used as the create button label when `newLabel` is not provided.
|
|
46
|
+
* Receives the singular form of the title. */
|
|
47
|
+
newPrefix?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const defaultStrings: Required<DynamicCRUDPageStrings> = {
|
|
51
|
+
refresh: 'Refresh',
|
|
52
|
+
export: 'Export',
|
|
53
|
+
import: 'Import',
|
|
54
|
+
newPrefix: 'New',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface DynamicCRUDPageClasses {
|
|
58
|
+
root?: string
|
|
59
|
+
container?: string
|
|
60
|
+
header?: string
|
|
61
|
+
title?: string
|
|
62
|
+
toolbar?: string
|
|
63
|
+
tableWrapper?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface DynamicCRUDPageProps {
|
|
67
|
+
/** Model key as registered on the backend (e.g. "customers"). */
|
|
68
|
+
model: string
|
|
69
|
+
/** Override the data endpoint. Defaults to `/dynamic/<model>`. */
|
|
70
|
+
endpoint?: string
|
|
71
|
+
/** Override the human title. Defaults to `metadata.title`. */
|
|
72
|
+
title?: string
|
|
73
|
+
/** Override the create button label. Defaults to `${newPrefix} ${singular}`. */
|
|
74
|
+
newLabel?: string
|
|
75
|
+
/** Strings used in default labels — pass when the host has its own i18n. */
|
|
76
|
+
i18n?: DynamicCRUDPageStrings
|
|
77
|
+
/** Hide the create button + dialog even when metadata says CRUD is enabled. */
|
|
78
|
+
hideCreate?: boolean
|
|
79
|
+
hideExport?: boolean
|
|
80
|
+
hideImport?: boolean
|
|
81
|
+
hideRefresh?: boolean
|
|
82
|
+
/** Slot rendered above the title row (e.g. branch switcher, kpi strip). */
|
|
83
|
+
headerExtras?: React.ReactNode
|
|
84
|
+
/** Slot rendered in the toolbar, before the create button. */
|
|
85
|
+
toolbarExtras?: React.ReactNode
|
|
86
|
+
/** Tailwind class overrides for layout primitives. */
|
|
87
|
+
classes?: DynamicCRUDPageClasses
|
|
88
|
+
/** Fired after a create/import/refresh successfully reloads the table. */
|
|
89
|
+
onChange?: () => void
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Page-level CRUD shell wired around <DynamicTable>. Hosts mount a single
|
|
94
|
+
* `<DynamicCRUDPage model="..." />` per route and the SDK takes care of
|
|
95
|
+
* metadata fetch, dialogs and toolbar.
|
|
96
|
+
*/
|
|
97
|
+
export function DynamicCRUDPage(props: DynamicCRUDPageProps) {
|
|
98
|
+
const {
|
|
99
|
+
model,
|
|
100
|
+
endpoint,
|
|
101
|
+
title: titleOverride,
|
|
102
|
+
newLabel,
|
|
103
|
+
i18n,
|
|
104
|
+
hideCreate,
|
|
105
|
+
hideExport,
|
|
106
|
+
hideImport,
|
|
107
|
+
hideRefresh,
|
|
108
|
+
headerExtras,
|
|
109
|
+
toolbarExtras,
|
|
110
|
+
classes,
|
|
111
|
+
onChange,
|
|
112
|
+
} = props
|
|
113
|
+
|
|
114
|
+
const strings = { ...defaultStrings, ...(i18n ?? {}) }
|
|
115
|
+
const dataEndpoint = endpoint ?? `/dynamic/${model}`
|
|
116
|
+
const ext = getModelExtension(model)
|
|
117
|
+
|
|
118
|
+
const api = useApi()
|
|
119
|
+
const cachedMeta = useMetadataCache((s) => s.getMetadata(model))
|
|
120
|
+
|
|
121
|
+
const [metadata, setMetadata] = useState<TableMetadata | null>(cachedMeta ?? null)
|
|
122
|
+
const [refreshKey, setRefreshKey] = useState(0)
|
|
123
|
+
const [openCreate, setOpenCreate] = useState(false)
|
|
124
|
+
const [openExport, setOpenExport] = useState(false)
|
|
125
|
+
const [openImport, setOpenImport] = useState(false)
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (cachedMeta) {
|
|
129
|
+
setMetadata(cachedMeta)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
let cancelled = false
|
|
133
|
+
api
|
|
134
|
+
.get(`/metadata/table/${model}`)
|
|
135
|
+
.then((res) => {
|
|
136
|
+
if (cancelled) return
|
|
137
|
+
const meta = (res.data?.data ?? res.data) as TableMetadata
|
|
138
|
+
setMetadata(meta ?? null)
|
|
139
|
+
})
|
|
140
|
+
.catch(() => {
|
|
141
|
+
if (!cancelled) setMetadata(null)
|
|
142
|
+
})
|
|
143
|
+
return () => {
|
|
144
|
+
cancelled = true
|
|
145
|
+
}
|
|
146
|
+
}, [model, cachedMeta, api])
|
|
147
|
+
|
|
148
|
+
const title = titleOverride ?? ext?.title ?? metadata?.title ?? model
|
|
149
|
+
const resolvedNewLabel = newLabel ?? ext?.newLabel
|
|
150
|
+
const singular = useMemo(() => {
|
|
151
|
+
const t = title.replace(/s$/i, '')
|
|
152
|
+
return t.charAt(0).toUpperCase() + t.slice(1)
|
|
153
|
+
}, [title])
|
|
154
|
+
|
|
155
|
+
const enableCRUD = metadata?.enableCRUDActions ?? false
|
|
156
|
+
const effectiveHideCreate = hideCreate || ext?.hideCreate
|
|
157
|
+
const effectiveHideExport = hideExport || ext?.hideExport
|
|
158
|
+
const effectiveHideImport = hideImport || ext?.hideImport
|
|
159
|
+
const effectiveHideRefresh = hideRefresh || ext?.hideRefresh
|
|
160
|
+
const showCreate = enableCRUD && !effectiveHideCreate
|
|
161
|
+
const showImport = enableCRUD && !effectiveHideImport
|
|
162
|
+
const showExport = !effectiveHideExport
|
|
163
|
+
const showRefresh = !effectiveHideRefresh
|
|
164
|
+
|
|
165
|
+
const handleRefresh = useCallback(() => {
|
|
166
|
+
setRefreshKey((k) => k + 1)
|
|
167
|
+
onChange?.()
|
|
168
|
+
}, [onChange])
|
|
169
|
+
|
|
170
|
+
const rootCls = classes?.root ?? 'flex flex-col h-full overflow-hidden'
|
|
171
|
+
const containerCls = classes?.container ?? 'flex flex-col flex-1 p-6 lg:p-8 gap-4 overflow-hidden'
|
|
172
|
+
const headerCls = classes?.header ?? 'flex items-center justify-between shrink-0'
|
|
173
|
+
const titleCls = classes?.title ?? 'text-2xl font-bold tracking-tight'
|
|
174
|
+
const toolbarCls = classes?.toolbar ?? 'flex items-center gap-2'
|
|
175
|
+
const tableWrapperCls = classes?.tableWrapper ?? 'flex-1 min-h-0'
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<div className={rootCls}>
|
|
179
|
+
<div className={containerCls}>
|
|
180
|
+
{ext?.headerExtras && <ext.headerExtras model={model} onRefresh={handleRefresh} />}
|
|
181
|
+
{headerExtras}
|
|
182
|
+
<div className={headerCls}>
|
|
183
|
+
{metadata ? (
|
|
184
|
+
<h1 className={titleCls}>{title}</h1>
|
|
185
|
+
) : (
|
|
186
|
+
<div className='h-8 w-48 bg-muted rounded animate-pulse' />
|
|
187
|
+
)}
|
|
188
|
+
<div className={toolbarCls}>
|
|
189
|
+
{showRefresh && (
|
|
190
|
+
<button
|
|
191
|
+
type='button'
|
|
192
|
+
onClick={handleRefresh}
|
|
193
|
+
aria-label={strings.refresh}
|
|
194
|
+
className='inline-flex items-center justify-center size-9 rounded-md border border-border bg-background hover:bg-accent text-foreground'
|
|
195
|
+
>
|
|
196
|
+
<RefreshCw className='size-4' />
|
|
197
|
+
</button>
|
|
198
|
+
)}
|
|
199
|
+
{metadata && showExport && (
|
|
200
|
+
<button
|
|
201
|
+
type='button'
|
|
202
|
+
onClick={() => setOpenExport(true)}
|
|
203
|
+
className='inline-flex items-center gap-2 h-9 px-3 rounded-md border border-border bg-background hover:bg-accent text-sm font-medium text-foreground'
|
|
204
|
+
>
|
|
205
|
+
<Download className='size-4' />
|
|
206
|
+
{strings.export}
|
|
207
|
+
</button>
|
|
208
|
+
)}
|
|
209
|
+
{metadata && showImport && (
|
|
210
|
+
<button
|
|
211
|
+
type='button'
|
|
212
|
+
onClick={() => setOpenImport(true)}
|
|
213
|
+
className='inline-flex items-center gap-2 h-9 px-3 rounded-md border border-border bg-background hover:bg-accent text-sm font-medium text-foreground'
|
|
214
|
+
>
|
|
215
|
+
<Upload className='size-4' />
|
|
216
|
+
{strings.import}
|
|
217
|
+
</button>
|
|
218
|
+
)}
|
|
219
|
+
{ext?.toolbarExtras && <ext.toolbarExtras model={model} onRefresh={handleRefresh} />}
|
|
220
|
+
{toolbarExtras}
|
|
221
|
+
{showCreate && (
|
|
222
|
+
<button
|
|
223
|
+
type='button'
|
|
224
|
+
onClick={() => setOpenCreate(true)}
|
|
225
|
+
className='inline-flex items-center gap-2 h-9 px-3 rounded-md bg-primary text-primary-foreground hover:opacity-90 text-sm font-medium'
|
|
226
|
+
>
|
|
227
|
+
<Plus className='size-4' />
|
|
228
|
+
{resolvedNewLabel ?? `${strings.newPrefix} ${singular}`}
|
|
229
|
+
</button>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<div className={tableWrapperCls}>
|
|
235
|
+
<DynamicTable
|
|
236
|
+
key={model}
|
|
237
|
+
model={model}
|
|
238
|
+
endpoint={dataEndpoint}
|
|
239
|
+
refreshTrigger={refreshKey}
|
|
240
|
+
/>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
{showCreate && (
|
|
245
|
+
<DynamicRecordDialog
|
|
246
|
+
open={openCreate}
|
|
247
|
+
onOpenChange={setOpenCreate}
|
|
248
|
+
mode='create'
|
|
249
|
+
model={model}
|
|
250
|
+
endpoint={dataEndpoint}
|
|
251
|
+
onSaved={handleRefresh}
|
|
252
|
+
/>
|
|
253
|
+
)}
|
|
254
|
+
|
|
255
|
+
{metadata && showExport && (
|
|
256
|
+
<ExportDialog
|
|
257
|
+
open={openExport}
|
|
258
|
+
onOpenChange={setOpenExport}
|
|
259
|
+
model={model}
|
|
260
|
+
metadata={metadata}
|
|
261
|
+
/>
|
|
262
|
+
)}
|
|
263
|
+
|
|
264
|
+
{metadata && showImport && (
|
|
265
|
+
<ImportDialog
|
|
266
|
+
open={openImport}
|
|
267
|
+
onOpenChange={setOpenImport}
|
|
268
|
+
model={model}
|
|
269
|
+
metadata={metadata}
|
|
270
|
+
onImported={handleRefresh}
|
|
271
|
+
/>
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
)
|
|
275
|
+
}
|
package/src/dynamic-table.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// DynamicTable — metadata-driven CRUD table used by every metacore host.
|
|
2
|
-
//
|
|
3
|
-
// for metacore packages + context-injected peer deps:
|
|
2
|
+
// Originally extracted from a host app and generalized so the host-specific
|
|
3
|
+
// aliases are swapped for metacore packages + context-injected peer deps:
|
|
4
4
|
// * `@/lib/api` → <ApiProvider> (see api-context.tsx)
|
|
5
5
|
// * `@/stores/branch-store` → <BranchProvider> (optional)
|
|
6
6
|
// * `@/stores/metadata-cache` → internal ./metadata-cache zustand store
|
|
@@ -65,6 +65,7 @@ import { Progress } from './dialogs/_primitives'
|
|
|
65
65
|
import { useMetadataCache } from './metadata-cache'
|
|
66
66
|
import { useApi, useCurrentBranch } from './api-context'
|
|
67
67
|
import type { ColumnFilterConfig, GetDynamicColumns } from './dynamic-columns-shim'
|
|
68
|
+
import { defaultGetDynamicColumns } from './dynamic-columns'
|
|
68
69
|
import { OptionsContext } from './options-context'
|
|
69
70
|
import { ActionModalDispatcher } from './action-modal-dispatcher'
|
|
70
71
|
import type { TableMetadata, ApiResponse, ActionMetadata } from './types'
|
|
@@ -477,8 +478,12 @@ export function DynamicTable({
|
|
|
477
478
|
|
|
478
479
|
const columnFilterConfigs = useMemo(() => {
|
|
479
480
|
const map = new Map<string, ColumnFilterConfig>()
|
|
480
|
-
if (!metadata
|
|
481
|
-
|
|
481
|
+
if (!metadata) return map
|
|
482
|
+
// Explicit `metadata.filters` wins. When the backend does not emit
|
|
483
|
+
// them, derive a filter chip from every column flagged
|
|
484
|
+
// `filterable: true` — keeps the kernel API minimal (one flag on the
|
|
485
|
+
// column) while still rendering the FilterableColumnHeader.
|
|
486
|
+
for (const f of metadata.filters ?? []) {
|
|
482
487
|
const fType = f.type as ColumnFilterConfig['filterType']
|
|
483
488
|
let options: { label: string; value: string; icon?: string; color?: string }[] = []
|
|
484
489
|
if (f.options && f.options.length > 0) {
|
|
@@ -498,6 +503,31 @@ export function DynamicTable({
|
|
|
498
503
|
searchEndpoint: f.searchEndpoint,
|
|
499
504
|
})
|
|
500
505
|
}
|
|
506
|
+
for (const c of metadata.columns ?? []) {
|
|
507
|
+
if (!c.filterable || map.has(c.key)) continue
|
|
508
|
+
const hasStaticOptions = (c.options?.length ?? 0) > 0
|
|
509
|
+
const hasEndpoint = !!c.searchEndpoint
|
|
510
|
+
if (!hasStaticOptions && !hasEndpoint && c.type !== 'boolean') continue
|
|
511
|
+
const options = hasStaticOptions
|
|
512
|
+
? c.options!.map(o => ({
|
|
513
|
+
label: o.label,
|
|
514
|
+
value: String(o.value),
|
|
515
|
+
icon: o.icon,
|
|
516
|
+
color: o.color,
|
|
517
|
+
}))
|
|
518
|
+
: hasEndpoint && filterOptionsMap.has(c.searchEndpoint!)
|
|
519
|
+
? filterOptionsMap.get(c.searchEndpoint!) || []
|
|
520
|
+
: []
|
|
521
|
+
map.set(c.key, {
|
|
522
|
+
filterType: 'select',
|
|
523
|
+
filterKey: c.key,
|
|
524
|
+
options,
|
|
525
|
+
selectedValues: dynamicFilters[c.key] || [],
|
|
526
|
+
onFilterChange: handleDynamicFilterChange,
|
|
527
|
+
loading: hasEndpoint && !filterOptionsMap.has(c.searchEndpoint!),
|
|
528
|
+
searchEndpoint: c.searchEndpoint,
|
|
529
|
+
})
|
|
530
|
+
}
|
|
501
531
|
return map
|
|
502
532
|
}, [metadata, filterOptionsMap, dynamicFilters, handleDynamicFilterChange])
|
|
503
533
|
|
|
@@ -757,10 +787,3 @@ export function DynamicTable({
|
|
|
757
787
|
)
|
|
758
788
|
}
|
|
759
789
|
|
|
760
|
-
/** Sensible default when hosts don't provide their own getDynamicColumns. */
|
|
761
|
-
const defaultGetDynamicColumns: GetDynamicColumns = (metadata, _handleAction, _t, _lang, _filters) =>
|
|
762
|
-
(metadata.columns ?? []).map((col: any) => ({
|
|
763
|
-
accessorKey: col.name,
|
|
764
|
-
header: col.label ?? col.name,
|
|
765
|
-
enableSorting: col.sortable ?? false,
|
|
766
|
-
}))
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,24 @@ export type {
|
|
|
25
25
|
GetDynamicColumns,
|
|
26
26
|
DynamicIconComponent,
|
|
27
27
|
} from './dynamic-columns-shim'
|
|
28
|
+
export {
|
|
29
|
+
defaultGetDynamicColumns,
|
|
30
|
+
makeDefaultGetDynamicColumns,
|
|
31
|
+
type DynamicColumnsHelpers,
|
|
32
|
+
} from './dynamic-columns'
|
|
28
33
|
export { DynamicRecordDialog } from './dialogs/dynamic-record'
|
|
29
34
|
export { ExportDialog } from './dialogs/export'
|
|
30
35
|
export { ImportDialog } from './dialogs/import'
|
|
36
|
+
export {
|
|
37
|
+
DynamicCRUDPage,
|
|
38
|
+
type DynamicCRUDPageProps,
|
|
39
|
+
type DynamicCRUDPageStrings,
|
|
40
|
+
type DynamicCRUDPageClasses,
|
|
41
|
+
} from './dynamic-crud-page'
|
|
42
|
+
export {
|
|
43
|
+
registerModelExtension,
|
|
44
|
+
getModelExtension,
|
|
45
|
+
clearModelExtensions,
|
|
46
|
+
type ModelExtension,
|
|
47
|
+
type ModelExtensionProps,
|
|
48
|
+
} from './model-extension-registry'
|
package/src/metadata-cache.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Metadata cache — a zustand store that memoizes table/modal metadata
|
|
2
|
-
// responses across dynamic-table mounts.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// responses across dynamic-table mounts. Generalized from a host-app
|
|
3
|
+
// metadata-cache store so the runtime-react package no longer depends on
|
|
4
|
+
// a host-specific alias.
|
|
5
5
|
//
|
|
6
6
|
// The prefetchAll() method needs an `api` client (axios-like); we keep that
|
|
7
7
|
// as an injectable parameter so the store stays host-agnostic. If a caller
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Per-model UI extension registry. Apps register once at boot and
|
|
2
|
+
// `<DynamicCRUDPage>` composes the registered extension automatically — no
|
|
3
|
+
// per-route forking, no copy-paste of the page shell.
|
|
4
|
+
//
|
|
5
|
+
// import { registerModelExtension } from '@asteby/metacore-runtime-react'
|
|
6
|
+
//
|
|
7
|
+
// registerModelExtension('customers', {
|
|
8
|
+
// headerExtras: CustomersKpiStrip,
|
|
9
|
+
// toolbarExtras: BulkAssignButton,
|
|
10
|
+
// hideImport: true,
|
|
11
|
+
// })
|
|
12
|
+
//
|
|
13
|
+
// The registry is a module-level singleton — intentional. A React context
|
|
14
|
+
// would force every consumer to wrap a provider just to read a static
|
|
15
|
+
// configuration that is set once at app boot.
|
|
16
|
+
import * as React from 'react'
|
|
17
|
+
|
|
18
|
+
export interface ModelExtensionProps {
|
|
19
|
+
model: string
|
|
20
|
+
onRefresh: () => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ModelExtension {
|
|
24
|
+
headerExtras?: React.ComponentType<ModelExtensionProps>
|
|
25
|
+
toolbarExtras?: React.ComponentType<ModelExtensionProps>
|
|
26
|
+
hideCreate?: boolean
|
|
27
|
+
hideExport?: boolean
|
|
28
|
+
hideImport?: boolean
|
|
29
|
+
hideRefresh?: boolean
|
|
30
|
+
title?: string
|
|
31
|
+
newLabel?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const registry = new Map<string, ModelExtension>()
|
|
35
|
+
|
|
36
|
+
export function registerModelExtension(model: string, ext: ModelExtension): void {
|
|
37
|
+
registry.set(model, ext)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getModelExtension(model: string): ModelExtension | undefined {
|
|
41
|
+
return registry.get(model)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function clearModelExtensions(): void {
|
|
45
|
+
registry.clear()
|
|
46
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Shared metadata shape consumed by every host. Some hosts add a `link`
|
|
2
|
+
// action type with a `linkUrl` template — represented here as part of the
|
|
3
|
+
// `type` union so the SDK can render it uniformly.
|
|
2
4
|
export interface TableMetadata {
|
|
3
5
|
title: string
|
|
4
6
|
endpoint: string
|