@lobb-js/studio 0.16.0 → 0.18.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/components/detailView/utils.js +2 -3
- package/dist/components/importButton.svelte +100 -39
- package/dist/components/routes/workflows/workflows.svelte +1 -1
- package/package.json +2 -2
- package/src/lib/components/detailView/utils.ts +2 -2
- package/src/lib/components/importButton.svelte +100 -39
- package/src/lib/components/routes/workflows/workflows.svelte +1 -1
|
@@ -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,7 +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
|
-
return [c.id, csvCol ? row[csvCol]
|
|
46
|
+
return [c.id, csvCol ? row[csvCol] : null];
|
|
47
47
|
})
|
|
48
48
|
)
|
|
49
49
|
);
|
|
@@ -98,13 +98,7 @@
|
|
|
98
98
|
try {
|
|
99
99
|
const rows = detectAndParse(content);
|
|
100
100
|
if (rows.length === 0) throw new Error("No data rows found");
|
|
101
|
-
|
|
102
|
-
step = "processing";
|
|
103
|
-
const eventResult = await emitEvent({ lobb, ctx }, "studio.collections.import", {
|
|
104
|
-
collectionName,
|
|
105
|
-
rows: mapped,
|
|
106
|
-
});
|
|
107
|
-
transformedRows = eventResult.rows ?? mapped;
|
|
101
|
+
transformedRows = applyColumnMapping(rows);
|
|
108
102
|
step = "preview";
|
|
109
103
|
} catch (e: any) {
|
|
110
104
|
step = "input";
|
|
@@ -125,14 +119,36 @@
|
|
|
125
119
|
}
|
|
126
120
|
|
|
127
121
|
async function handleImport() {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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";
|
|
132
151
|
}
|
|
133
|
-
toast.success(`Imported ${transformedRows.length} records`);
|
|
134
|
-
if (onSuccessfullSave) await onSuccessfullSave();
|
|
135
|
-
hideDrawer();
|
|
136
152
|
}
|
|
137
153
|
|
|
138
154
|
function showDrawer() {
|
|
@@ -142,11 +158,14 @@
|
|
|
142
158
|
|
|
143
159
|
function hideDrawer() {
|
|
144
160
|
openDrawer = false;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
step = "input";
|
|
163
|
+
activeTab = "upload";
|
|
164
|
+
pasteContent = "";
|
|
165
|
+
transformedRows = [];
|
|
166
|
+
importResults = [];
|
|
167
|
+
parseError = "";
|
|
168
|
+
}, 200);
|
|
150
169
|
}
|
|
151
170
|
</script>
|
|
152
171
|
|
|
@@ -161,7 +180,7 @@
|
|
|
161
180
|
onOpenChange={(open) => { if (!open) hideDrawer(); }}
|
|
162
181
|
>
|
|
163
182
|
<Dialog.Content
|
|
164
|
-
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'}"
|
|
165
184
|
>
|
|
166
185
|
<!-- Header -->
|
|
167
186
|
<div class="flex h-12 shrink-0 items-center justify-between border-b px-4">
|
|
@@ -243,29 +262,18 @@
|
|
|
243
262
|
{/if}
|
|
244
263
|
</div>
|
|
245
264
|
|
|
246
|
-
{:else}
|
|
265
|
+
{:else if step === "preview"}
|
|
247
266
|
<!-- Preview -->
|
|
248
267
|
<div class="shrink-0 border-b px-4 py-2 text-sm text-muted-foreground">
|
|
249
268
|
{transformedRows.length} rows ready to import
|
|
250
269
|
</div>
|
|
251
|
-
<div class="flex-1 overflow-auto">
|
|
270
|
+
<div class="relative flex-1 overflow-auto w-full">
|
|
252
271
|
<Table
|
|
253
272
|
data={transformedRows}
|
|
254
|
-
columns={collectionColumns}
|
|
255
|
-
showLastRowBorder={true}
|
|
256
|
-
showLastColumnBorder={true}
|
|
273
|
+
columns={collectionColumns.filter((c) => c.id !== "id")}
|
|
257
274
|
showCheckboxes={false}
|
|
258
275
|
unifiedBgColor="bg-background"
|
|
259
|
-
|
|
260
|
-
{#snippet overrideCell(value, column, entry)}
|
|
261
|
-
<FieldCell
|
|
262
|
-
{collectionName}
|
|
263
|
-
fieldName={column.id}
|
|
264
|
-
{value}
|
|
265
|
-
{entry}
|
|
266
|
-
/>
|
|
267
|
-
{/snippet}
|
|
268
|
-
</Table>
|
|
276
|
+
/>
|
|
269
277
|
</div>
|
|
270
278
|
|
|
271
279
|
<div class="flex h-12 shrink-0 items-center justify-end gap-2 border-t px-4">
|
|
@@ -276,6 +284,59 @@
|
|
|
276
284
|
Import {transformedRows.length} records
|
|
277
285
|
</Button>
|
|
278
286
|
</div>
|
|
287
|
+
|
|
288
|
+
{:else if step === "results"}
|
|
289
|
+
{@const failed = importResults.filter((r) => r.error !== null)}
|
|
290
|
+
{@const succeeded = importResults.filter((r) => r.error === null)}
|
|
291
|
+
{@const failedData = failed.map((r) => ({ __error: r.error, ...r.row }))}
|
|
292
|
+
{#if failed.length === 0}
|
|
293
|
+
<div class="flex flex-1 flex-col items-center justify-center gap-6 px-8 py-12">
|
|
294
|
+
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-muted">
|
|
295
|
+
<Check size="22" class="text-foreground" />
|
|
296
|
+
</div>
|
|
297
|
+
<div class="text-center">
|
|
298
|
+
<p class="text-sm font-medium text-foreground">Import complete</p>
|
|
299
|
+
<p class="mt-1 text-xs text-muted-foreground">{succeeded.length} {succeeded.length === 1 ? "record" : "records"} imported successfully</p>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
{:else}
|
|
303
|
+
<!-- Summary strip -->
|
|
304
|
+
<div class="shrink-0 border-b px-4 py-3 flex items-center gap-4">
|
|
305
|
+
<div class="flex items-center gap-1.5">
|
|
306
|
+
<span class="inline-flex h-5 w-5 items-center justify-center rounded-full bg-muted">
|
|
307
|
+
<Check size="11" class="text-foreground" />
|
|
308
|
+
</span>
|
|
309
|
+
<span class="text-xs text-muted-foreground">{succeeded.length} imported</span>
|
|
310
|
+
</div>
|
|
311
|
+
<div class="flex items-center gap-1.5">
|
|
312
|
+
<span class="inline-flex h-5 w-5 items-center justify-center rounded-full bg-destructive/10">
|
|
313
|
+
<X size="11" class="text-destructive" />
|
|
314
|
+
</span>
|
|
315
|
+
<span class="text-xs text-muted-foreground">{failed.length} failed</span>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
<div class="relative flex-1 overflow-auto w-full">
|
|
319
|
+
<Table
|
|
320
|
+
data={failedData}
|
|
321
|
+
columns={[{ id: "__error", icon: AlertCircle }, ...collectionColumns.filter((c) => c.id !== "id")]}
|
|
322
|
+
showCheckboxes={false}
|
|
323
|
+
unifiedBgColor="bg-background"
|
|
324
|
+
>
|
|
325
|
+
{#snippet overrideCell(value, column)}
|
|
326
|
+
{#if column.id === "__error"}
|
|
327
|
+
<span class="text-destructive">{value}</span>
|
|
328
|
+
{:else}
|
|
329
|
+
{value}
|
|
330
|
+
{/if}
|
|
331
|
+
{/snippet}
|
|
332
|
+
</Table>
|
|
333
|
+
</div>
|
|
334
|
+
{/if}
|
|
335
|
+
<div class="flex h-12 shrink-0 items-center justify-end gap-2 border-t px-4">
|
|
336
|
+
<Button onclick={hideDrawer} class="h-7 px-3 text-xs font-normal" Icon={Check}>
|
|
337
|
+
Done
|
|
338
|
+
</Button>
|
|
339
|
+
</div>
|
|
279
340
|
{/if}
|
|
280
341
|
</Dialog.Content>
|
|
281
342
|
</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.0",
|
|
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",
|
|
@@ -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,7 +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
|
-
return [c.id, csvCol ? row[csvCol]
|
|
46
|
+
return [c.id, csvCol ? row[csvCol] : null];
|
|
47
47
|
})
|
|
48
48
|
)
|
|
49
49
|
);
|
|
@@ -98,13 +98,7 @@
|
|
|
98
98
|
try {
|
|
99
99
|
const rows = detectAndParse(content);
|
|
100
100
|
if (rows.length === 0) throw new Error("No data rows found");
|
|
101
|
-
|
|
102
|
-
step = "processing";
|
|
103
|
-
const eventResult = await emitEvent({ lobb, ctx }, "studio.collections.import", {
|
|
104
|
-
collectionName,
|
|
105
|
-
rows: mapped,
|
|
106
|
-
});
|
|
107
|
-
transformedRows = eventResult.rows ?? mapped;
|
|
101
|
+
transformedRows = applyColumnMapping(rows);
|
|
108
102
|
step = "preview";
|
|
109
103
|
} catch (e: any) {
|
|
110
104
|
step = "input";
|
|
@@ -125,14 +119,36 @@
|
|
|
125
119
|
}
|
|
126
120
|
|
|
127
121
|
async function handleImport() {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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";
|
|
132
151
|
}
|
|
133
|
-
toast.success(`Imported ${transformedRows.length} records`);
|
|
134
|
-
if (onSuccessfullSave) await onSuccessfullSave();
|
|
135
|
-
hideDrawer();
|
|
136
152
|
}
|
|
137
153
|
|
|
138
154
|
function showDrawer() {
|
|
@@ -142,11 +158,14 @@
|
|
|
142
158
|
|
|
143
159
|
function hideDrawer() {
|
|
144
160
|
openDrawer = false;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
step = "input";
|
|
163
|
+
activeTab = "upload";
|
|
164
|
+
pasteContent = "";
|
|
165
|
+
transformedRows = [];
|
|
166
|
+
importResults = [];
|
|
167
|
+
parseError = "";
|
|
168
|
+
}, 200);
|
|
150
169
|
}
|
|
151
170
|
</script>
|
|
152
171
|
|
|
@@ -161,7 +180,7 @@
|
|
|
161
180
|
onOpenChange={(open) => { if (!open) hideDrawer(); }}
|
|
162
181
|
>
|
|
163
182
|
<Dialog.Content
|
|
164
|
-
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'}"
|
|
165
184
|
>
|
|
166
185
|
<!-- Header -->
|
|
167
186
|
<div class="flex h-12 shrink-0 items-center justify-between border-b px-4">
|
|
@@ -243,29 +262,18 @@
|
|
|
243
262
|
{/if}
|
|
244
263
|
</div>
|
|
245
264
|
|
|
246
|
-
{:else}
|
|
265
|
+
{:else if step === "preview"}
|
|
247
266
|
<!-- Preview -->
|
|
248
267
|
<div class="shrink-0 border-b px-4 py-2 text-sm text-muted-foreground">
|
|
249
268
|
{transformedRows.length} rows ready to import
|
|
250
269
|
</div>
|
|
251
|
-
<div class="flex-1 overflow-auto">
|
|
270
|
+
<div class="relative flex-1 overflow-auto w-full">
|
|
252
271
|
<Table
|
|
253
272
|
data={transformedRows}
|
|
254
|
-
columns={collectionColumns}
|
|
255
|
-
showLastRowBorder={true}
|
|
256
|
-
showLastColumnBorder={true}
|
|
273
|
+
columns={collectionColumns.filter((c) => c.id !== "id")}
|
|
257
274
|
showCheckboxes={false}
|
|
258
275
|
unifiedBgColor="bg-background"
|
|
259
|
-
|
|
260
|
-
{#snippet overrideCell(value, column, entry)}
|
|
261
|
-
<FieldCell
|
|
262
|
-
{collectionName}
|
|
263
|
-
fieldName={column.id}
|
|
264
|
-
{value}
|
|
265
|
-
{entry}
|
|
266
|
-
/>
|
|
267
|
-
{/snippet}
|
|
268
|
-
</Table>
|
|
276
|
+
/>
|
|
269
277
|
</div>
|
|
270
278
|
|
|
271
279
|
<div class="flex h-12 shrink-0 items-center justify-end gap-2 border-t px-4">
|
|
@@ -276,6 +284,59 @@
|
|
|
276
284
|
Import {transformedRows.length} records
|
|
277
285
|
</Button>
|
|
278
286
|
</div>
|
|
287
|
+
|
|
288
|
+
{:else if step === "results"}
|
|
289
|
+
{@const failed = importResults.filter((r) => r.error !== null)}
|
|
290
|
+
{@const succeeded = importResults.filter((r) => r.error === null)}
|
|
291
|
+
{@const failedData = failed.map((r) => ({ __error: r.error, ...r.row }))}
|
|
292
|
+
{#if failed.length === 0}
|
|
293
|
+
<div class="flex flex-1 flex-col items-center justify-center gap-6 px-8 py-12">
|
|
294
|
+
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-muted">
|
|
295
|
+
<Check size="22" class="text-foreground" />
|
|
296
|
+
</div>
|
|
297
|
+
<div class="text-center">
|
|
298
|
+
<p class="text-sm font-medium text-foreground">Import complete</p>
|
|
299
|
+
<p class="mt-1 text-xs text-muted-foreground">{succeeded.length} {succeeded.length === 1 ? "record" : "records"} imported successfully</p>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
{:else}
|
|
303
|
+
<!-- Summary strip -->
|
|
304
|
+
<div class="shrink-0 border-b px-4 py-3 flex items-center gap-4">
|
|
305
|
+
<div class="flex items-center gap-1.5">
|
|
306
|
+
<span class="inline-flex h-5 w-5 items-center justify-center rounded-full bg-muted">
|
|
307
|
+
<Check size="11" class="text-foreground" />
|
|
308
|
+
</span>
|
|
309
|
+
<span class="text-xs text-muted-foreground">{succeeded.length} imported</span>
|
|
310
|
+
</div>
|
|
311
|
+
<div class="flex items-center gap-1.5">
|
|
312
|
+
<span class="inline-flex h-5 w-5 items-center justify-center rounded-full bg-destructive/10">
|
|
313
|
+
<X size="11" class="text-destructive" />
|
|
314
|
+
</span>
|
|
315
|
+
<span class="text-xs text-muted-foreground">{failed.length} failed</span>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
<div class="relative flex-1 overflow-auto w-full">
|
|
319
|
+
<Table
|
|
320
|
+
data={failedData}
|
|
321
|
+
columns={[{ id: "__error", icon: AlertCircle }, ...collectionColumns.filter((c) => c.id !== "id")]}
|
|
322
|
+
showCheckboxes={false}
|
|
323
|
+
unifiedBgColor="bg-background"
|
|
324
|
+
>
|
|
325
|
+
{#snippet overrideCell(value, column)}
|
|
326
|
+
{#if column.id === "__error"}
|
|
327
|
+
<span class="text-destructive">{value}</span>
|
|
328
|
+
{:else}
|
|
329
|
+
{value}
|
|
330
|
+
{/if}
|
|
331
|
+
{/snippet}
|
|
332
|
+
</Table>
|
|
333
|
+
</div>
|
|
334
|
+
{/if}
|
|
335
|
+
<div class="flex h-12 shrink-0 items-center justify-end gap-2 border-t px-4">
|
|
336
|
+
<Button onclick={hideDrawer} class="h-7 px-3 text-xs font-normal" Icon={Check}>
|
|
337
|
+
Done
|
|
338
|
+
</Button>
|
|
339
|
+
</div>
|
|
279
340
|
{/if}
|
|
280
341
|
</Dialog.Content>
|
|
281
342
|
</Dialog.Root>
|