@lobb-js/studio 0.17.0 → 0.18.1
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/components/dataTable/table.svelte +5 -0
- package/dist/components/dataTable/table.svelte.d.ts +1 -0
- package/dist/components/detailView/utils.js +2 -3
- package/dist/components/importButton.svelte +103 -41
- package/dist/components/routes/workflows/workflows.svelte +1 -1
- package/package.json +2 -2
- package/src/lib/components/dataTable/table.svelte +5 -0
- package/src/lib/components/detailView/utils.ts +2 -2
- package/src/lib/components/importButton.svelte +103 -41
- package/src/lib/components/routes/workflows/workflows.svelte +1 -1
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
// styles
|
|
29
29
|
showLastRowBorder?: boolean;
|
|
30
30
|
showLastColumnBorder?: boolean;
|
|
31
|
+
headerBorderTop?: boolean;
|
|
31
32
|
|
|
32
33
|
// snippets
|
|
33
34
|
overrideCell?: Snippet<[any, Column, Entry]>;
|
|
@@ -70,6 +71,7 @@
|
|
|
70
71
|
selectByColumn,
|
|
71
72
|
showLastRowBorder,
|
|
72
73
|
showLastColumnBorder,
|
|
74
|
+
headerBorderTop = false,
|
|
73
75
|
parentWidth,
|
|
74
76
|
overrideCell,
|
|
75
77
|
tools,
|
|
@@ -181,6 +183,7 @@
|
|
|
181
183
|
sticky left-0 top-0 z-20
|
|
182
184
|
flex items-center p-2.5 text-xs h-10
|
|
183
185
|
border-r border-b gap-2
|
|
186
|
+
{headerBorderTop ? 'border-t' : ''}
|
|
184
187
|
{unifiedBgColor ? unifiedBgColor : 'bg-muted'}
|
|
185
188
|
"
|
|
186
189
|
>
|
|
@@ -209,6 +212,7 @@
|
|
|
209
212
|
{unifiedBgColor ? unifiedBgColor : 'bg-muted/30'}
|
|
210
213
|
{lastColumn && !showLastColumnBorder ? '' : 'border-r'}
|
|
211
214
|
border-b gap-2
|
|
215
|
+
{headerBorderTop ? 'border-t' : ''}
|
|
212
216
|
"
|
|
213
217
|
>
|
|
214
218
|
{#if sort[column.id] === "asc"}
|
|
@@ -234,6 +238,7 @@
|
|
|
234
238
|
flex items-center p-2.5 h-10
|
|
235
239
|
{unifiedBgColor ? unifiedBgColor : 'bg-muted'}
|
|
236
240
|
border-l border-b
|
|
241
|
+
{headerBorderTop ? 'border-t' : ''}
|
|
237
242
|
"
|
|
238
243
|
></div>
|
|
239
244
|
{/if}
|
|
@@ -18,6 +18,7 @@ export interface TableProps {
|
|
|
18
18
|
showCheckboxes?: boolean;
|
|
19
19
|
showLastRowBorder?: boolean;
|
|
20
20
|
showLastColumnBorder?: boolean;
|
|
21
|
+
headerBorderTop?: boolean;
|
|
21
22
|
overrideCell?: Snippet<[any, Column, Entry]>;
|
|
22
23
|
tools?: Snippet<[Entry, number]>;
|
|
23
24
|
rowActions?: Snippet<[Entry, number]>;
|
|
@@ -14,14 +14,13 @@ import { getFieldRelation } from "../../utils";
|
|
|
14
14
|
import { getField } from "../dataTable/utils";
|
|
15
15
|
export function getDefaultEntry(ctx, fieldNames, collectionName, values) {
|
|
16
16
|
return Object.fromEntries(fieldNames.map(function (fieldName) {
|
|
17
|
-
var _a;
|
|
18
17
|
var value = null;
|
|
19
18
|
var field = getField(ctx, fieldName, collectionName);
|
|
20
19
|
if (values && values[fieldName] !== undefined) {
|
|
21
20
|
value = values[fieldName];
|
|
22
21
|
}
|
|
23
|
-
else if (
|
|
24
|
-
var defualtValue = field.
|
|
22
|
+
else if (field.default !== undefined && field.default !== null) {
|
|
23
|
+
var defualtValue = field.default;
|
|
25
24
|
if (typeof defualtValue === "string") {
|
|
26
25
|
value = Mustache.render(defualtValue, {
|
|
27
26
|
now: new Date().toISOString(),
|
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
import { toast } from "svelte-sonner";
|
|
5
5
|
import { getStudioContext } from "../context";
|
|
6
6
|
import * as Dialog from "./ui/dialog";
|
|
7
|
-
import Table from "./dataTable/table.svelte";
|
|
8
|
-
import FieldCell from "./dataTable/fieldCell.svelte";
|
|
9
7
|
import { getCollectionColumns } from "./dataTable/utils";
|
|
8
|
+
import Table from "./dataTable/table.svelte";
|
|
10
9
|
import Fuse from "fuse.js";
|
|
11
10
|
import { emitEvent } from "../eventSystem";
|
|
12
11
|
|
|
@@ -21,11 +20,12 @@
|
|
|
21
20
|
|
|
22
21
|
let openDrawer = $state(false);
|
|
23
22
|
let activeTab = $state<"upload" | "paste">("upload");
|
|
24
|
-
let step = $state<"input" | "processing" | "preview">("input");
|
|
23
|
+
let step = $state<"input" | "processing" | "preview" | "results">("input");
|
|
25
24
|
let pasteContent = $state("");
|
|
26
25
|
let isDragging = $state(false);
|
|
27
26
|
let parseError = $state("");
|
|
28
27
|
let transformedRows = $state<any[]>([]);
|
|
28
|
+
let importResults = $state<{ row: any; error: string | null }[]>([]);
|
|
29
29
|
|
|
30
30
|
const collectionColumns = $derived(getCollectionColumns(ctx, collectionName) ?? []);
|
|
31
31
|
|
|
@@ -43,8 +43,7 @@
|
|
|
43
43
|
Object.fromEntries(
|
|
44
44
|
collectionColumns.map((c) => {
|
|
45
45
|
const csvCol = Object.keys(mapping).find((k) => mapping[k] === c.id);
|
|
46
|
-
|
|
47
|
-
return [c.id, raw];
|
|
46
|
+
return [c.id, csvCol ? row[csvCol] : null];
|
|
48
47
|
})
|
|
49
48
|
)
|
|
50
49
|
);
|
|
@@ -99,13 +98,7 @@
|
|
|
99
98
|
try {
|
|
100
99
|
const rows = detectAndParse(content);
|
|
101
100
|
if (rows.length === 0) throw new Error("No data rows found");
|
|
102
|
-
|
|
103
|
-
step = "processing";
|
|
104
|
-
const eventResult = await emitEvent({ lobb, ctx }, "studio.collections.import", {
|
|
105
|
-
collectionName,
|
|
106
|
-
rows: mapped,
|
|
107
|
-
});
|
|
108
|
-
transformedRows = eventResult.rows ?? mapped;
|
|
101
|
+
transformedRows = applyColumnMapping(rows);
|
|
109
102
|
step = "preview";
|
|
110
103
|
} catch (e: any) {
|
|
111
104
|
step = "input";
|
|
@@ -126,14 +119,36 @@
|
|
|
126
119
|
}
|
|
127
120
|
|
|
128
121
|
async function handleImport() {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
122
|
+
step = "processing";
|
|
123
|
+
try {
|
|
124
|
+
const eventResult = await emitEvent({ lobb, ctx }, "studio.collections.import", {
|
|
125
|
+
collectionName,
|
|
126
|
+
rows: transformedRows.map((r) => ({ ...r })),
|
|
127
|
+
});
|
|
128
|
+
const finalRows = eventResult.rows ?? transformedRows;
|
|
129
|
+
|
|
130
|
+
const results: { row: any; error: string | null }[] = [];
|
|
131
|
+
for (const row of finalRows) {
|
|
132
|
+
const response = await lobb.createOne(collectionName, row);
|
|
133
|
+
if (response.ok) {
|
|
134
|
+
results.push({ row, error: null });
|
|
135
|
+
} else {
|
|
136
|
+
const body = await response.json().catch(() => null);
|
|
137
|
+
const message = body?.details
|
|
138
|
+
? Object.entries(body.details).map(([f, msgs]) => `${f}: ${(msgs as string[]).join(", ")}`).join(" | ")
|
|
139
|
+
: (body?.message ?? `HTTP ${response.status}`);
|
|
140
|
+
results.push({ row, error: message });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
importResults = results;
|
|
145
|
+
const succeeded = results.filter((r) => r.error === null);
|
|
146
|
+
|
|
147
|
+
if (succeeded.length > 0 && onSuccessfullSave) await onSuccessfullSave();
|
|
148
|
+
step = "results";
|
|
149
|
+
} catch (e: any) {
|
|
150
|
+
step = "preview";
|
|
133
151
|
}
|
|
134
|
-
toast.success(`Imported ${transformedRows.length} records`);
|
|
135
|
-
if (onSuccessfullSave) await onSuccessfullSave();
|
|
136
|
-
hideDrawer();
|
|
137
152
|
}
|
|
138
153
|
|
|
139
154
|
function showDrawer() {
|
|
@@ -143,11 +158,14 @@
|
|
|
143
158
|
|
|
144
159
|
function hideDrawer() {
|
|
145
160
|
openDrawer = false;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
step = "input";
|
|
163
|
+
activeTab = "upload";
|
|
164
|
+
pasteContent = "";
|
|
165
|
+
transformedRows = [];
|
|
166
|
+
importResults = [];
|
|
167
|
+
parseError = "";
|
|
168
|
+
}, 200);
|
|
151
169
|
}
|
|
152
170
|
</script>
|
|
153
171
|
|
|
@@ -162,7 +180,7 @@
|
|
|
162
180
|
onOpenChange={(open) => { if (!open) hideDrawer(); }}
|
|
163
181
|
>
|
|
164
182
|
<Dialog.Content
|
|
165
|
-
class="flex flex-col gap-0 p-0 overflow-
|
|
183
|
+
class="flex flex-col gap-0 p-0 overflow-clip {step === 'preview' || (step === 'results' && importResults.some((r) => r.error !== null)) ? 'max-w-5xl h-[80vh]' : 'max-w-lg'}"
|
|
166
184
|
>
|
|
167
185
|
<!-- Header -->
|
|
168
186
|
<div class="flex h-12 shrink-0 items-center justify-between border-b px-4">
|
|
@@ -244,29 +262,19 @@
|
|
|
244
262
|
{/if}
|
|
245
263
|
</div>
|
|
246
264
|
|
|
247
|
-
{:else}
|
|
265
|
+
{:else if step === "preview"}
|
|
248
266
|
<!-- Preview -->
|
|
249
|
-
<div class="shrink-0
|
|
267
|
+
<div class="shrink-0 px-4 py-2 text-sm text-muted-foreground">
|
|
250
268
|
{transformedRows.length} rows ready to import
|
|
251
269
|
</div>
|
|
252
|
-
<div class="flex-1 overflow-auto">
|
|
270
|
+
<div class="relative flex-1 overflow-auto w-full">
|
|
253
271
|
<Table
|
|
254
272
|
data={transformedRows}
|
|
255
|
-
columns={collectionColumns}
|
|
256
|
-
showLastRowBorder={true}
|
|
257
|
-
showLastColumnBorder={true}
|
|
273
|
+
columns={collectionColumns.filter((c) => c.id !== "id")}
|
|
258
274
|
showCheckboxes={false}
|
|
259
275
|
unifiedBgColor="bg-background"
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
<FieldCell
|
|
263
|
-
{collectionName}
|
|
264
|
-
fieldName={column.id}
|
|
265
|
-
{value}
|
|
266
|
-
{entry}
|
|
267
|
-
/>
|
|
268
|
-
{/snippet}
|
|
269
|
-
</Table>
|
|
276
|
+
headerBorderTop={true}
|
|
277
|
+
/>
|
|
270
278
|
</div>
|
|
271
279
|
|
|
272
280
|
<div class="flex h-12 shrink-0 items-center justify-end gap-2 border-t px-4">
|
|
@@ -277,6 +285,60 @@
|
|
|
277
285
|
Import {transformedRows.length} records
|
|
278
286
|
</Button>
|
|
279
287
|
</div>
|
|
288
|
+
|
|
289
|
+
{:else if step === "results"}
|
|
290
|
+
{@const failed = importResults.filter((r) => r.error !== null)}
|
|
291
|
+
{@const succeeded = importResults.filter((r) => r.error === null)}
|
|
292
|
+
{@const failedData = failed.map((r) => ({ __error: r.error, ...r.row }))}
|
|
293
|
+
{#if failed.length === 0}
|
|
294
|
+
<div class="flex flex-1 flex-col items-center justify-center gap-6 px-8 py-12">
|
|
295
|
+
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-muted">
|
|
296
|
+
<Check size="22" class="text-foreground" />
|
|
297
|
+
</div>
|
|
298
|
+
<div class="text-center">
|
|
299
|
+
<p class="text-sm font-medium text-foreground">Import complete</p>
|
|
300
|
+
<p class="mt-1 text-xs text-muted-foreground">{succeeded.length} {succeeded.length === 1 ? "record" : "records"} imported successfully</p>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
{:else}
|
|
304
|
+
<!-- Summary strip -->
|
|
305
|
+
<div class="shrink-0 px-4 py-3 flex items-center gap-4">
|
|
306
|
+
<div class="flex items-center gap-1.5">
|
|
307
|
+
<span class="inline-flex h-5 w-5 items-center justify-center rounded-full bg-muted">
|
|
308
|
+
<Check size="11" class="text-foreground" />
|
|
309
|
+
</span>
|
|
310
|
+
<span class="text-xs text-muted-foreground">{succeeded.length} imported</span>
|
|
311
|
+
</div>
|
|
312
|
+
<div class="flex items-center gap-1.5">
|
|
313
|
+
<span class="inline-flex h-5 w-5 items-center justify-center rounded-full bg-destructive/10">
|
|
314
|
+
<X size="11" class="text-destructive" />
|
|
315
|
+
</span>
|
|
316
|
+
<span class="text-xs text-muted-foreground">{failed.length} failed</span>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
<div class="relative flex-1 overflow-auto w-full">
|
|
320
|
+
<Table
|
|
321
|
+
data={failedData}
|
|
322
|
+
columns={[{ id: "__error", icon: AlertCircle }, ...collectionColumns.filter((c) => c.id !== "id")]}
|
|
323
|
+
showCheckboxes={false}
|
|
324
|
+
unifiedBgColor="bg-background"
|
|
325
|
+
headerBorderTop={true}
|
|
326
|
+
>
|
|
327
|
+
{#snippet overrideCell(value, column)}
|
|
328
|
+
{#if column.id === "__error"}
|
|
329
|
+
<span class="text-destructive">{value}</span>
|
|
330
|
+
{:else}
|
|
331
|
+
{value}
|
|
332
|
+
{/if}
|
|
333
|
+
{/snippet}
|
|
334
|
+
</Table>
|
|
335
|
+
</div>
|
|
336
|
+
{/if}
|
|
337
|
+
<div class="flex h-12 shrink-0 items-center justify-end gap-2 border-t px-4">
|
|
338
|
+
<Button onclick={hideDrawer} class="h-7 px-3 text-xs font-normal" Icon={Check}>
|
|
339
|
+
Done
|
|
340
|
+
</Button>
|
|
341
|
+
</div>
|
|
280
342
|
{/if}
|
|
281
343
|
</Dialog.Content>
|
|
282
344
|
</Dialog.Root>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobb-js/studio",
|
|
3
3
|
"license": "UNLICENSED",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.18.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"postpublish": "./scripts/postpublish.sh"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@lobb-js/core": "^0.
|
|
45
|
+
"@lobb-js/core": "^0.23.0",
|
|
46
46
|
"@chromatic-com/storybook": "^4.1.2",
|
|
47
47
|
"@storybook/addon-a11y": "^10.0.1",
|
|
48
48
|
"@storybook/addon-docs": "^10.0.1",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
// styles
|
|
29
29
|
showLastRowBorder?: boolean;
|
|
30
30
|
showLastColumnBorder?: boolean;
|
|
31
|
+
headerBorderTop?: boolean;
|
|
31
32
|
|
|
32
33
|
// snippets
|
|
33
34
|
overrideCell?: Snippet<[any, Column, Entry]>;
|
|
@@ -70,6 +71,7 @@
|
|
|
70
71
|
selectByColumn,
|
|
71
72
|
showLastRowBorder,
|
|
72
73
|
showLastColumnBorder,
|
|
74
|
+
headerBorderTop = false,
|
|
73
75
|
parentWidth,
|
|
74
76
|
overrideCell,
|
|
75
77
|
tools,
|
|
@@ -181,6 +183,7 @@
|
|
|
181
183
|
sticky left-0 top-0 z-20
|
|
182
184
|
flex items-center p-2.5 text-xs h-10
|
|
183
185
|
border-r border-b gap-2
|
|
186
|
+
{headerBorderTop ? 'border-t' : ''}
|
|
184
187
|
{unifiedBgColor ? unifiedBgColor : 'bg-muted'}
|
|
185
188
|
"
|
|
186
189
|
>
|
|
@@ -209,6 +212,7 @@
|
|
|
209
212
|
{unifiedBgColor ? unifiedBgColor : 'bg-muted/30'}
|
|
210
213
|
{lastColumn && !showLastColumnBorder ? '' : 'border-r'}
|
|
211
214
|
border-b gap-2
|
|
215
|
+
{headerBorderTop ? 'border-t' : ''}
|
|
212
216
|
"
|
|
213
217
|
>
|
|
214
218
|
{#if sort[column.id] === "asc"}
|
|
@@ -234,6 +238,7 @@
|
|
|
234
238
|
flex items-center p-2.5 h-10
|
|
235
239
|
{unifiedBgColor ? unifiedBgColor : 'bg-muted'}
|
|
236
240
|
border-l border-b
|
|
241
|
+
{headerBorderTop ? 'border-t' : ''}
|
|
237
242
|
"
|
|
238
243
|
></div>
|
|
239
244
|
{/if}
|
|
@@ -11,8 +11,8 @@ export function getDefaultEntry(ctx: CTX, fieldNames: string[], collectionName:
|
|
|
11
11
|
const field = getField(ctx, fieldName, collectionName);
|
|
12
12
|
if (values && values[fieldName] !== undefined) {
|
|
13
13
|
value = values[fieldName];
|
|
14
|
-
} else if (field.
|
|
15
|
-
const defualtValue = field.
|
|
14
|
+
} else if (field.default !== undefined && field.default !== null) {
|
|
15
|
+
const defualtValue = field.default;
|
|
16
16
|
if (typeof defualtValue === "string") {
|
|
17
17
|
value = Mustache.render(defualtValue, {
|
|
18
18
|
now: new Date().toISOString(),
|
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
import { toast } from "svelte-sonner";
|
|
5
5
|
import { getStudioContext } from "../context";
|
|
6
6
|
import * as Dialog from "./ui/dialog";
|
|
7
|
-
import Table from "./dataTable/table.svelte";
|
|
8
|
-
import FieldCell from "./dataTable/fieldCell.svelte";
|
|
9
7
|
import { getCollectionColumns } from "./dataTable/utils";
|
|
8
|
+
import Table from "./dataTable/table.svelte";
|
|
10
9
|
import Fuse from "fuse.js";
|
|
11
10
|
import { emitEvent } from "../eventSystem";
|
|
12
11
|
|
|
@@ -21,11 +20,12 @@
|
|
|
21
20
|
|
|
22
21
|
let openDrawer = $state(false);
|
|
23
22
|
let activeTab = $state<"upload" | "paste">("upload");
|
|
24
|
-
let step = $state<"input" | "processing" | "preview">("input");
|
|
23
|
+
let step = $state<"input" | "processing" | "preview" | "results">("input");
|
|
25
24
|
let pasteContent = $state("");
|
|
26
25
|
let isDragging = $state(false);
|
|
27
26
|
let parseError = $state("");
|
|
28
27
|
let transformedRows = $state<any[]>([]);
|
|
28
|
+
let importResults = $state<{ row: any; error: string | null }[]>([]);
|
|
29
29
|
|
|
30
30
|
const collectionColumns = $derived(getCollectionColumns(ctx, collectionName) ?? []);
|
|
31
31
|
|
|
@@ -43,8 +43,7 @@
|
|
|
43
43
|
Object.fromEntries(
|
|
44
44
|
collectionColumns.map((c) => {
|
|
45
45
|
const csvCol = Object.keys(mapping).find((k) => mapping[k] === c.id);
|
|
46
|
-
|
|
47
|
-
return [c.id, raw];
|
|
46
|
+
return [c.id, csvCol ? row[csvCol] : null];
|
|
48
47
|
})
|
|
49
48
|
)
|
|
50
49
|
);
|
|
@@ -99,13 +98,7 @@
|
|
|
99
98
|
try {
|
|
100
99
|
const rows = detectAndParse(content);
|
|
101
100
|
if (rows.length === 0) throw new Error("No data rows found");
|
|
102
|
-
|
|
103
|
-
step = "processing";
|
|
104
|
-
const eventResult = await emitEvent({ lobb, ctx }, "studio.collections.import", {
|
|
105
|
-
collectionName,
|
|
106
|
-
rows: mapped,
|
|
107
|
-
});
|
|
108
|
-
transformedRows = eventResult.rows ?? mapped;
|
|
101
|
+
transformedRows = applyColumnMapping(rows);
|
|
109
102
|
step = "preview";
|
|
110
103
|
} catch (e: any) {
|
|
111
104
|
step = "input";
|
|
@@ -126,14 +119,36 @@
|
|
|
126
119
|
}
|
|
127
120
|
|
|
128
121
|
async function handleImport() {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
122
|
+
step = "processing";
|
|
123
|
+
try {
|
|
124
|
+
const eventResult = await emitEvent({ lobb, ctx }, "studio.collections.import", {
|
|
125
|
+
collectionName,
|
|
126
|
+
rows: transformedRows.map((r) => ({ ...r })),
|
|
127
|
+
});
|
|
128
|
+
const finalRows = eventResult.rows ?? transformedRows;
|
|
129
|
+
|
|
130
|
+
const results: { row: any; error: string | null }[] = [];
|
|
131
|
+
for (const row of finalRows) {
|
|
132
|
+
const response = await lobb.createOne(collectionName, row);
|
|
133
|
+
if (response.ok) {
|
|
134
|
+
results.push({ row, error: null });
|
|
135
|
+
} else {
|
|
136
|
+
const body = await response.json().catch(() => null);
|
|
137
|
+
const message = body?.details
|
|
138
|
+
? Object.entries(body.details).map(([f, msgs]) => `${f}: ${(msgs as string[]).join(", ")}`).join(" | ")
|
|
139
|
+
: (body?.message ?? `HTTP ${response.status}`);
|
|
140
|
+
results.push({ row, error: message });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
importResults = results;
|
|
145
|
+
const succeeded = results.filter((r) => r.error === null);
|
|
146
|
+
|
|
147
|
+
if (succeeded.length > 0 && onSuccessfullSave) await onSuccessfullSave();
|
|
148
|
+
step = "results";
|
|
149
|
+
} catch (e: any) {
|
|
150
|
+
step = "preview";
|
|
133
151
|
}
|
|
134
|
-
toast.success(`Imported ${transformedRows.length} records`);
|
|
135
|
-
if (onSuccessfullSave) await onSuccessfullSave();
|
|
136
|
-
hideDrawer();
|
|
137
152
|
}
|
|
138
153
|
|
|
139
154
|
function showDrawer() {
|
|
@@ -143,11 +158,14 @@
|
|
|
143
158
|
|
|
144
159
|
function hideDrawer() {
|
|
145
160
|
openDrawer = false;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
step = "input";
|
|
163
|
+
activeTab = "upload";
|
|
164
|
+
pasteContent = "";
|
|
165
|
+
transformedRows = [];
|
|
166
|
+
importResults = [];
|
|
167
|
+
parseError = "";
|
|
168
|
+
}, 200);
|
|
151
169
|
}
|
|
152
170
|
</script>
|
|
153
171
|
|
|
@@ -162,7 +180,7 @@
|
|
|
162
180
|
onOpenChange={(open) => { if (!open) hideDrawer(); }}
|
|
163
181
|
>
|
|
164
182
|
<Dialog.Content
|
|
165
|
-
class="flex flex-col gap-0 p-0 overflow-
|
|
183
|
+
class="flex flex-col gap-0 p-0 overflow-clip {step === 'preview' || (step === 'results' && importResults.some((r) => r.error !== null)) ? 'max-w-5xl h-[80vh]' : 'max-w-lg'}"
|
|
166
184
|
>
|
|
167
185
|
<!-- Header -->
|
|
168
186
|
<div class="flex h-12 shrink-0 items-center justify-between border-b px-4">
|
|
@@ -244,29 +262,19 @@
|
|
|
244
262
|
{/if}
|
|
245
263
|
</div>
|
|
246
264
|
|
|
247
|
-
{:else}
|
|
265
|
+
{:else if step === "preview"}
|
|
248
266
|
<!-- Preview -->
|
|
249
|
-
<div class="shrink-0
|
|
267
|
+
<div class="shrink-0 px-4 py-2 text-sm text-muted-foreground">
|
|
250
268
|
{transformedRows.length} rows ready to import
|
|
251
269
|
</div>
|
|
252
|
-
<div class="flex-1 overflow-auto">
|
|
270
|
+
<div class="relative flex-1 overflow-auto w-full">
|
|
253
271
|
<Table
|
|
254
272
|
data={transformedRows}
|
|
255
|
-
columns={collectionColumns}
|
|
256
|
-
showLastRowBorder={true}
|
|
257
|
-
showLastColumnBorder={true}
|
|
273
|
+
columns={collectionColumns.filter((c) => c.id !== "id")}
|
|
258
274
|
showCheckboxes={false}
|
|
259
275
|
unifiedBgColor="bg-background"
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
<FieldCell
|
|
263
|
-
{collectionName}
|
|
264
|
-
fieldName={column.id}
|
|
265
|
-
{value}
|
|
266
|
-
{entry}
|
|
267
|
-
/>
|
|
268
|
-
{/snippet}
|
|
269
|
-
</Table>
|
|
276
|
+
headerBorderTop={true}
|
|
277
|
+
/>
|
|
270
278
|
</div>
|
|
271
279
|
|
|
272
280
|
<div class="flex h-12 shrink-0 items-center justify-end gap-2 border-t px-4">
|
|
@@ -277,6 +285,60 @@
|
|
|
277
285
|
Import {transformedRows.length} records
|
|
278
286
|
</Button>
|
|
279
287
|
</div>
|
|
288
|
+
|
|
289
|
+
{:else if step === "results"}
|
|
290
|
+
{@const failed = importResults.filter((r) => r.error !== null)}
|
|
291
|
+
{@const succeeded = importResults.filter((r) => r.error === null)}
|
|
292
|
+
{@const failedData = failed.map((r) => ({ __error: r.error, ...r.row }))}
|
|
293
|
+
{#if failed.length === 0}
|
|
294
|
+
<div class="flex flex-1 flex-col items-center justify-center gap-6 px-8 py-12">
|
|
295
|
+
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-muted">
|
|
296
|
+
<Check size="22" class="text-foreground" />
|
|
297
|
+
</div>
|
|
298
|
+
<div class="text-center">
|
|
299
|
+
<p class="text-sm font-medium text-foreground">Import complete</p>
|
|
300
|
+
<p class="mt-1 text-xs text-muted-foreground">{succeeded.length} {succeeded.length === 1 ? "record" : "records"} imported successfully</p>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
{:else}
|
|
304
|
+
<!-- Summary strip -->
|
|
305
|
+
<div class="shrink-0 px-4 py-3 flex items-center gap-4">
|
|
306
|
+
<div class="flex items-center gap-1.5">
|
|
307
|
+
<span class="inline-flex h-5 w-5 items-center justify-center rounded-full bg-muted">
|
|
308
|
+
<Check size="11" class="text-foreground" />
|
|
309
|
+
</span>
|
|
310
|
+
<span class="text-xs text-muted-foreground">{succeeded.length} imported</span>
|
|
311
|
+
</div>
|
|
312
|
+
<div class="flex items-center gap-1.5">
|
|
313
|
+
<span class="inline-flex h-5 w-5 items-center justify-center rounded-full bg-destructive/10">
|
|
314
|
+
<X size="11" class="text-destructive" />
|
|
315
|
+
</span>
|
|
316
|
+
<span class="text-xs text-muted-foreground">{failed.length} failed</span>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
<div class="relative flex-1 overflow-auto w-full">
|
|
320
|
+
<Table
|
|
321
|
+
data={failedData}
|
|
322
|
+
columns={[{ id: "__error", icon: AlertCircle }, ...collectionColumns.filter((c) => c.id !== "id")]}
|
|
323
|
+
showCheckboxes={false}
|
|
324
|
+
unifiedBgColor="bg-background"
|
|
325
|
+
headerBorderTop={true}
|
|
326
|
+
>
|
|
327
|
+
{#snippet overrideCell(value, column)}
|
|
328
|
+
{#if column.id === "__error"}
|
|
329
|
+
<span class="text-destructive">{value}</span>
|
|
330
|
+
{:else}
|
|
331
|
+
{value}
|
|
332
|
+
{/if}
|
|
333
|
+
{/snippet}
|
|
334
|
+
</Table>
|
|
335
|
+
</div>
|
|
336
|
+
{/if}
|
|
337
|
+
<div class="flex h-12 shrink-0 items-center justify-end gap-2 border-t px-4">
|
|
338
|
+
<Button onclick={hideDrawer} class="h-7 px-3 text-xs font-normal" Icon={Check}>
|
|
339
|
+
Done
|
|
340
|
+
</Button>
|
|
341
|
+
</div>
|
|
280
342
|
{/if}
|
|
281
343
|
</Dialog.Content>
|
|
282
344
|
</Dialog.Root>
|