@postxl/generators 1.13.0 → 1.15.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 (117) hide show
  1. package/dist/backend-authentication/generators/authentication-service.generator.js +1 -1
  2. package/dist/backend-core/backend.generator.js +0 -4
  3. package/dist/backend-core/backend.generator.js.map +1 -1
  4. package/dist/backend-core/modules/backend-module-xlport.generator.js +1 -1
  5. package/dist/backend-excel-io/excel-io.generator.d.ts +19 -0
  6. package/dist/backend-excel-io/excel-io.generator.js +106 -0
  7. package/dist/backend-excel-io/excel-io.generator.js.map +1 -0
  8. package/dist/backend-excel-io/generators/excel-io-service.generator.d.ts +9 -0
  9. package/dist/backend-excel-io/generators/excel-io-service.generator.js +680 -0
  10. package/dist/backend-excel-io/generators/excel-io-service.generator.js.map +1 -0
  11. package/dist/backend-excel-io/index.d.ts +2 -0
  12. package/dist/backend-excel-io/index.js +22 -0
  13. package/dist/backend-excel-io/index.js.map +1 -0
  14. package/dist/backend-excel-io/template/README.md +24 -0
  15. package/dist/backend-excel-io/template/excel-io.controller.ts +195 -0
  16. package/dist/backend-excel-io/template/excel-io.module.ts +17 -0
  17. package/dist/backend-import/generators/detect-delta/detect-delta-functions.generator.js +148 -13
  18. package/dist/backend-import/generators/detect-delta/detect-delta-functions.generator.js.map +1 -1
  19. package/dist/backend-import/generators/filter-error-rows.generator.d.ts +2 -0
  20. package/dist/backend-import/generators/filter-error-rows.generator.js +28 -0
  21. package/dist/backend-import/generators/filter-error-rows.generator.js.map +1 -0
  22. package/dist/backend-import/generators/import-service.generator.js +126 -2
  23. package/dist/backend-import/generators/import-service.generator.js.map +1 -1
  24. package/dist/backend-import/import.generator.js +2 -0
  25. package/dist/backend-import/import.generator.js.map +1 -1
  26. package/dist/backend-repositories/generators/model-repository.generator.js +17 -2
  27. package/dist/backend-repositories/generators/model-repository.generator.js.map +1 -1
  28. package/dist/backend-router-trpc/generators/app-routes.generator.js +5 -0
  29. package/dist/backend-router-trpc/generators/app-routes.generator.js.map +1 -1
  30. package/dist/backend-router-trpc/generators/excel-io-route.generator.d.ts +4 -0
  31. package/dist/backend-router-trpc/generators/excel-io-route.generator.js +35 -0
  32. package/dist/backend-router-trpc/generators/excel-io-route.generator.js.map +1 -0
  33. package/dist/backend-router-trpc/generators/trpc-plugin.generator.js +6 -0
  34. package/dist/backend-router-trpc/generators/trpc-plugin.generator.js.map +1 -1
  35. package/dist/backend-router-trpc/generators/trpc-router-module.generator.js +8 -1
  36. package/dist/backend-router-trpc/generators/trpc-router-module.generator.js.map +1 -1
  37. package/dist/backend-router-trpc/generators/trpc-shared.generator.js +9 -0
  38. package/dist/backend-router-trpc/generators/trpc-shared.generator.js.map +1 -1
  39. package/dist/backend-router-trpc/router-trpc.generator.d.ts +2 -1
  40. package/dist/backend-router-trpc/router-trpc.generator.js +4 -0
  41. package/dist/backend-router-trpc/router-trpc.generator.js.map +1 -1
  42. package/dist/backend-update/model-update-service.generator.js +54 -0
  43. package/dist/backend-update/model-update-service.generator.js.map +1 -1
  44. package/dist/backend-update/update-actions.decoders.d.ts +2 -0
  45. package/dist/backend-update/update-actions.decoders.js +2 -0
  46. package/dist/backend-update/update-actions.decoders.js.map +1 -1
  47. package/dist/backend-update/update.generator.js +6 -0
  48. package/dist/backend-update/update.generator.js.map +1 -1
  49. package/dist/base/base.generator.js +0 -4
  50. package/dist/base/base.generator.js.map +1 -1
  51. package/dist/decoders/datamodel-decoder.generator.js +91 -1
  52. package/dist/decoders/datamodel-decoder.generator.js.map +1 -1
  53. package/dist/decoders/decoders.generator.d.ts +9 -0
  54. package/dist/decoders/decoders.generator.js +15 -0
  55. package/dist/decoders/decoders.generator.js.map +1 -1
  56. package/dist/decoders/discriminated-union.decoder.generator.js +4 -3
  57. package/dist/decoders/discriminated-union.decoder.generator.js.map +1 -1
  58. package/dist/devops/devops.generator.d.ts +5 -1
  59. package/dist/devops/devops.generator.js +5 -4
  60. package/dist/devops/devops.generator.js.map +1 -1
  61. package/dist/devops/generators/docker-compose-yml.generator.d.ts +1 -1
  62. package/dist/devops/generators/docker-compose-yml.generator.js +16 -1
  63. package/dist/devops/generators/docker-compose-yml.generator.js.map +1 -1
  64. package/dist/e2e/template/e2e/specs/example.spec.ts-snapshots/Navigate-to-homepage-and-take-snapshot-1-chromium-linux.png +0 -0
  65. package/dist/frontend-actions/generators/filter-utils.generator.js +1 -1
  66. package/dist/frontend-admin/admin.generator.d.ts +2 -0
  67. package/dist/frontend-admin/admin.generator.js +28 -0
  68. package/dist/frontend-admin/admin.generator.js.map +1 -1
  69. package/dist/frontend-admin/generators/admin-global-actions.generator.js +78 -0
  70. package/dist/frontend-admin/generators/admin-global-actions.generator.js.map +1 -1
  71. package/dist/frontend-admin/generators/admin-overview-page.generator.js +21 -1
  72. package/dist/frontend-admin/generators/admin-overview-page.generator.js.map +1 -1
  73. package/dist/frontend-admin/generators/admin-sidebar.generator.js +20 -0
  74. package/dist/frontend-admin/generators/admin-sidebar.generator.js.map +1 -1
  75. package/dist/frontend-admin/generators/excel-io-page.generator.d.ts +4 -0
  76. package/dist/frontend-admin/generators/excel-io-page.generator.js +258 -0
  77. package/dist/frontend-admin/generators/excel-io-page.generator.js.map +1 -0
  78. package/dist/frontend-admin/generators/import-review-page-result-stage.generator.d.ts +1 -0
  79. package/dist/frontend-admin/generators/import-review-page-result-stage.generator.js +104 -0
  80. package/dist/frontend-admin/generators/import-review-page-result-stage.generator.js.map +1 -0
  81. package/dist/frontend-admin/generators/import-review-page-review-stage.generator.d.ts +1 -0
  82. package/dist/frontend-admin/generators/import-review-page-review-stage.generator.js +1031 -0
  83. package/dist/frontend-admin/generators/import-review-page-review-stage.generator.js.map +1 -0
  84. package/dist/frontend-admin/generators/import-review-page-upload-stage.generator.d.ts +1 -0
  85. package/dist/frontend-admin/generators/import-review-page-upload-stage.generator.js +77 -0
  86. package/dist/frontend-admin/generators/import-review-page-upload-stage.generator.js.map +1 -0
  87. package/dist/frontend-admin/generators/import-review-page.generator.d.ts +7 -0
  88. package/dist/frontend-admin/generators/import-review-page.generator.js +180 -0
  89. package/dist/frontend-admin/generators/import-review-page.generator.js.map +1 -0
  90. package/dist/frontend-admin/generators/model-admin-page.generator.js +60 -56
  91. package/dist/frontend-admin/generators/model-admin-page.generator.js.map +1 -1
  92. package/dist/frontend-admin/utils.js +25 -33
  93. package/dist/frontend-admin/utils.js.map +1 -1
  94. package/dist/frontend-core/frontend.generator.js +1 -2
  95. package/dist/frontend-core/frontend.generator.js.map +1 -1
  96. package/dist/frontend-core/template/src/components/admin/excel-io-actions.tsx +64 -0
  97. package/dist/frontend-core/template/src/components/admin/table-view-panel.tsx +41 -3
  98. package/dist/frontend-core/template/src/hooks/use-excel-io.ts +137 -0
  99. package/dist/frontend-core/template/src/hooks/use-import-review.ts +143 -0
  100. package/dist/frontend-core/template/src/lib/excel-download.ts +28 -0
  101. package/dist/frontend-core/types/hook.d.ts +1 -1
  102. package/dist/frontend-tables/generators/model-table.generator.js +21 -13
  103. package/dist/frontend-tables/generators/model-table.generator.js.map +1 -1
  104. package/dist/frontend-trpc-client/generators/model-hook.generator.js +4 -0
  105. package/dist/frontend-trpc-client/generators/model-hook.generator.js.map +1 -1
  106. package/dist/frontend-trpc-client/trpc-client.generator.d.ts +4 -0
  107. package/dist/frontend-trpc-client/trpc-client.generator.js +1 -0
  108. package/dist/frontend-trpc-client/trpc-client.generator.js.map +1 -1
  109. package/dist/generators.js +2 -0
  110. package/dist/generators.js.map +1 -1
  111. package/dist/index.d.ts +1 -0
  112. package/dist/index.js +5 -2
  113. package/dist/index.js.map +1 -1
  114. package/dist/seed-data/seed-data.generator.d.ts +3 -0
  115. package/dist/seed-data/seed-data.generator.js +45 -1
  116. package/dist/seed-data/seed-data.generator.js.map +1 -1
  117. package/package.json +3 -3
