@lobb-js/studio 0.45.0 → 0.46.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.
@@ -28,7 +28,12 @@
28
28
  let isDragging = $state(false);
29
29
  let parseError = $state("");
30
30
  let transformedRows = $state<any[]>([]);
31
- let importResults = $state<{ row: any; error: string | null }[]>([]);
31
+ // `action` is only set when a `studio.collections.import` workflow takes
32
+ // ownership of the writes (handled: true) and tells us per row whether
33
+ // the record was created or updated. The default code path leaves it
34
+ // undefined; the UI then just says "imported".
35
+ type ImportAction = "created" | "updated";
36
+ let importResults = $state<{ row: any; error: string | null; action?: ImportAction }[]>([]);
32
37
  // Which results tab is visible. Defaults to "failed" when there are
33
38
  // any failures (most users want to fix those first); falls back to
34
39
  // "imported" when everything went through.
@@ -168,23 +173,45 @@
168
173
  collectionName,
169
174
  rows: transformedRows.map((r) => ({ ...r })),
170
175
  });
171
- const finalRows = eventResult.rows ?? transformedRows;
172
176
 
173
177
  importResults = [];
174
178
  let hasSuccess = false;
175
- for (const row of finalRows) {
176
- const response = await lobb.createOne(collectionName, { data: row });
177
- if (response.ok) {
178
- importResults.push({ row, error: null });
179
+
180
+ if (eventResult?.handled && eventResult.results) {
181
+ // The workflow took ownership — it did the writes itself
182
+ // (typical use case: upsert-style imports where the workflow
183
+ // needs to decide create-vs-update per row). Render its
184
+ // results verbatim, no extra writes from our side.
185
+ const r = eventResult.results as {
186
+ imported?: Array<{ row: any; action: ImportAction }>;
187
+ failed?: Array<{ row: any; error: string; action?: ImportAction }>;
188
+ };
189
+ for (const item of r.imported ?? []) {
190
+ importResults.push({ row: item.row, error: null, action: item.action });
179
191
  hasSuccess = true;
180
- } else {
181
- const body = await response.json().catch(() => null);
182
- const message = body?.details
183
- ? Object.entries(body.details).map(([f, msgs]) => `${f}: ${(msgs as string[]).join(", ")}`).join(" | ")
184
- : (body?.message ?? `HTTP ${response.status}`);
185
- importResults.push({ row, error: message });
192
+ }
193
+ for (const item of r.failed ?? []) {
194
+ importResults.push({ row: item.row, error: item.error, action: item.action });
195
+ }
196
+ } else {
197
+ // Default path workflow only transformed rows (or did
198
+ // nothing). Loop createOne ourselves and track results.
199
+ const finalRows = eventResult.rows ?? transformedRows;
200
+ for (const row of finalRows) {
201
+ const response = await lobb.createOne(collectionName, { data: row });
202
+ if (response.ok) {
203
+ importResults.push({ row, error: null });
204
+ hasSuccess = true;
205
+ } else {
206
+ const body = await response.json().catch(() => null);
207
+ const message = body?.details
208
+ ? Object.entries(body.details).map(([f, msgs]) => `${f}: ${(msgs as string[]).join(", ")}`).join(" | ")
209
+ : (body?.message ?? `HTTP ${response.status}`);
210
+ importResults.push({ row, error: message });
211
+ }
186
212
  }
187
213
  }
214
+
188
215
  if (hasSuccess && onSuccessfullSave) await onSuccessfullSave();
189
216
 
190
217
  // Default to the failed tab when there are any failures; if
@@ -379,6 +406,8 @@
379
406
  {:else if step === "results"}
380
407
  {@const failed = importResults.filter((r) => r.error !== null)}
381
408
  {@const succeeded = importResults.filter((r) => r.error === null)}
409
+ {@const createdCount = succeeded.filter((r) => r.action === "created").length}
410
+ {@const updatedCount = succeeded.filter((r) => r.action === "updated").length}
382
411
  {@const failedData = failed.map((r) => ({ __error: r.error, ...r.row }))}
383
412
  {#if failed.length === 0}
384
413
  <!-- All rows imported successfully: nothing to audit here —
@@ -392,7 +421,11 @@
392
421
  <div class="text-center">
393
422
  <p class="text-sm font-medium text-foreground">Import complete</p>
394
423
  <p class="mt-1 text-xs text-muted-foreground">
395
- {succeeded.length} {succeeded.length === 1 ? "record" : "records"} imported successfully
424
+ {#if createdCount + updatedCount > 0}
425
+ {createdCount} created · {updatedCount} updated
426
+ {:else}
427
+ {succeeded.length} {succeeded.length === 1 ? "record" : "records"} imported successfully
428
+ {/if}
396
429
  </p>
397
430
  </div>
398
431
  </div>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lobb-js/studio",
3
3
  "license": "UNLICENSED",
4
- "version": "0.45.0",
4
+ "version": "0.46.0",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -45,7 +45,7 @@
45
45
  "postpublish": "./scripts/postpublish.sh"
46
46
  },
47
47
  "devDependencies": {
48
- "@lobb-js/core": "^0.39.0",
48
+ "@lobb-js/core": "^0.40.0",
49
49
  "@chromatic-com/storybook": "^4.1.2",
50
50
  "@playwright/test": "^1.60.0",
51
51
  "@storybook/addon-a11y": "^10.0.1",
@@ -91,7 +91,7 @@
91
91
  "@codemirror/view": "^6.39.12",
92
92
  "@dagrejs/dagre": "^1.1.5",
93
93
  "@internationalized/date": "^3.12.0",
94
- "@lobb-js/sdk": "^0.3.0",
94
+ "@lobb-js/sdk": "^0.4.0",
95
95
  "@lucide/svelte": "^0.563.1",
96
96
  "@tailwindcss/vite": "^4.3.0",
97
97
  "@tiptap/core": "^3.0.0",
@@ -28,7 +28,12 @@
28
28
  let isDragging = $state(false);
29
29
  let parseError = $state("");
30
30
  let transformedRows = $state<any[]>([]);
31
- let importResults = $state<{ row: any; error: string | null }[]>([]);
31
+ // `action` is only set when a `studio.collections.import` workflow takes
32
+ // ownership of the writes (handled: true) and tells us per row whether
33
+ // the record was created or updated. The default code path leaves it
34
+ // undefined; the UI then just says "imported".
35
+ type ImportAction = "created" | "updated";
36
+ let importResults = $state<{ row: any; error: string | null; action?: ImportAction }[]>([]);
32
37
  // Which results tab is visible. Defaults to "failed" when there are
33
38
  // any failures (most users want to fix those first); falls back to
34
39
  // "imported" when everything went through.
@@ -168,23 +173,45 @@
168
173
  collectionName,
169
174
  rows: transformedRows.map((r) => ({ ...r })),
170
175
  });
171
- const finalRows = eventResult.rows ?? transformedRows;
172
176
 
173
177
  importResults = [];
174
178
  let hasSuccess = false;
175
- for (const row of finalRows) {
176
- const response = await lobb.createOne(collectionName, { data: row });
177
- if (response.ok) {
178
- importResults.push({ row, error: null });
179
+
180
+ if (eventResult?.handled && eventResult.results) {
181
+ // The workflow took ownership — it did the writes itself
182
+ // (typical use case: upsert-style imports where the workflow
183
+ // needs to decide create-vs-update per row). Render its
184
+ // results verbatim, no extra writes from our side.
185
+ const r = eventResult.results as {
186
+ imported?: Array<{ row: any; action: ImportAction }>;
187
+ failed?: Array<{ row: any; error: string; action?: ImportAction }>;
188
+ };
189
+ for (const item of r.imported ?? []) {
190
+ importResults.push({ row: item.row, error: null, action: item.action });
179
191
  hasSuccess = true;
180
- } else {
181
- const body = await response.json().catch(() => null);
182
- const message = body?.details
183
- ? Object.entries(body.details).map(([f, msgs]) => `${f}: ${(msgs as string[]).join(", ")}`).join(" | ")
184
- : (body?.message ?? `HTTP ${response.status}`);
185
- importResults.push({ row, error: message });
192
+ }
193
+ for (const item of r.failed ?? []) {
194
+ importResults.push({ row: item.row, error: item.error, action: item.action });
195
+ }
196
+ } else {
197
+ // Default path workflow only transformed rows (or did
198
+ // nothing). Loop createOne ourselves and track results.
199
+ const finalRows = eventResult.rows ?? transformedRows;
200
+ for (const row of finalRows) {
201
+ const response = await lobb.createOne(collectionName, { data: row });
202
+ if (response.ok) {
203
+ importResults.push({ row, error: null });
204
+ hasSuccess = true;
205
+ } else {
206
+ const body = await response.json().catch(() => null);
207
+ const message = body?.details
208
+ ? Object.entries(body.details).map(([f, msgs]) => `${f}: ${(msgs as string[]).join(", ")}`).join(" | ")
209
+ : (body?.message ?? `HTTP ${response.status}`);
210
+ importResults.push({ row, error: message });
211
+ }
186
212
  }
187
213
  }
214
+
188
215
  if (hasSuccess && onSuccessfullSave) await onSuccessfullSave();
189
216
 
190
217
  // Default to the failed tab when there are any failures; if
@@ -379,6 +406,8 @@
379
406
  {:else if step === "results"}
380
407
  {@const failed = importResults.filter((r) => r.error !== null)}
381
408
  {@const succeeded = importResults.filter((r) => r.error === null)}
409
+ {@const createdCount = succeeded.filter((r) => r.action === "created").length}
410
+ {@const updatedCount = succeeded.filter((r) => r.action === "updated").length}
382
411
  {@const failedData = failed.map((r) => ({ __error: r.error, ...r.row }))}
383
412
  {#if failed.length === 0}
384
413
  <!-- All rows imported successfully: nothing to audit here —
@@ -392,7 +421,11 @@
392
421
  <div class="text-center">
393
422
  <p class="text-sm font-medium text-foreground">Import complete</p>
394
423
  <p class="mt-1 text-xs text-muted-foreground">
395
- {succeeded.length} {succeeded.length === 1 ? "record" : "records"} imported successfully
424
+ {#if createdCount + updatedCount > 0}
425
+ {createdCount} created · {updatedCount} updated
426
+ {:else}
427
+ {succeeded.length} {succeeded.length === 1 ? "record" : "records"} imported successfully
428
+ {/if}
396
429
  </p>
397
430
  </div>
398
431
  </div>