@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.
- package/dist/backend-authentication/generators/authentication-service.generator.js +1 -1
- package/dist/backend-core/backend.generator.js +0 -4
- package/dist/backend-core/backend.generator.js.map +1 -1
- package/dist/backend-core/modules/backend-module-xlport.generator.js +1 -1
- package/dist/backend-excel-io/excel-io.generator.d.ts +19 -0
- package/dist/backend-excel-io/excel-io.generator.js +106 -0
- package/dist/backend-excel-io/excel-io.generator.js.map +1 -0
- package/dist/backend-excel-io/generators/excel-io-service.generator.d.ts +9 -0
- package/dist/backend-excel-io/generators/excel-io-service.generator.js +680 -0
- package/dist/backend-excel-io/generators/excel-io-service.generator.js.map +1 -0
- package/dist/backend-excel-io/index.d.ts +2 -0
- package/dist/backend-excel-io/index.js +22 -0
- package/dist/backend-excel-io/index.js.map +1 -0
- package/dist/backend-excel-io/template/README.md +24 -0
- package/dist/backend-excel-io/template/excel-io.controller.ts +195 -0
- package/dist/backend-excel-io/template/excel-io.module.ts +17 -0
- package/dist/backend-import/generators/detect-delta/detect-delta-functions.generator.js +148 -13
- package/dist/backend-import/generators/detect-delta/detect-delta-functions.generator.js.map +1 -1
- package/dist/backend-import/generators/filter-error-rows.generator.d.ts +2 -0
- package/dist/backend-import/generators/filter-error-rows.generator.js +28 -0
- package/dist/backend-import/generators/filter-error-rows.generator.js.map +1 -0
- package/dist/backend-import/generators/import-service.generator.js +126 -2
- package/dist/backend-import/generators/import-service.generator.js.map +1 -1
- package/dist/backend-import/import.generator.js +2 -0
- package/dist/backend-import/import.generator.js.map +1 -1
- package/dist/backend-repositories/generators/model-repository.generator.js +17 -2
- package/dist/backend-repositories/generators/model-repository.generator.js.map +1 -1
- package/dist/backend-router-trpc/generators/app-routes.generator.js +5 -0
- package/dist/backend-router-trpc/generators/app-routes.generator.js.map +1 -1
- package/dist/backend-router-trpc/generators/excel-io-route.generator.d.ts +4 -0
- package/dist/backend-router-trpc/generators/excel-io-route.generator.js +35 -0
- package/dist/backend-router-trpc/generators/excel-io-route.generator.js.map +1 -0
- package/dist/backend-router-trpc/generators/trpc-plugin.generator.js +6 -0
- package/dist/backend-router-trpc/generators/trpc-plugin.generator.js.map +1 -1
- package/dist/backend-router-trpc/generators/trpc-router-module.generator.js +8 -1
- package/dist/backend-router-trpc/generators/trpc-router-module.generator.js.map +1 -1
- package/dist/backend-router-trpc/generators/trpc-shared.generator.js +9 -0
- package/dist/backend-router-trpc/generators/trpc-shared.generator.js.map +1 -1
- package/dist/backend-router-trpc/router-trpc.generator.d.ts +2 -1
- package/dist/backend-router-trpc/router-trpc.generator.js +4 -0
- package/dist/backend-router-trpc/router-trpc.generator.js.map +1 -1
- package/dist/backend-update/model-update-service.generator.js +54 -0
- package/dist/backend-update/model-update-service.generator.js.map +1 -1
- package/dist/backend-update/update-actions.decoders.d.ts +2 -0
- package/dist/backend-update/update-actions.decoders.js +2 -0
- package/dist/backend-update/update-actions.decoders.js.map +1 -1
- package/dist/backend-update/update.generator.js +6 -0
- package/dist/backend-update/update.generator.js.map +1 -1
- package/dist/base/base.generator.js +0 -4
- package/dist/base/base.generator.js.map +1 -1
- package/dist/decoders/datamodel-decoder.generator.js +91 -1
- package/dist/decoders/datamodel-decoder.generator.js.map +1 -1
- package/dist/decoders/decoders.generator.d.ts +9 -0
- package/dist/decoders/decoders.generator.js +15 -0
- package/dist/decoders/decoders.generator.js.map +1 -1
- package/dist/decoders/discriminated-union.decoder.generator.js +4 -3
- package/dist/decoders/discriminated-union.decoder.generator.js.map +1 -1
- package/dist/devops/devops.generator.d.ts +5 -1
- package/dist/devops/devops.generator.js +5 -4
- package/dist/devops/devops.generator.js.map +1 -1
- package/dist/devops/generators/docker-compose-yml.generator.d.ts +1 -1
- package/dist/devops/generators/docker-compose-yml.generator.js +16 -1
- package/dist/devops/generators/docker-compose-yml.generator.js.map +1 -1
- package/dist/e2e/template/e2e/specs/example.spec.ts-snapshots/Navigate-to-homepage-and-take-snapshot-1-chromium-linux.png +0 -0
- package/dist/frontend-actions/generators/filter-utils.generator.js +1 -1
- package/dist/frontend-admin/admin.generator.d.ts +2 -0
- package/dist/frontend-admin/admin.generator.js +28 -0
- package/dist/frontend-admin/admin.generator.js.map +1 -1
- package/dist/frontend-admin/generators/admin-global-actions.generator.js +78 -0
- package/dist/frontend-admin/generators/admin-global-actions.generator.js.map +1 -1
- package/dist/frontend-admin/generators/admin-overview-page.generator.js +21 -1
- package/dist/frontend-admin/generators/admin-overview-page.generator.js.map +1 -1
- package/dist/frontend-admin/generators/admin-sidebar.generator.js +20 -0
- package/dist/frontend-admin/generators/admin-sidebar.generator.js.map +1 -1
- package/dist/frontend-admin/generators/excel-io-page.generator.d.ts +4 -0
- package/dist/frontend-admin/generators/excel-io-page.generator.js +258 -0
- package/dist/frontend-admin/generators/excel-io-page.generator.js.map +1 -0
- package/dist/frontend-admin/generators/import-review-page-result-stage.generator.d.ts +1 -0
- package/dist/frontend-admin/generators/import-review-page-result-stage.generator.js +104 -0
- package/dist/frontend-admin/generators/import-review-page-result-stage.generator.js.map +1 -0
- package/dist/frontend-admin/generators/import-review-page-review-stage.generator.d.ts +1 -0
- package/dist/frontend-admin/generators/import-review-page-review-stage.generator.js +1031 -0
- package/dist/frontend-admin/generators/import-review-page-review-stage.generator.js.map +1 -0
- package/dist/frontend-admin/generators/import-review-page-upload-stage.generator.d.ts +1 -0
- package/dist/frontend-admin/generators/import-review-page-upload-stage.generator.js +77 -0
- package/dist/frontend-admin/generators/import-review-page-upload-stage.generator.js.map +1 -0
- package/dist/frontend-admin/generators/import-review-page.generator.d.ts +7 -0
- package/dist/frontend-admin/generators/import-review-page.generator.js +180 -0
- package/dist/frontend-admin/generators/import-review-page.generator.js.map +1 -0
- package/dist/frontend-admin/generators/model-admin-page.generator.js +60 -56
- package/dist/frontend-admin/generators/model-admin-page.generator.js.map +1 -1
- package/dist/frontend-admin/utils.js +25 -33
- package/dist/frontend-admin/utils.js.map +1 -1
- package/dist/frontend-core/frontend.generator.js +1 -2
- package/dist/frontend-core/frontend.generator.js.map +1 -1
- package/dist/frontend-core/template/src/components/admin/excel-io-actions.tsx +64 -0
- package/dist/frontend-core/template/src/components/admin/table-view-panel.tsx +41 -3
- package/dist/frontend-core/template/src/hooks/use-excel-io.ts +137 -0
- package/dist/frontend-core/template/src/hooks/use-import-review.ts +143 -0
- package/dist/frontend-core/template/src/lib/excel-download.ts +28 -0
- package/dist/frontend-core/types/hook.d.ts +1 -1
- package/dist/frontend-tables/generators/model-table.generator.js +21 -13
- package/dist/frontend-tables/generators/model-table.generator.js.map +1 -1
- package/dist/frontend-trpc-client/generators/model-hook.generator.js +4 -0
- package/dist/frontend-trpc-client/generators/model-hook.generator.js.map +1 -1
- package/dist/frontend-trpc-client/trpc-client.generator.d.ts +4 -0
- package/dist/frontend-trpc-client/trpc-client.generator.js +1 -0
- package/dist/frontend-trpc-client/trpc-client.generator.js.map +1 -1
- package/dist/generators.js +2 -0
- package/dist/generators.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/seed-data/seed-data.generator.d.ts +3 -0
- package/dist/seed-data/seed-data.generator.js +45 -1
- package/dist/seed-data/seed-data.generator.js.map +1 -1
- 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
|