@@ -0,0 +1,1031 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateImportReviewReviewStage = generateImportReviewReviewStage;
4
+ function generateImportReviewReviewStage() {
5
+ return `
6
+ function ReviewStage({
7
+ preview,
8
+ isLoading,
9
+ onExecute,
10
+ onReset,
11
+ }: Readonly<{
12
+ preview: ImportPreviewResponse
13
+ isLoading: boolean
14
+ onExecute: (options: {
15
+ ignoreErrors: boolean
16
+ confirmDeletes: boolean
17
+ selectedDeltaIndicesByModel: SelectedDeltaIndicesByModel
18
+ }) => Promise<void>
19
+ onReset: () => void
20
+ }>) {
21
+ const [activeModel, setActiveModel] = useState<string | null>(null)
22
+ const [ignoreErrors, setIgnoreErrors] = useState(false)
23
+ const [confirmDeletesChecked, setConfirmDeletesChecked] = useState(false)
24
+ const [cancelDialogOpen, setCancelDialogOpen] = useState(false)
25
+ const [executeDialogOpen, setExecuteDialogOpen] = useState(false)
26
+ const [selectedDeltaIndicesByModel, setSelectedDeltaIndicesByModel] = useState<SelectedDeltaIndicesByModel>(() =>
27
+ buildDefaultSelection(preview.models),
28
+ )
29
+
30
+ useEffect(() => {
31
+ setSelectedDeltaIndicesByModel(buildDefaultSelection(preview.models))
32
+ setActiveModel(null)
33
+ setIgnoreErrors(false)
34
+ setConfirmDeletesChecked(false)
35
+ setCancelDialogOpen(false)
36
+ setExecuteDialogOpen(false)
37
+ }, [preview])
38
+
39
+ const modelEntries = useMemo(
40
+ () => Object.entries(preview.models).filter(([, model]) => model.items.length > 0),
41
+ [preview.models],
42
+ )
43
+
44
+ const selectedStatsByModel = useMemo(() => {
45
+ const result: Record<string, { selected: number; executable: number; selectedDeletes: number }> = {}
46
+ for (const [modelName, model] of modelEntries) {
47
+ const selectedSet = new Set(selectedDeltaIndicesByModel[modelName] ?? [])
48
+ let selected = 0
49
+ let executable = 0
50
+ let selectedDeletes = 0
51
+
52
+ for (const item of model.items) {
53
+ if (!isExecutableItem(item) || typeof item.deltaIndex !== 'number') {
54
+ continue
55
+ }
56
+
57
+ executable++
58
+ if (selectedSet.has(item.deltaIndex)) {
59
+ selected++
60
+ if (item.type === 'delete') {
61
+ selectedDeletes++
62
+ }
63
+ }
64
+ }
65
+
66
+ result[modelName] = { selected, executable, selectedDeletes }
67
+ }
68
+ return result
69
+ }, [modelEntries, selectedDeltaIndicesByModel])
70
+
71
+ const totalSelected = Object.values(selectedStatsByModel).reduce((acc, item) => acc + item.selected, 0)
72
+ const totalExecutable = Object.values(selectedStatsByModel).reduce((acc, item) => acc + item.executable, 0)
73
+ const selectedDeletes = Object.values(selectedStatsByModel).reduce((acc, item) => acc + item.selectedDeletes, 0)
74
+
75
+ const hasProcessingErrors = !!preview.processingErrors?.length
76
+ const errorCount = preview.summary.errors
77
+ const hasRowErrors = errorCount > 0
78
+ const canExecute =
79
+ !hasProcessingErrors &&
80
+ totalSelected > 0 &&
81
+ (selectedDeletes === 0 || confirmDeletesChecked) &&
82
+ (!hasRowErrors || ignoreErrors)
83
+
84
+ const hasSelectionChanges = !selectionEquals(selectedDeltaIndicesByModel, buildDefaultSelection(preview.models))
85
+
86
+ const handleExecuteConfirmed = () => {
87
+ setExecuteDialogOpen(false)
88
+ void onExecute({
89
+ ignoreErrors,
90
+ confirmDeletes: selectedDeletes === 0 || confirmDeletesChecked,
91
+ selectedDeltaIndicesByModel,
92
+ })
93
+ }
94
+
95
+ return (
96
+ <>
97
+ <AlertDialog open={cancelDialogOpen} onOpenChange={setCancelDialogOpen}>
98
+ <AlertDialogContent>
99
+ <AlertDialogHeader>
100
+ <AlertDialogTitle>Discard Preview?</AlertDialogTitle>
101
+ <AlertDialogDescription>
102
+ {hasSelectionChanges
103
+ ? 'This will discard the uploaded preview and your row selections.'
104
+ : 'This will discard the uploaded preview.'}
105
+ </AlertDialogDescription>
106
+ </AlertDialogHeader>
107
+ <AlertDialogFooter>
108
+ <AlertDialogCancel>Continue Reviewing</AlertDialogCancel>
109
+ <AlertDialogAction onClick={onReset}>Discard and Return</AlertDialogAction>
110
+ </AlertDialogFooter>
111
+ </AlertDialogContent>
112
+ </AlertDialog>
113
+
114
+ <AlertDialog open={executeDialogOpen} onOpenChange={setExecuteDialogOpen}>
115
+ <AlertDialogContent>
116
+ <AlertDialogHeader>
117
+ <AlertDialogTitle>Execute Selected Changes?</AlertDialogTitle>
118
+ <AlertDialogDescription>
119
+ You are about to execute {totalSelected} selected changes ({selectedDeletes} delete{selectedDeletes === 1 ? '' : 's'}).
120
+ </AlertDialogDescription>
121
+ </AlertDialogHeader>
122
+ <AlertDialogFooter>
123
+ <AlertDialogCancel>Back</AlertDialogCancel>
124
+ <AlertDialogAction onClick={handleExecuteConfirmed}>Execute</AlertDialogAction>
125
+ </AlertDialogFooter>
126
+ </AlertDialogContent>
127
+ </AlertDialog>
128
+
129
+ <div className="grid gap-4 lg:grid-cols-[360px_1fr]">
130
+ <PreviewSidebar
131
+ filename={preview.filename}
132
+ modelEntries={modelEntries}
133
+ selectedStatsByModel={selectedStatsByModel}
134
+ setSelectedDeltaIndicesByModel={setSelectedDeltaIndicesByModel}
135
+ activeModel={activeModel}
136
+ onSelectModel={setActiveModel}
137
+ onCancel={() => setCancelDialogOpen(true)}
138
+ onExecute={() => setExecuteDialogOpen(true)}
139
+ canExecute={canExecute}
140
+ isLoading={isLoading}
141
+ totalSelected={totalSelected}
142
+ totalExecutable={totalExecutable}
143
+ selectedDeletes={selectedDeletes}
144
+ confirmDeletesChecked={confirmDeletesChecked}
145
+ setConfirmDeletesChecked={setConfirmDeletesChecked}
146
+ errorCount={errorCount}
147
+ ignoreErrors={ignoreErrors}
148
+ setIgnoreErrors={setIgnoreErrors}
149
+ />
150
+
151
+ <div className="flex min-w-0 flex-col gap-4">
152
+ {hasProcessingErrors && (
153
+ <Card className="border-destructive">
154
+ <CardHeader>
155
+ <CardTitle className="text-destructive">Processing Errors</CardTitle>
156
+ </CardHeader>
157
+ <CardContent className="space-y-2">
158
+ {preview.processingErrors?.map((err) => (
159
+ <Alert key={err} variant="destructive">
160
+ <AlertDescription>
161
+ <pre className="whitespace-pre-wrap break-all text-sm">{err}</pre>
162
+ </AlertDescription>
163
+ </Alert>
164
+ ))}
165
+ </CardContent>
166
+ </Card>
167
+ )}
168
+
169
+ {!activeModel && (
170
+ <OverviewTable
171
+ modelEntries={modelEntries}
172
+ selectedStatsByModel={selectedStatsByModel}
173
+ onOpenModel={setActiveModel}
174
+ onSelectAllModel={(modelName) =>
175
+ setSelectedDeltaIndicesByModel((prev) => {
176
+ const model = preview.models[modelName]
177
+ return { ...prev, [modelName]: model ? selectModel(model) : [] }
178
+ })
179
+ }
180
+ onDeselectAllModel={(modelName) =>
181
+ setSelectedDeltaIndicesByModel((prev) => ({ ...prev, [modelName]: [] }))
182
+ }
183
+ />
184
+ )}
185
+
186
+ {activeModel && preview.models[activeModel] && (
187
+ <ModelReviewPanel
188
+ modelName={activeModel}
189
+ model={preview.models[activeModel]}
190
+ selectedIndices={selectedDeltaIndicesByModel[activeModel] ?? []}
191
+ onSelectionChange={(indices) =>
192
+ setSelectedDeltaIndicesByModel((prev) => ({ ...prev, [activeModel]: indices }))
193
+ }
194
+ onBack={() => setActiveModel(null)}
195
+ />
196
+ )}
197
+ </div>
198
+ </div>
199
+ </>
200
+ )
201
+ }
202
+
203
+ function PreviewSidebar({
204
+ filename,
205
+ modelEntries,
206
+ selectedStatsByModel,
207
+ setSelectedDeltaIndicesByModel,
208
+ activeModel,
209
+ onSelectModel,
210
+ onCancel,
211
+ onExecute,
212
+ canExecute,
213
+ isLoading,
214
+ totalSelected,
215
+ totalExecutable,
216
+ selectedDeletes,
217
+ confirmDeletesChecked,
218
+ setConfirmDeletesChecked,
219
+ errorCount,
220
+ ignoreErrors,
221
+ setIgnoreErrors,
222
+ }: Readonly<{
223
+ filename: string
224
+ modelEntries: [string, ImportPreviewModelSummary][]
225
+ selectedStatsByModel: Record<string, { selected: number; executable: number; selectedDeletes: number }>
226
+ setSelectedDeltaIndicesByModel: React.Dispatch<React.SetStateAction<SelectedDeltaIndicesByModel>>
227
+ activeModel: string | null
228
+ onSelectModel: (modelName: string | null) => void
229
+ onCancel: () => void
230
+ onExecute: () => void
231
+ canExecute: boolean
232
+ isLoading: boolean
233
+ totalSelected: number
234
+ totalExecutable: number
235
+ selectedDeletes: number
236
+ confirmDeletesChecked: boolean
237
+ setConfirmDeletesChecked: (checked: boolean) => void
238
+ errorCount: number
239
+ ignoreErrors: boolean
240
+ setIgnoreErrors: (checked: boolean) => void
241
+ }>) {
242
+ const groupedModels = useMemo(() => {
243
+ const bySchema = new Map<string, [string, ImportPreviewModelSummary][]> ()
244
+ for (const entry of modelEntries) {
245
+ const modelName = entry[0]
246
+ const schema = MODEL_SCHEMA_MAP[resolveCanonicalModelKey(modelName)] ?? 'default'
247
+ if (!bySchema.has(schema)) {
248
+ bySchema.set(schema, [])
249
+ }
250
+ bySchema.get(schema)?.push(entry)
251
+ }
252
+
253
+ for (const entries of bySchema.values()) {
254
+ entries.sort((a, b) => getModelLabel(a[0]).localeCompare(getModelLabel(b[0])))
255
+ }
256
+
257
+ const orderedSchemas = [...new Set([...SCHEMA_ORDER, ...bySchema.keys()])].filter((schema) => bySchema.has(schema))
258
+
259
+ return orderedSchemas.map((schema) => ({ schema, entries: bySchema.get(schema) ?? [] }))
260
+ }, [modelEntries])
261
+
262
+ return (
263
+ <Card className="flex min-h-[70vh] flex-col">
264
+ <CardHeader className="space-y-3">
265
+ <CardTitle>Import for {filename}</CardTitle>
266
+ <div className="grid grid-cols-2 gap-2 text-sm">
267
+ <div className="rounded border p-2">
268
+ <div className="text-muted-foreground">Selected</div>
269
+ <div className="text-lg font-semibold">{totalSelected}</div>
270
+ </div>
271
+ <div className="rounded border p-2">
272
+ <div className="text-muted-foreground">Executable</div>
273
+ <div className="text-lg font-semibold">{totalExecutable}</div>
274
+ </div>
275
+ </div>
276
+
277
+ <div className="flex flex-wrap gap-2">
278
+ <Button variant="outline" size="sm" onClick={() => setSelectedDeltaIndicesByModel(selectAll(modelEntries))}>
279
+ Select All
280
+ </Button>
281
+ <Button variant="outline" size="sm" onClick={() => setSelectedDeltaIndicesByModel(deselectAll(modelEntries))}>
282
+ Deselect All
283
+ </Button>
284
+ {activeModel && (
285
+ <Button variant="outline" size="sm" onClick={() => onSelectModel(null)}>
286
+ Back to Overview
287
+ </Button>
288
+ )}
289
+ </div>
290
+ </CardHeader>
291
+
292
+ <CardContent className="flex min-h-0 flex-1 flex-col gap-3">
293
+ <div className="min-h-0 flex-1 overflow-y-auto pr-1">
294
+ {groupedModels.map((schemaGroup) => (
295
+ <div key={schemaGroup.schema} className="mb-4">
296
+ <p className="mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">{schemaGroup.schema}</p>
297
+ <div className="space-y-2">
298
+ {schemaGroup.entries.map(([modelName, model]) => {
299
+ const selectedStats = selectedStatsByModel[modelName] ?? { selected: 0, executable: 0, selectedDeletes: 0 }
300
+ const isActive = activeModel === modelName
301
+
302
+ return (
303
+ <button
304
+ key={modelName}
305
+ type="button"
306
+ className={
307
+ 'w-full rounded border p-2 text-left transition ' +
308
+ (isActive
309
+ ? 'border-blue-400 bg-blue-50 dark:border-blue-700 dark:bg-blue-950/40'
310
+ : 'border-border hover:bg-muted/30')
311
+ }
312
+ onClick={() => onSelectModel(modelName)}
313
+ >
314
+ <div className="flex items-center justify-between gap-2">
315
+ <span className="flex items-center gap-1.5 font-medium">
316
+ {getModelLabel(modelName)}
317
+ {model.errors > 0 && (
318
+ <Badge variant="outline" size="sm" className="text-red-700 border-red-300 dark:text-red-400 dark:border-red-700">
319
+ <AlertTriangleIcon className="size-3" />
320
+ !
321
+ </Badge>
322
+ )}
323
+ </span>
324
+ <span className="text-xs text-muted-foreground">
325
+ {selectedStats.selected}/{selectedStats.executable}
326
+ </span>
327
+ </div>
328
+ <div className="mt-2 flex flex-wrap gap-1">
329
+ <TooltipProvider>
330
+ <Tooltip>
331
+ <TooltipTrigger asChild>
332
+ <Badge variant="outline" size="sm" className={TYPE_BADGE_CLASSES.create}>
333
+ <PlusIcon className="size-3" />
334
+ {model.create}
335
+ </Badge>
336
+ </TooltipTrigger>
337
+ <TooltipContent>
338
+ <p>Rows that will be created.</p>
339
+ </TooltipContent>
340
+ </Tooltip>
341
+ <Tooltip>
342
+ <TooltipTrigger asChild>
343
+ <Badge variant="outline" size="sm" className={TYPE_BADGE_CLASSES.update}>
344
+ <PencilIcon className="size-3" />
345
+ {model.update}
346
+ </Badge>
347
+ </TooltipTrigger>
348
+ <TooltipContent>
349
+ <p>Rows that will be updated.</p>
350
+ </TooltipContent>
351
+ </Tooltip>
352
+ <Tooltip>
353
+ <TooltipTrigger asChild>
354
+ <Badge variant="outline" size="sm" className={TYPE_BADGE_CLASSES.delete}>
355
+ <Trash2Icon className="size-3" />
356
+ {model.delete}
357
+ </Badge>
358
+ </TooltipTrigger>
359
+ <TooltipContent>
360
+ <p>Rows that will be deleted.</p>
361
+ </TooltipContent>
362
+ </Tooltip>
363
+ <Tooltip>
364
+ <TooltipTrigger asChild>
365
+ <Badge variant="outline" size="sm" className={TYPE_BADGE_CLASSES.errors}>
366
+ <AlertTriangleIcon className="size-3" />
367
+ {model.errors}
368
+ </Badge>
369
+ </TooltipTrigger>
370
+ <TooltipContent>
371
+ <p>Rows with validation or parse errors.</p>
372
+ </TooltipContent>
373
+ </Tooltip>
374
+ <Tooltip>
375
+ <TooltipTrigger asChild>
376
+ <Badge variant="outline" size="sm" className={TYPE_BADGE_CLASSES.unchanged}>
377
+ <MinusIcon className="size-3" />
378
+ {model.unchanged}
379
+ </Badge>
380
+ </TooltipTrigger>
381
+ <TooltipContent>
382
+ <p>Rows with no data changes.</p>
383
+ </TooltipContent>
384
+ </Tooltip>
385
+ </TooltipProvider>
386
+ </div>
387
+ </button>
388
+ )
389
+ })}
390
+ </div>
391
+ </div>
392
+ ))}
393
+ </div>
394
+
395
+ <div className="space-y-3 border-t pt-3">
396
+ {errorCount > 0 && (
397
+ <div className="flex items-center gap-3">
398
+ <Checkbox
399
+ id="ignore-errors"
400
+ checked={ignoreErrors}
401
+ checkIcon="check"
402
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) => setIgnoreErrors(event.target.checked)}
403
+ />
404
+ <Label htmlFor="ignore-errors">Ignore {errorCount} error{errorCount === 1 ? '' : 's'}</Label>
405
+ </div>
406
+ )}
407
+
408
+ {selectedDeletes > 0 && (
409
+ <div className="flex items-center gap-2 rounded border border-red-300 bg-red-50 p-2 dark:border-red-900 dark:bg-red-950/20">
410
+ <Checkbox
411
+ id="confirm-deletes"
412
+ checked={confirmDeletesChecked}
413
+ checkIcon="check"
414
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) => setConfirmDeletesChecked(event.target.checked)}
415
+ />
416
+ <Label htmlFor="confirm-deletes">Confirm {selectedDeletes} delete{selectedDeletes === 1 ? '' : 's'}</Label>
417
+ </div>
418
+ )}
419
+
420
+ <div className="flex gap-2">
421
+ <Button variant="outline" onClick={onCancel} disabled={isLoading} className="flex-1">
422
+ Cancel
423
+ </Button>
424
+ <Button onClick={onExecute} disabled={!canExecute || isLoading} className="flex-1">
425
+ {isLoading ? 'Executing...' : 'Execute'}
426
+ </Button>
427
+ </div>
428
+ </div>
429
+ </CardContent>
430
+ </Card>
431
+ )
432
+ }
433
+
434
+ function OverviewTable({
435
+ modelEntries,
436
+ selectedStatsByModel,
437
+ onOpenModel,
438
+ onSelectAllModel,
439
+ onDeselectAllModel,
440
+ }: Readonly<{
441
+ modelEntries: [string, ImportPreviewModelSummary][]
442
+ selectedStatsByModel: Record<string, { selected: number; executable: number; selectedDeletes: number }>
443
+ onOpenModel: (modelName: string) => void
444
+ onSelectAllModel: (modelName: string) => void
445
+ onDeselectAllModel: (modelName: string) => void
446
+ }>) {
447
+ return (
448
+ <Card>
449
+ <CardHeader>
450
+ <CardTitle>Model Overview</CardTitle>
451
+ </CardHeader>
452
+ <CardContent className="overflow-x-auto">
453
+ <table className="w-full min-w-[700px] text-sm">
454
+ <thead>
455
+ <tr className="border-b text-left text-muted-foreground">
456
+ <th className="p-2">Model</th>
457
+ <th className="p-2 text-right">Create</th>
458
+ <th className="p-2 text-right">Update</th>
459
+ <th className="p-2 text-right">Delete</th>
460
+ <th className="p-2 text-right">Unchanged</th>
461
+ <th className="p-2 text-right">Errors</th>
462
+ <th className="p-2 text-right">Selected</th>
463
+ <th className="p-2 text-right">Selection</th>
464
+ </tr>
465
+ </thead>
466
+ <tbody>
467
+ {modelEntries.map(([modelName, model]) => {
468
+ const selected = selectedStatsByModel[modelName] ?? { selected: 0, executable: 0, selectedDeletes: 0 }
469
+ return (
470
+ <tr
471
+ key={modelName}
472
+ className="cursor-pointer border-b transition hover:bg-muted/20 last:border-b-0"
473
+ onClick={() => onOpenModel(modelName)}
474
+ >
475
+ <td className="p-2 font-medium">{getModelLabel(modelName)}</td>
476
+ <td className="p-2 text-right">{model.create}</td>
477
+ <td className="p-2 text-right">{model.update}</td>
478
+ <td className="p-2 text-right">{model.delete}</td>
479
+ <td className="p-2 text-right">{model.unchanged}</td>
480
+ <td className="p-2 text-right font-medium text-red-600 dark:text-red-400">{model.errors}</td>
481
+ <td className="p-2 text-right font-semibold">
482
+ {selected.selected}/{selected.executable}
483
+ </td>
484
+ <td className="p-2 text-right">
485
+ <Button
486
+ size="sm"
487
+ variant="outline"
488
+ onClick={(event) => {
489
+ event.stopPropagation()
490
+ onSelectAllModel(modelName)
491
+ }}
492
+ >
493
+ Select All
494
+ </Button>
495
+ <Button
496
+ size="sm"
497
+ variant="outline"
498
+ className="ml-2"
499
+ onClick={(event) => {
500
+ event.stopPropagation()
501
+ onDeselectAllModel(modelName)
502
+ }}
503
+ >
504
+ Deselect All
505
+ </Button>
506
+ </td>
507
+ </tr>
508
+ )
509
+ })}
510
+ </tbody>
511
+ </table>
512
+ </CardContent>
513
+ </Card>
514
+ )
515
+ }
516
+
517
+ function ModelReviewPanel({
518
+ modelName,
519
+ model,
520
+ selectedIndices,
521
+ onSelectionChange,
522
+ onBack,
523
+ }: Readonly<{
524
+ modelName: string
525
+ model: ImportPreviewModelSummary
526
+ selectedIndices: number[]
527
+ onSelectionChange: (indices: number[]) => void
528
+ onBack: () => void
529
+ }>) {
530
+ return (
531
+ <Card>
532
+ <CardHeader className="space-y-3">
533
+ <div className="flex items-center justify-between gap-3">
534
+ <CardTitle>{getModelLabel(modelName)}</CardTitle>
535
+ <Button variant="outline" size="sm" onClick={onBack}>
536
+ Back to Overview
537
+ </Button>
538
+ </div>
539
+ <div className="flex flex-wrap gap-2">
540
+ <Button size="sm" variant="outline" onClick={() => onSelectionChange(selectModel(model))}>
541
+ Select Model
542
+ </Button>
543
+ <Button size="sm" variant="outline" onClick={() => onSelectionChange([])}>
544
+ Deselect Model
545
+ </Button>
546
+ </div>
547
+ </CardHeader>
548
+ <CardContent>
549
+ <ModelDataGrid
550
+ modelName={modelName}
551
+ items={model.items}
552
+ selectedIndices={selectedIndices}
553
+ onSelectionChange={onSelectionChange}
554
+ />
555
+ </CardContent>
556
+ </Card>
557
+ )
558
+ }
559
+
560
+
561
+ function ModelDataGrid({
562
+ modelName,
563
+ items,
564
+ selectedIndices,
565
+ onSelectionChange,
566
+ }: Readonly<{
567
+ modelName: string
568
+ items: ImportPreviewItem[]
569
+ selectedIndices: number[]
570
+ onSelectionChange: (indices: number[]) => void
571
+ }>) {
572
+ const sorted = useMemo(
573
+ () => [...items].sort((a, b) => TYPE_ORDER.indexOf(a.type) - TYPE_ORDER.indexOf(b.type)),
574
+ [items],
575
+ )
576
+
577
+ const selectedSet = useMemo(() => new Set(selectedIndices), [selectedIndices])
578
+
579
+ const fieldNames = useMemo(() => {
580
+ const preferred = MODEL_FIELD_ORDER[resolveCanonicalModelKey(modelName)] ?? []
581
+ if (preferred.length > 0) {
582
+ return preferred
583
+ }
584
+
585
+ const fields: string[] = []
586
+ const seen = new Set<string>()
587
+ for (const item of sorted) {
588
+ for (const key of Object.keys(item.input)) {
589
+ if (!seen.has(key)) {
590
+ seen.add(key)
591
+ fields.push(key)
592
+ }
593
+ }
594
+ }
595
+ return fields
596
+ }, [modelName, sorted])
597
+
598
+ const columns = useMemo<ColumnDef<ImportPreviewItem>[]>(
599
+ () => [
600
+ {
601
+ id: '_selected',
602
+ header: 'Use',
603
+ size: 74,
604
+ enableResizing: false,
605
+ meta: {
606
+ cell: {
607
+ variant: 'react-node' as const,
608
+ render: ({ cell }: { cell: { row: { original: ImportPreviewItem } } }) => {
609
+ const row = cell.row.original
610
+ if (!isExecutableItem(row) || typeof row.deltaIndex !== 'number') {
611
+ return null
612
+ }
613
+
614
+ return (
615
+ <Checkbox
616
+ checked={selectedSet.has(row.deltaIndex)}
617
+ checkIcon="check"
618
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
619
+ const next = new Set(selectedSet)
620
+ if (event.target.checked) {
621
+ next.add(row.deltaIndex as number)
622
+ } else {
623
+ next.delete(row.deltaIndex as number)
624
+ }
625
+ onSelectionChange([...next].sort((a, b) => a - b)) // NOSONAR
626
+ }}
627
+ />
628
+ )
629
+ },
630
+ },
631
+ },
632
+ },
633
+ {
634
+ id: '_type',
635
+ accessorKey: 'type',
636
+ header: 'Type',
637
+ size: 110,
638
+ enableResizing: false,
639
+ meta: {
640
+ align: 'center' as const,
641
+ cell: {
642
+ variant: 'react-node' as const,
643
+ render: ({ cell }: { cell: { row: { original: ImportPreviewItem } } }) => {
644
+ const rowType = cell.row.original.type
645
+ return (
646
+ <div className="flex h-full w-full items-center justify-center overflow-hidden">
647
+ <Badge variant="outline" size="sm" className={"capitalize " + TYPE_BADGE_CLASSES[rowType]}>
648
+ {rowType}
649
+ </Badge>
650
+ </div>
651
+ )
652
+ },
653
+ },
654
+ },
655
+ },
656
+ ...fieldNames.map(
657
+ (field): ColumnDef<ImportPreviewItem> => ({
658
+ id: field,
659
+ accessorFn: (row: ImportPreviewItem) => row.input[field],
660
+ header: formatFieldHeader(field),
661
+ size: guessColumnSize(field, sorted),
662
+ meta: {
663
+ cell: {
664
+ variant: 'react-node' as const,
665
+ render: ({ cell }: { cell: { row: { original: ImportPreviewItem } } }) => (
666
+ <FieldCell item={cell.row.original} field={field} />
667
+ ),
668
+ },
669
+ },
670
+ }),
671
+ ),
672
+ ],
673
+ [fieldNames, onSelectionChange, selectedSet, sorted],
674
+ )
675
+
676
+ const { table, ...dataGridProps } = useDataGrid({
677
+ columns,
678
+ data: sorted,
679
+ initialState: {
680
+ columnPinning: { left: ['_selected', '_type'] },
681
+ },
682
+ })
683
+
684
+ const gridHeight = Math.min(760, sorted.length * 36 + 48)
685
+
686
+ return <DataGrid {...dataGridProps} table={table} height={gridHeight} />
687
+ }
688
+
689
+ function FieldCell({ item, field }: Readonly<{ item: ImportPreviewItem; field: string }>) {
690
+ const value = item.input[field]
691
+ const formatted = formatValue(value)
692
+ const cellClass = 'flex h-full w-full min-w-0 items-center overflow-hidden'
693
+ const textClass = 'block w-full min-w-0 overflow-hidden text-ellipsis whitespace-nowrap'
694
+
695
+ switch (item.type) {
696
+ case 'unchanged':
697
+ return (
698
+ <div className={cellClass}>
699
+ <span data-slot="grid-cell-content" className={textClass + ' text-muted-foreground'} title={formatted}>
700
+ {formatted}
701
+ </span>
702
+ </div>
703
+ )
704
+
705
+ case 'create':
706
+ return (
707
+ <div className={cellClass}>
708
+ <span
709
+ data-slot="grid-cell-content"
710
+ className={textClass + ' text-green-600 dark:text-green-400'}
711
+ title={formatted}
712
+ >
713
+ {formatted}
714
+ </span>
715
+ </div>
716
+ )
717
+
718
+ case 'delete':
719
+ return (
720
+ <div className={cellClass}>
721
+ <span
722
+ data-slot="grid-cell-content"
723
+ className={textClass + ' line-through text-red-600 dark:text-red-400'}
724
+ title={formatted}
725
+ >
726
+ {formatted}
727
+ </span>
728
+ </div>
729
+ )
730
+
731
+ case 'update': {
732
+ if (item.delta && field in item.delta) {
733
+ const oldNew = item.delta[field]
734
+ return (
735
+ <div className={cellClass}>
736
+ <span
737
+ data-slot="grid-cell-content"
738
+ className="block w-full min-w-0 truncate text-sm"
739
+ title={formatValue(oldNew.old) + ' → ' + formatValue(oldNew.new)}
740
+ >
741
+ <span className="line-through text-red-600 opacity-70 dark:text-red-400">{formatValue(oldNew.old)}</span>
742
+ <span className="text-xs text-muted-foreground"> → </span>
743
+ <span className="font-medium text-green-600 dark:text-green-400">{formatValue(oldNew.new)}</span>
744
+ </span>
745
+ </div>
746
+ )
747
+ }
748
+ return (
749
+ <div className={cellClass}>
750
+ <span data-slot="grid-cell-content" className={textClass + ' text-muted-foreground'} title={formatted}>
751
+ {formatted}
752
+ </span>
753
+ </div>
754
+ )
755
+ }
756
+
757
+ case 'errors': {
758
+ const errorForField = getFieldError(item.errors, field)
759
+ if (errorForField) {
760
+ return (
761
+ <div className={cellClass}>
762
+ <TooltipProvider>
763
+ <Tooltip>
764
+ <TooltipTrigger asChild>
765
+ <span
766
+ data-slot="grid-cell-content"
767
+ className="block w-full min-w-0 cursor-help truncate underline decoration-wavy text-red-600 dark:text-red-400"
768
+ title={formatted || '(empty)'}
769
+ >
770
+ {formatted || '(empty)'}
771
+ </span>
772
+ </TooltipTrigger>
773
+ <TooltipContent>
774
+ <p>{errorForField}</p>
775
+ </TooltipContent>
776
+ </Tooltip>
777
+ </TooltipProvider>
778
+ </div>
779
+ )
780
+ }
781
+ return (
782
+ <div className={cellClass}>
783
+ <span data-slot="grid-cell-content" className={textClass + ' text-muted-foreground'} title={formatted}>
784
+ {formatted}
785
+ </span>
786
+ </div>
787
+ )
788
+ }
789
+ }
790
+ }
791
+
792
+ function isExecutableItem(item: ImportPreviewItem): boolean {
793
+ return EXECUTABLE_TYPES.has(item.type)
794
+ }
795
+
796
+ function buildDefaultSelection(models: Record<string, ImportPreviewModelSummary>): SelectedDeltaIndicesByModel {
797
+ const selection: SelectedDeltaIndicesByModel = {}
798
+
799
+ for (const [modelName, model] of Object.entries(models)) {
800
+ selection[modelName] = model.items
801
+ .filter((item) => isExecutableItem(item) && typeof item.deltaIndex === 'number')
802
+ .map((item) => item.deltaIndex as number)
803
+ .sort((a, b) => a - b)
804
+ }
805
+
806
+ return selection
807
+ }
808
+
809
+ function selectionEquals(a: SelectedDeltaIndicesByModel, b: SelectedDeltaIndicesByModel): boolean {
810
+ const keys = new Set([...Object.keys(a), ...Object.keys(b)])
811
+ for (const key of keys) {
812
+ const left = [...(a[key] ?? [])].sort((x, y) => x - y)
813
+ const right = [...(b[key] ?? [])].sort((x, y) => x - y)
814
+ if (left.length !== right.length) {
815
+ return false
816
+ }
817
+ for (let i = 0; i < left.length; i++) {
818
+ if (left[i] !== right[i]) {
819
+ return false
820
+ }
821
+ }
822
+ }
823
+ return true
824
+ }
825
+
826
+ function selectModel(model: ImportPreviewModelSummary): number[] {
827
+ return model.items
828
+ .filter((item) => isExecutableItem(item) && typeof item.deltaIndex === 'number')
829
+ .map((item) => item.deltaIndex as number)
830
+ .sort((a, b) => a - b)
831
+ }
832
+
833
+ function selectAll(modelEntries: [string, ImportPreviewModelSummary][]): SelectedDeltaIndicesByModel {
834
+ const next: SelectedDeltaIndicesByModel = {}
835
+ for (const [modelName, model] of modelEntries) {
836
+ next[modelName] = selectModel(model)
837
+ }
838
+ return next
839
+ }
840
+
841
+ function deselectAll(modelEntries: [string, ImportPreviewModelSummary][]): SelectedDeltaIndicesByModel {
842
+ const next: SelectedDeltaIndicesByModel = {}
843
+ for (const [modelName] of modelEntries) {
844
+ next[modelName] = []
845
+ }
846
+ return next
847
+ }
848
+
849
+ function getModelLabel(modelName: string): string {
850
+ const canonicalModelKey = resolveCanonicalModelKey(modelName)
851
+ return MODEL_LABEL_MAP[canonicalModelKey] ?? modelName
852
+ }
853
+
854
+ function resolveCanonicalModelKey(modelName: string): string {
855
+ if (MODEL_LABEL_MAP[modelName] || MODEL_SCHEMA_MAP[modelName] || MODEL_FIELD_ORDER[modelName]) {
856
+ return modelName
857
+ }
858
+
859
+ const normalizedModelName = modelName.toLowerCase()
860
+ for (const [key, label] of Object.entries(MODEL_LABEL_MAP)) {
861
+ if (key.toLowerCase() === normalizedModelName || label.toLowerCase() === normalizedModelName) {
862
+ return key
863
+ }
864
+ }
865
+
866
+ return modelName
867
+ }
868
+
869
+ function getFieldError(errors: unknown[] | undefined, field: string): string | null {
870
+ if (!errors) {
871
+ return null
872
+ }
873
+
874
+ let genericParseError: string | null = null
875
+ for (const err of errors) {
876
+ const fieldError = getFieldLevelError(err, field)
877
+ if (fieldError) {
878
+ return fieldError
879
+ }
880
+
881
+ const parseErrorMessage = getParseErrorMessage(err)
882
+ if (!parseErrorMessage) {
883
+ continue
884
+ }
885
+
886
+ if (parseErrorMentionsField(parseErrorMessage, field)) {
887
+ return parseErrorMessage
888
+ }
889
+
890
+ if (!genericParseError) {
891
+ genericParseError = parseErrorMessage
892
+ }
893
+ }
894
+
895
+ return genericParseError
896
+ }
897
+
898
+ function getFieldLevelError(err: unknown, field: string): string | null {
899
+ if (typeof err !== 'object' || err === null || !('fieldName' in err)) {
900
+ return null
901
+ }
902
+ const errObj = err as { fieldName: string; error: string; message?: string }
903
+ if (errObj.fieldName !== field) {
904
+ return null
905
+ }
906
+ return errObj.message ?? errObj.error
907
+ }
908
+
909
+ function getParseErrorMessage(err: unknown): string | null {
910
+ if (typeof err !== 'object' || err === null || !('error' in err)) {
911
+ return null
912
+ }
913
+ if ((err as { error?: unknown }).error !== 'parse-error') {
914
+ return null
915
+ }
916
+ const messageValue = 'message' in err ? (err as { message?: unknown }).message : undefined
917
+ return typeof messageValue === 'string' ? messageValue : null
918
+ }
919
+
920
+ function parseErrorMentionsField(message: string, field: string): boolean {
921
+ return (
922
+ message.startsWith('Field "' + field + '":') ||
923
+ message.startsWith('Field "' + field + '."') ||
924
+ message.includes('."' + field + '".') ||
925
+ message.includes('.' + field + ':')
926
+ )
927
+ }
928
+
929
+ function formatFieldHeader(field: string): string {
930
+ const withCaseSpacing = field.replaceAll(/([a-z])([A-Z])/g, '$1 $2')
931
+ const normalized = withCaseSpacing.replaceAll('_', ' ').replaceAll('-', ' ')
932
+ return normalized
933
+ .split(/\\s+/)
934
+ .filter(Boolean)
935
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
936
+ .join(' ')
937
+ }
938
+
939
+ function formatValue(value: unknown): string {
940
+ if (value === null) {
941
+ return 'null'
942
+ }
943
+ if (value === undefined) {
944
+ return ''
945
+ }
946
+ if (typeof value === 'string') {
947
+ return value
948
+ }
949
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
950
+ return \`\${value}\` // NOSONAR
951
+ }
952
+ if (typeof value === 'symbol') {
953
+ return value.toString()
954
+ }
955
+ if (typeof value === 'object') {
956
+ return formatObjectValue(value as Record<string, unknown>)
957
+ }
958
+ return String(value) // NOSONAR
959
+ }
960
+
961
+ function formatObjectValue(obj: Record<string, unknown>): string {
962
+ const serialized = safeJsonStringify(obj)
963
+ if (serialized) {
964
+ return truncate(serialized, 140)
965
+ }
966
+
967
+ const keys = Object.keys(obj).slice(0, 8)
968
+ if (keys.length === 0) {
969
+ return '{}'
970
+ }
971
+
972
+ const parts: string[] = []
973
+ for (const key of keys) {
974
+ const val = obj[key]
975
+ if (val !== null && val !== undefined && val !== '') {
976
+ const formatted = typeof val === 'object' ? '{...}' : formatValue(val)
977
+ parts.push(key + ': ' + formatted)
978
+ }
979
+ }
980
+
981
+ return truncate(parts.join(', '), 140)
982
+ }
983
+
984
+ function safeJsonStringify(value: unknown): string | null {
985
+ try {
986
+ return JSON.stringify(value)
987
+ } catch {
988
+ return null
989
+ }
990
+ }
991
+
992
+ function truncate(value: string, maxLength: number): string {
993
+ if (value.length <= maxLength) {
994
+ return value
995
+ }
996
+ return value.slice(0, maxLength - 3) + '...'
997
+ }
998
+
999
+ function guessColumnSize(field: string, items: ImportPreviewItem[]): number {
1000
+ const sample = items.slice(0, 5)
1001
+ const values = sample.map((item) => item.input[field])
1002
+
1003
+ for (const val of values) {
1004
+ if (val === undefined || val === null) {
1005
+ continue
1006
+ }
1007
+ if (typeof val === 'object') {
1008
+ return 240
1009
+ }
1010
+ const str = String(val)
1011
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-/.test(str)) {
1012
+ return 280
1013
+ }
1014
+ if (/^\\d{4}-\\d{2}-\\d{2}T/.test(str)) {
1015
+ return 220
1016
+ }
1017
+ }
1018
+
1019
+ const lower = field.toLowerCase()
1020
+ if (lower.includes('id')) {
1021
+ return 280
1022
+ }
1023
+ if (lower.includes('date') || lower.includes('at')) {
1024
+ return 220
1025
+ }
1026
+
1027
+ return 180
1028
+ }
1029
+ `;
1030
+ }
1031
+ //# sourceMappingURL=import-review-page-review-stage.generator.js.map