@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.
Files changed (81) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +201 -0
  3. package/README.md +59 -0
  4. package/dist/action-modal-dispatcher.d.ts +4 -0
  5. package/dist/action-modal-dispatcher.d.ts.map +1 -0
  6. package/dist/action-modal-dispatcher.js +123 -0
  7. package/dist/addon-loader.d.ts +27 -0
  8. package/dist/addon-loader.d.ts.map +1 -0
  9. package/dist/addon-loader.js +73 -0
  10. package/dist/api-context.d.ts +40 -0
  11. package/dist/api-context.d.ts.map +1 -0
  12. package/dist/api-context.js +25 -0
  13. package/dist/capability-gate.d.ts +29 -0
  14. package/dist/capability-gate.d.ts.map +1 -0
  15. package/dist/capability-gate.js +43 -0
  16. package/dist/dialogs/_primitives.d.ts +29 -0
  17. package/dist/dialogs/_primitives.d.ts.map +1 -0
  18. package/dist/dialogs/_primitives.js +35 -0
  19. package/dist/dialogs/dynamic-record.d.ts +11 -0
  20. package/dist/dialogs/dynamic-record.d.ts.map +1 -0
  21. package/dist/dialogs/dynamic-record.js +377 -0
  22. package/dist/dialogs/export.d.ts +12 -0
  23. package/dist/dialogs/export.d.ts.map +1 -0
  24. package/dist/dialogs/export.js +146 -0
  25. package/dist/dialogs/import.d.ts +11 -0
  26. package/dist/dialogs/import.d.ts.map +1 -0
  27. package/dist/dialogs/import.js +128 -0
  28. package/dist/dynamic-columns-shim.d.ts +25 -0
  29. package/dist/dynamic-columns-shim.d.ts.map +1 -0
  30. package/dist/dynamic-columns-shim.js +1 -0
  31. package/dist/dynamic-form.d.ts +12 -0
  32. package/dist/dynamic-form.d.ts.map +1 -0
  33. package/dist/dynamic-form.js +51 -0
  34. package/dist/dynamic-icon.d.ts +6 -0
  35. package/dist/dynamic-icon.d.ts.map +1 -0
  36. package/dist/dynamic-icon.js +11 -0
  37. package/dist/dynamic-table.d.ts +22 -0
  38. package/dist/dynamic-table.d.ts.map +1 -0
  39. package/dist/dynamic-table.js +516 -0
  40. package/dist/i18n-provider.d.ts +16 -0
  41. package/dist/i18n-provider.d.ts.map +1 -0
  42. package/dist/i18n-provider.js +16 -0
  43. package/dist/index.d.ts +18 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +21 -0
  46. package/dist/metadata-cache.d.ts +42 -0
  47. package/dist/metadata-cache.d.ts.map +1 -0
  48. package/dist/metadata-cache.js +71 -0
  49. package/dist/navigation-builder.d.ts +34 -0
  50. package/dist/navigation-builder.d.ts.map +1 -0
  51. package/dist/navigation-builder.js +45 -0
  52. package/dist/options-context.d.ts +8 -0
  53. package/dist/options-context.d.ts.map +1 -0
  54. package/dist/options-context.js +5 -0
  55. package/dist/slot.d.ts +32 -0
  56. package/dist/slot.d.ts.map +1 -0
  57. package/dist/slot.js +45 -0
  58. package/dist/types.d.ts +114 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +1 -0
  61. package/package.json +67 -0
  62. package/src/action-modal-dispatcher.tsx +275 -0
  63. package/src/addon-loader.tsx +111 -0
  64. package/src/api-context.tsx +55 -0
  65. package/src/capability-gate.tsx +69 -0
  66. package/src/dialogs/_primitives.tsx +114 -0
  67. package/src/dialogs/dynamic-record.tsx +770 -0
  68. package/src/dialogs/export.tsx +339 -0
  69. package/src/dialogs/import.tsx +404 -0
  70. package/src/dynamic-columns-shim.ts +36 -0
  71. package/src/dynamic-form.tsx +108 -0
  72. package/src/dynamic-icon.tsx +15 -0
  73. package/src/dynamic-table.tsx +766 -0
  74. package/src/i18n-provider.tsx +33 -0
  75. package/src/index.ts +30 -0
  76. package/src/metadata-cache.ts +103 -0
  77. package/src/navigation-builder.tsx +77 -0
  78. package/src/options-context.tsx +11 -0
  79. package/src/slot.tsx +77 -0
  80. package/src/types.ts +112 -0
  81. 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
+ }