@asteby/metacore-runtime-react 4.0.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 +31 -0
- package/LICENSE +201 -0
- package/README.md +59 -0
- package/dist/action-modal-dispatcher.d.ts +4 -0
- package/dist/action-modal-dispatcher.d.ts.map +1 -0
- package/dist/action-modal-dispatcher.js +123 -0
- package/dist/addon-loader.d.ts +27 -0
- package/dist/addon-loader.d.ts.map +1 -0
- package/dist/addon-loader.js +73 -0
- package/dist/api-context.d.ts +40 -0
- package/dist/api-context.d.ts.map +1 -0
- package/dist/api-context.js +25 -0
- package/dist/capability-gate.d.ts +29 -0
- package/dist/capability-gate.d.ts.map +1 -0
- package/dist/capability-gate.js +43 -0
- package/dist/dialogs/_primitives.d.ts +29 -0
- package/dist/dialogs/_primitives.d.ts.map +1 -0
- package/dist/dialogs/_primitives.js +35 -0
- package/dist/dialogs/dynamic-record.d.ts +11 -0
- package/dist/dialogs/dynamic-record.d.ts.map +1 -0
- package/dist/dialogs/dynamic-record.js +377 -0
- package/dist/dialogs/export.d.ts +12 -0
- package/dist/dialogs/export.d.ts.map +1 -0
- package/dist/dialogs/export.js +146 -0
- package/dist/dialogs/import.d.ts +11 -0
- package/dist/dialogs/import.d.ts.map +1 -0
- package/dist/dialogs/import.js +128 -0
- package/dist/dynamic-columns-shim.d.ts +25 -0
- package/dist/dynamic-columns-shim.d.ts.map +1 -0
- package/dist/dynamic-columns-shim.js +1 -0
- package/dist/dynamic-form.d.ts +12 -0
- package/dist/dynamic-form.d.ts.map +1 -0
- package/dist/dynamic-form.js +51 -0
- package/dist/dynamic-icon.d.ts +6 -0
- package/dist/dynamic-icon.d.ts.map +1 -0
- package/dist/dynamic-icon.js +11 -0
- package/dist/dynamic-table.d.ts +22 -0
- package/dist/dynamic-table.d.ts.map +1 -0
- package/dist/dynamic-table.js +516 -0
- package/dist/i18n-provider.d.ts +16 -0
- package/dist/i18n-provider.d.ts.map +1 -0
- package/dist/i18n-provider.js +16 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/metadata-cache.d.ts +42 -0
- package/dist/metadata-cache.d.ts.map +1 -0
- package/dist/metadata-cache.js +71 -0
- package/dist/navigation-builder.d.ts +34 -0
- package/dist/navigation-builder.d.ts.map +1 -0
- package/dist/navigation-builder.js +45 -0
- package/dist/options-context.d.ts +8 -0
- package/dist/options-context.d.ts.map +1 -0
- package/dist/options-context.js +5 -0
- package/dist/slot.d.ts +32 -0
- package/dist/slot.d.ts.map +1 -0
- package/dist/slot.js +45 -0
- package/dist/types.d.ts +114 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +67 -0
- package/src/action-modal-dispatcher.tsx +275 -0
- package/src/addon-loader.tsx +111 -0
- package/src/api-context.tsx +55 -0
- package/src/capability-gate.tsx +69 -0
- package/src/dialogs/_primitives.tsx +114 -0
- package/src/dialogs/dynamic-record.tsx +770 -0
- package/src/dialogs/export.tsx +339 -0
- package/src/dialogs/import.tsx +404 -0
- package/src/dynamic-columns-shim.ts +36 -0
- package/src/dynamic-form.tsx +108 -0
- package/src/dynamic-icon.tsx +15 -0
- package/src/dynamic-table.tsx +766 -0
- package/src/i18n-provider.tsx +33 -0
- package/src/index.ts +30 -0
- package/src/metadata-cache.ts +103 -0
- package/src/navigation-builder.tsx +77 -0
- package/src/options-context.tsx +11 -0
- package/src/slot.tsx +77 -0
- package/src/types.ts +112 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
// ExportDialog — lets users pick format (csv/json) + columns and kicks off
|
|
2
|
+
// either a sync download or an async export job (polled via /exports/:id/status).
|
|
3
|
+
// Ported from the ops starter. Axios-like client is provided by <ApiProvider>.
|
|
4
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
5
|
+
import {
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
DialogDescription,
|
|
11
|
+
DialogFooter,
|
|
12
|
+
Button,
|
|
13
|
+
Label,
|
|
14
|
+
Checkbox,
|
|
15
|
+
Collapsible,
|
|
16
|
+
CollapsibleContent,
|
|
17
|
+
CollapsibleTrigger,
|
|
18
|
+
} from '@asteby/metacore-ui/primitives'
|
|
19
|
+
import { Progress, RadioGroup, RadioGroupItem } from './_primitives'
|
|
20
|
+
import { toast } from 'sonner'
|
|
21
|
+
import { Download, ChevronDown, Loader2 } from 'lucide-react'
|
|
22
|
+
import type { TableMetadata } from '../types'
|
|
23
|
+
import { useApi } from '../api-context'
|
|
24
|
+
|
|
25
|
+
interface ExportDialogProps {
|
|
26
|
+
open: boolean
|
|
27
|
+
onOpenChange: (open: boolean) => void
|
|
28
|
+
model: string
|
|
29
|
+
metadata: TableMetadata
|
|
30
|
+
currentFilters?: Record<string, any>
|
|
31
|
+
hasActiveFilters?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function ExportDialog({
|
|
35
|
+
open,
|
|
36
|
+
onOpenChange,
|
|
37
|
+
model,
|
|
38
|
+
metadata,
|
|
39
|
+
currentFilters,
|
|
40
|
+
hasActiveFilters,
|
|
41
|
+
}: ExportDialogProps) {
|
|
42
|
+
const api = useApi()
|
|
43
|
+
const [format, setFormat] = useState<'csv' | 'json'>('csv')
|
|
44
|
+
const [exportAll, setExportAll] = useState(false)
|
|
45
|
+
const [selectedColumns, setSelectedColumns] = useState<string[]>([])
|
|
46
|
+
const [columnsOpen, setColumnsOpen] = useState(false)
|
|
47
|
+
const [exporting, setExporting] = useState(false)
|
|
48
|
+
const [progress, setProgress] = useState(0)
|
|
49
|
+
const [asyncJobId, setAsyncJobId] = useState<string | null>(null)
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (open && metadata?.columns) {
|
|
53
|
+
setSelectedColumns(
|
|
54
|
+
metadata.columns
|
|
55
|
+
.filter(col => !col.hidden)
|
|
56
|
+
.map(col => col.key)
|
|
57
|
+
)
|
|
58
|
+
setFormat('csv')
|
|
59
|
+
setExportAll(false)
|
|
60
|
+
setColumnsOpen(false)
|
|
61
|
+
setExporting(false)
|
|
62
|
+
setProgress(0)
|
|
63
|
+
setAsyncJobId(null)
|
|
64
|
+
}
|
|
65
|
+
}, [open, metadata])
|
|
66
|
+
|
|
67
|
+
const toggleColumn = useCallback((key: string) => {
|
|
68
|
+
setSelectedColumns((prev: string[]) =>
|
|
69
|
+
prev.includes(key)
|
|
70
|
+
? prev.filter((k: string) => k !== key)
|
|
71
|
+
: [...prev, key]
|
|
72
|
+
)
|
|
73
|
+
}, [])
|
|
74
|
+
|
|
75
|
+
const toggleAllColumns = useCallback(() => {
|
|
76
|
+
const visibleKeys = metadata.columns
|
|
77
|
+
.filter(col => !col.hidden)
|
|
78
|
+
.map(col => col.key)
|
|
79
|
+
|
|
80
|
+
if (selectedColumns.length === visibleKeys.length) {
|
|
81
|
+
setSelectedColumns([])
|
|
82
|
+
} else {
|
|
83
|
+
setSelectedColumns(visibleKeys)
|
|
84
|
+
}
|
|
85
|
+
}, [metadata, selectedColumns])
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!asyncJobId) return
|
|
89
|
+
|
|
90
|
+
const interval = setInterval(async () => {
|
|
91
|
+
try {
|
|
92
|
+
const res = await api.get(`/exports/${asyncJobId}/status`)
|
|
93
|
+
const status = res.data?.data ?? res.data
|
|
94
|
+
|
|
95
|
+
if (status.progress !== undefined) {
|
|
96
|
+
setProgress(status.progress)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (status.status === 'completed') {
|
|
100
|
+
clearInterval(interval)
|
|
101
|
+
const downloadRes = await api.get(
|
|
102
|
+
`/exports/${asyncJobId}/download`,
|
|
103
|
+
{ responseType: 'blob' }
|
|
104
|
+
)
|
|
105
|
+
triggerDownload(downloadRes.data, format)
|
|
106
|
+
setExporting(false)
|
|
107
|
+
setAsyncJobId(null)
|
|
108
|
+
toast.success('Exportación completada')
|
|
109
|
+
onOpenChange(false)
|
|
110
|
+
} else if (status.status === 'failed') {
|
|
111
|
+
clearInterval(interval)
|
|
112
|
+
setExporting(false)
|
|
113
|
+
setAsyncJobId(null)
|
|
114
|
+
toast.error(status.error_message || 'Error al exportar')
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
clearInterval(interval)
|
|
118
|
+
setExporting(false)
|
|
119
|
+
setAsyncJobId(null)
|
|
120
|
+
toast.error('Error al verificar el estado de la exportación')
|
|
121
|
+
}
|
|
122
|
+
}, 2000)
|
|
123
|
+
|
|
124
|
+
return () => clearInterval(interval)
|
|
125
|
+
}, [asyncJobId, format, onOpenChange, api])
|
|
126
|
+
|
|
127
|
+
const triggerDownload = (blob: Blob, fmt: string) => {
|
|
128
|
+
const url = window.URL.createObjectURL(blob)
|
|
129
|
+
const link = document.createElement('a')
|
|
130
|
+
link.href = url
|
|
131
|
+
link.download = `${model}-export.${fmt === 'json' ? 'json' : 'csv'}`
|
|
132
|
+
document.body.appendChild(link)
|
|
133
|
+
link.click()
|
|
134
|
+
document.body.removeChild(link)
|
|
135
|
+
window.URL.revokeObjectURL(url)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const handleExport = async () => {
|
|
139
|
+
if (selectedColumns.length === 0) {
|
|
140
|
+
toast.error('Selecciona al menos una columna para exportar')
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
setExporting(true)
|
|
145
|
+
setProgress(0)
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const params: Record<string, any> = {
|
|
149
|
+
format,
|
|
150
|
+
columns: selectedColumns.join(','),
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!exportAll && currentFilters) {
|
|
154
|
+
Object.entries(currentFilters).forEach(([key, value]) => {
|
|
155
|
+
if (value !== undefined && value !== '') {
|
|
156
|
+
params[key] = value
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const response = await api.get(`/data/${model}/export`, {
|
|
162
|
+
params,
|
|
163
|
+
responseType: 'blob',
|
|
164
|
+
validateStatus: () => true,
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
const contentType = response.headers?.['content-type'] || ''
|
|
168
|
+
|
|
169
|
+
if (contentType.includes('application/json')) {
|
|
170
|
+
const text = await response.data.text()
|
|
171
|
+
const json = JSON.parse(text)
|
|
172
|
+
|
|
173
|
+
if (json.async && json.job_id) {
|
|
174
|
+
setAsyncJobId(json.job_id)
|
|
175
|
+
setProgress(10)
|
|
176
|
+
toast.info(`Exportando ${json.total} registros...`)
|
|
177
|
+
} else if (!json.success) {
|
|
178
|
+
setExporting(false)
|
|
179
|
+
toast.error(json.message || 'Error al exportar')
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
triggerDownload(response.data, format)
|
|
183
|
+
setExporting(false)
|
|
184
|
+
toast.success('Exportación completada')
|
|
185
|
+
onOpenChange(false)
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
setExporting(false)
|
|
189
|
+
toast.error('Error al exportar los datos')
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const visibleColumns = metadata?.columns?.filter(col => !col.hidden) ?? []
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
197
|
+
<DialogContent className="sm:max-w-md max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden">
|
|
198
|
+
<DialogHeader className="p-6 pb-4 border-b shrink-0">
|
|
199
|
+
<DialogTitle>Exportar {metadata.title}</DialogTitle>
|
|
200
|
+
<DialogDescription>
|
|
201
|
+
Selecciona el formato y las columnas a exportar.
|
|
202
|
+
</DialogDescription>
|
|
203
|
+
</DialogHeader>
|
|
204
|
+
|
|
205
|
+
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
|
206
|
+
{exporting ? (
|
|
207
|
+
<div className="space-y-4">
|
|
208
|
+
<p className="text-sm text-muted-foreground text-center">
|
|
209
|
+
Exportando datos...
|
|
210
|
+
</p>
|
|
211
|
+
<Progress value={progress} />
|
|
212
|
+
<p className="text-xs text-muted-foreground text-center">
|
|
213
|
+
{progress > 0 ? `${Math.round(progress)}%` : 'Preparando...'}
|
|
214
|
+
</p>
|
|
215
|
+
</div>
|
|
216
|
+
) : (
|
|
217
|
+
<>
|
|
218
|
+
<div className="space-y-3">
|
|
219
|
+
<Label className="text-sm font-medium">Formato</Label>
|
|
220
|
+
<RadioGroup
|
|
221
|
+
value={format}
|
|
222
|
+
onValueChange={(val: string) => setFormat(val as 'csv' | 'json')}
|
|
223
|
+
className="flex gap-4"
|
|
224
|
+
>
|
|
225
|
+
<div className="flex items-center gap-2">
|
|
226
|
+
<RadioGroupItem value="csv" id="format-csv" />
|
|
227
|
+
<Label htmlFor="format-csv" className="font-normal cursor-pointer">
|
|
228
|
+
CSV
|
|
229
|
+
</Label>
|
|
230
|
+
</div>
|
|
231
|
+
<div className="flex items-center gap-2">
|
|
232
|
+
<RadioGroupItem value="json" id="format-json" />
|
|
233
|
+
<Label htmlFor="format-json" className="font-normal cursor-pointer">
|
|
234
|
+
JSON
|
|
235
|
+
</Label>
|
|
236
|
+
</div>
|
|
237
|
+
</RadioGroup>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
{hasActiveFilters && (
|
|
241
|
+
<div className="flex items-center gap-2">
|
|
242
|
+
<Checkbox
|
|
243
|
+
id="export-all"
|
|
244
|
+
checked={exportAll}
|
|
245
|
+
onCheckedChange={(checked) =>
|
|
246
|
+
setExportAll(checked === true)
|
|
247
|
+
}
|
|
248
|
+
/>
|
|
249
|
+
<Label
|
|
250
|
+
htmlFor="export-all"
|
|
251
|
+
className="font-normal cursor-pointer text-sm"
|
|
252
|
+
>
|
|
253
|
+
Exportar todos los registros (ignorar filtros)
|
|
254
|
+
</Label>
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
|
|
258
|
+
<Collapsible open={columnsOpen} onOpenChange={setColumnsOpen}>
|
|
259
|
+
<CollapsibleTrigger asChild>
|
|
260
|
+
<Button
|
|
261
|
+
variant="ghost"
|
|
262
|
+
size="sm"
|
|
263
|
+
className="w-full justify-between px-0 hover:bg-transparent"
|
|
264
|
+
>
|
|
265
|
+
<span className="text-sm font-medium">
|
|
266
|
+
Columnas ({selectedColumns.length}/{visibleColumns.length})
|
|
267
|
+
</span>
|
|
268
|
+
<ChevronDown
|
|
269
|
+
className={`h-4 w-4 transition-transform ${columnsOpen ? 'rotate-180' : ''}`}
|
|
270
|
+
/>
|
|
271
|
+
</Button>
|
|
272
|
+
</CollapsibleTrigger>
|
|
273
|
+
<CollapsibleContent className="space-y-2 pt-2">
|
|
274
|
+
<div className="flex items-center gap-2 pb-2 border-b">
|
|
275
|
+
<Checkbox
|
|
276
|
+
id="select-all-columns"
|
|
277
|
+
checked={
|
|
278
|
+
selectedColumns.length === visibleColumns.length
|
|
279
|
+
}
|
|
280
|
+
onCheckedChange={toggleAllColumns}
|
|
281
|
+
/>
|
|
282
|
+
<Label
|
|
283
|
+
htmlFor="select-all-columns"
|
|
284
|
+
className="font-normal cursor-pointer text-sm"
|
|
285
|
+
>
|
|
286
|
+
Seleccionar todas
|
|
287
|
+
</Label>
|
|
288
|
+
</div>
|
|
289
|
+
<div className="grid grid-cols-2 gap-2 max-h-48 overflow-y-auto">
|
|
290
|
+
{visibleColumns.map(col => (
|
|
291
|
+
<div
|
|
292
|
+
key={col.key}
|
|
293
|
+
className="flex items-center gap-2"
|
|
294
|
+
>
|
|
295
|
+
<Checkbox
|
|
296
|
+
id={`col-${col.key}`}
|
|
297
|
+
checked={selectedColumns.includes(col.key)}
|
|
298
|
+
onCheckedChange={() => toggleColumn(col.key)}
|
|
299
|
+
/>
|
|
300
|
+
<Label
|
|
301
|
+
htmlFor={`col-${col.key}`}
|
|
302
|
+
className="font-normal cursor-pointer text-sm truncate"
|
|
303
|
+
>
|
|
304
|
+
{col.label}
|
|
305
|
+
</Label>
|
|
306
|
+
</div>
|
|
307
|
+
))}
|
|
308
|
+
</div>
|
|
309
|
+
</CollapsibleContent>
|
|
310
|
+
</Collapsible>
|
|
311
|
+
</>
|
|
312
|
+
)}
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
<DialogFooter className="p-4 border-t shrink-0">
|
|
316
|
+
<Button
|
|
317
|
+
variant="outline"
|
|
318
|
+
onClick={() => onOpenChange(false)}
|
|
319
|
+
disabled={exporting}
|
|
320
|
+
>
|
|
321
|
+
Cancelar
|
|
322
|
+
</Button>
|
|
323
|
+
{!exporting && (
|
|
324
|
+
<Button onClick={handleExport} disabled={selectedColumns.length === 0}>
|
|
325
|
+
<Download className="h-4 w-4 mr-1" />
|
|
326
|
+
Exportar
|
|
327
|
+
</Button>
|
|
328
|
+
)}
|
|
329
|
+
{exporting && (
|
|
330
|
+
<Button disabled>
|
|
331
|
+
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
|
332
|
+
Exportando...
|
|
333
|
+
</Button>
|
|
334
|
+
)}
|
|
335
|
+
</DialogFooter>
|
|
336
|
+
</DialogContent>
|
|
337
|
+
</Dialog>
|
|
338
|
+
)
|
|
339
|
+
}
|