@lobb-js/studio 0.17.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.
@@ -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 ((_a = field.pre_processors) === null || _a === void 0 ? void 0 : _a.default) {
24
- var defualtValue = field.pre_processors.default;
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
- const raw = csvCol ? row[csvCol] ?? null : null;
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
- const mapped = applyColumnMapping(rows);
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
- const res = await lobb.createMany(collectionName, transformedRows);
130
- if (res.status >= 400) {
131
- toast.error("Import failed");
132
- return;
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
- step = "input";
147
- activeTab = "upload";
148
- pasteContent = "";
149
- transformedRows = [];
150
- parseError = "";
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-hidden {step === 'preview' ? 'max-w-5xl h-[80vh]' : 'max-w-lg'}"
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,18 @@
244
262
  {/if}
245
263
  </div>
246
264
 
247
- {:else}
265
+ {:else if step === "preview"}
248
266
  <!-- Preview -->
249
267
  <div class="shrink-0 border-b 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
- {#snippet overrideCell(value, column, entry)}
262
- <FieldCell
263
- {collectionName}
264
- fieldName={column.id}
265
- {value}
266
- {entry}
267
- />
268
- {/snippet}
269
- </Table>
276
+ />
270
277
  </div>
271
278
 
272
279
  <div class="flex h-12 shrink-0 items-center justify-end gap-2 border-t px-4">
@@ -277,6 +284,59 @@
277
284
  Import {transformedRows.length} records
278
285
  </Button>
279
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>
280
340
  {/if}
281
341
  </Dialog.Content>
282
342
  </Dialog.Root>
@@ -58,7 +58,7 @@
58
58
  } else {
59
59
  const workflowHandlerDefaultValue =
60
60
  ctx.meta.collections.core_workflows.fields.handler
61
- .pre_processors.default;
61
+ .default;
62
62
  workflowEntry = {
63
63
  name: "",
64
64
  event_name: "",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lobb-js/studio",
3
3
  "license": "UNLICENSED",
4
- "version": "0.17.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.22.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.pre_processors?.default) {
15
- const defualtValue = field.pre_processors.default;
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
- const raw = csvCol ? row[csvCol] ?? null : null;
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
- const mapped = applyColumnMapping(rows);
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
- const res = await lobb.createMany(collectionName, transformedRows);
130
- if (res.status >= 400) {
131
- toast.error("Import failed");
132
- return;
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
- step = "input";
147
- activeTab = "upload";
148
- pasteContent = "";
149
- transformedRows = [];
150
- parseError = "";
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-hidden {step === 'preview' ? 'max-w-5xl h-[80vh]' : 'max-w-lg'}"
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,18 @@
244
262
  {/if}
245
263
  </div>
246
264
 
247
- {:else}
265
+ {:else if step === "preview"}
248
266
  <!-- Preview -->
249
267
  <div class="shrink-0 border-b 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
- {#snippet overrideCell(value, column, entry)}
262
- <FieldCell
263
- {collectionName}
264
- fieldName={column.id}
265
- {value}
266
- {entry}
267
- />
268
- {/snippet}
269
- </Table>
276
+ />
270
277
  </div>
271
278
 
272
279
  <div class="flex h-12 shrink-0 items-center justify-end gap-2 border-t px-4">
@@ -277,6 +284,59 @@
277
284
  Import {transformedRows.length} records
278
285
  </Button>
279
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>
280
340
  {/if}
281
341
  </Dialog.Content>
282
342
  </Dialog.Root>
@@ -58,7 +58,7 @@
58
58
  } else {
59
59
  const workflowHandlerDefaultValue =
60
60
  ctx.meta.collections.core_workflows.fields.handler
61
- .pre_processors.default;
61
+ .default;
62
62
  workflowEntry = {
63
63
  name: "",
64
64
  event_name: "",