@lobb-js/studio 0.44.1 → 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.
- package/dist/components/dataTable/dataTable.svelte +6 -3
- package/dist/components/dataTable/header.svelte +13 -2
- package/dist/components/dataTable/header.svelte.d.ts +2 -1
- package/dist/components/importButton.svelte +46 -13
- package/package.json +3 -3
- package/src/lib/components/dataTable/dataTable.svelte +6 -3
- package/src/lib/components/dataTable/header.svelte +13 -2
- package/src/lib/components/importButton.svelte +46 -13
|
@@ -210,6 +210,7 @@
|
|
|
210
210
|
});
|
|
211
211
|
|
|
212
212
|
let selectedRecords = $state([]);
|
|
213
|
+
let searchTerm = $state('');
|
|
213
214
|
let totalCount = $state(0);
|
|
214
215
|
let serverData: TableProps["data"] = $state([]);
|
|
215
216
|
let loading = $state(true);
|
|
@@ -228,14 +229,15 @@
|
|
|
228
229
|
const showLeftTools = $derived(!isSelectMode);
|
|
229
230
|
let childrenDrawerEntry = $state<Record<string, any> | null>(null);
|
|
230
231
|
|
|
231
|
-
// requests the data from the server when
|
|
232
|
+
// requests the data from the server when params or searchTerm changes
|
|
232
233
|
$effect(() => {
|
|
233
|
-
loadData(params);
|
|
234
|
+
loadData(params, searchTerm);
|
|
234
235
|
});
|
|
235
236
|
|
|
236
|
-
async function loadData(params: any) {
|
|
237
|
+
async function loadData(params: any, search: string = '') {
|
|
237
238
|
loading = true;
|
|
238
239
|
const paramsCopy = $state.snapshot(params);
|
|
240
|
+
if (search.trim()) paramsCopy.search = search.trim();
|
|
239
241
|
const sort: TableProps["sort"] = paramsCopy.sort;
|
|
240
242
|
const sortStrings: string[] = [];
|
|
241
243
|
if (sort) {
|
|
@@ -417,6 +419,7 @@
|
|
|
417
419
|
onLink={isRecordingMode ? handleLink : undefined}
|
|
418
420
|
onCreate={isRecordingMode ? handleCreate : undefined}
|
|
419
421
|
{excludeIds}
|
|
422
|
+
bind:searchTerm
|
|
420
423
|
>
|
|
421
424
|
{#snippet left()}
|
|
422
425
|
{@render headerLeft?.()}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { Changes } from "../detailView/utils";
|
|
4
4
|
import type { ParentContext } from "./dataTable.svelte";
|
|
5
5
|
import CanAccess from "../canAccess.svelte";
|
|
6
|
-
import { Download, ListRestart, LoaderCircle, Plus, Trash, Link } from "lucide-svelte";
|
|
6
|
+
import { Download, ListRestart, LoaderCircle, Plus, Trash, Link, Search } from "lucide-svelte";
|
|
7
7
|
import FilterButton from "./filterButton.svelte";
|
|
8
8
|
import SortButton from "./sortButton.svelte";
|
|
9
9
|
import Button from "../ui/button/button.svelte";
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
loading?: boolean;
|
|
31
31
|
left?: Snippet<[]>;
|
|
32
32
|
excludeIds?: (string | number)[];
|
|
33
|
+
searchTerm?: string;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
let {
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
loading = false,
|
|
46
47
|
left,
|
|
47
48
|
excludeIds = [],
|
|
49
|
+
searchTerm = $bindable(''),
|
|
48
50
|
}: Props = $props();
|
|
49
51
|
|
|
50
52
|
function handleLink(record: any) {
|
|
@@ -139,9 +141,18 @@
|
|
|
139
141
|
bind:sort={params.sort}
|
|
140
142
|
showText={!headerIsSmall}
|
|
141
143
|
/>
|
|
144
|
+
<div class="relative flex items-center">
|
|
145
|
+
<Search size="13" class="absolute left-2 text-muted-foreground pointer-events-none" />
|
|
146
|
+
<input
|
|
147
|
+
type="text"
|
|
148
|
+
placeholder="Search..."
|
|
149
|
+
bind:value={searchTerm}
|
|
150
|
+
class="h-8 rounded-md border bg-muted pl-7 pr-2 text-xs outline-none focus:ring-1 focus:ring-ring w-40"
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
142
153
|
{/if}
|
|
143
154
|
</div>
|
|
144
|
-
<div>
|
|
155
|
+
<div class="flex items-center gap-1">
|
|
145
156
|
<Button
|
|
146
157
|
variant="ghost"
|
|
147
158
|
size="sm"
|
|
@@ -14,7 +14,8 @@ interface Props {
|
|
|
14
14
|
loading?: boolean;
|
|
15
15
|
left?: Snippet<[]>;
|
|
16
16
|
excludeIds?: (string | number)[];
|
|
17
|
+
searchTerm?: string;
|
|
17
18
|
}
|
|
18
|
-
declare const Header: import("svelte").Component<Props, {}, "selectedRecords" | "params">;
|
|
19
|
+
declare const Header: import("svelte").Component<Props, {}, "selectedRecords" | "params" | "searchTerm">;
|
|
19
20
|
type Header = ReturnType<typeof Header>;
|
|
20
21
|
export default Header;
|
|
@@ -28,7 +28,12 @@
|
|
|
28
28
|
let isDragging = $state(false);
|
|
29
29
|
let parseError = $state("");
|
|
30
30
|
let transformedRows = $state<any[]>([]);
|
|
31
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
{
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
|
@@ -210,6 +210,7 @@
|
|
|
210
210
|
});
|
|
211
211
|
|
|
212
212
|
let selectedRecords = $state([]);
|
|
213
|
+
let searchTerm = $state('');
|
|
213
214
|
let totalCount = $state(0);
|
|
214
215
|
let serverData: TableProps["data"] = $state([]);
|
|
215
216
|
let loading = $state(true);
|
|
@@ -228,14 +229,15 @@
|
|
|
228
229
|
const showLeftTools = $derived(!isSelectMode);
|
|
229
230
|
let childrenDrawerEntry = $state<Record<string, any> | null>(null);
|
|
230
231
|
|
|
231
|
-
// requests the data from the server when
|
|
232
|
+
// requests the data from the server when params or searchTerm changes
|
|
232
233
|
$effect(() => {
|
|
233
|
-
loadData(params);
|
|
234
|
+
loadData(params, searchTerm);
|
|
234
235
|
});
|
|
235
236
|
|
|
236
|
-
async function loadData(params: any) {
|
|
237
|
+
async function loadData(params: any, search: string = '') {
|
|
237
238
|
loading = true;
|
|
238
239
|
const paramsCopy = $state.snapshot(params);
|
|
240
|
+
if (search.trim()) paramsCopy.search = search.trim();
|
|
239
241
|
const sort: TableProps["sort"] = paramsCopy.sort;
|
|
240
242
|
const sortStrings: string[] = [];
|
|
241
243
|
if (sort) {
|
|
@@ -417,6 +419,7 @@
|
|
|
417
419
|
onLink={isRecordingMode ? handleLink : undefined}
|
|
418
420
|
onCreate={isRecordingMode ? handleCreate : undefined}
|
|
419
421
|
{excludeIds}
|
|
422
|
+
bind:searchTerm
|
|
420
423
|
>
|
|
421
424
|
{#snippet left()}
|
|
422
425
|
{@render headerLeft?.()}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { Changes } from "../detailView/utils";
|
|
4
4
|
import type { ParentContext } from "./dataTable.svelte";
|
|
5
5
|
import CanAccess from "../canAccess.svelte";
|
|
6
|
-
import { Download, ListRestart, LoaderCircle, Plus, Trash, Link } from "lucide-svelte";
|
|
6
|
+
import { Download, ListRestart, LoaderCircle, Plus, Trash, Link, Search } from "lucide-svelte";
|
|
7
7
|
import FilterButton from "./filterButton.svelte";
|
|
8
8
|
import SortButton from "./sortButton.svelte";
|
|
9
9
|
import Button from "../ui/button/button.svelte";
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
loading?: boolean;
|
|
31
31
|
left?: Snippet<[]>;
|
|
32
32
|
excludeIds?: (string | number)[];
|
|
33
|
+
searchTerm?: string;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
let {
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
loading = false,
|
|
46
47
|
left,
|
|
47
48
|
excludeIds = [],
|
|
49
|
+
searchTerm = $bindable(''),
|
|
48
50
|
}: Props = $props();
|
|
49
51
|
|
|
50
52
|
function handleLink(record: any) {
|
|
@@ -139,9 +141,18 @@
|
|
|
139
141
|
bind:sort={params.sort}
|
|
140
142
|
showText={!headerIsSmall}
|
|
141
143
|
/>
|
|
144
|
+
<div class="relative flex items-center">
|
|
145
|
+
<Search size="13" class="absolute left-2 text-muted-foreground pointer-events-none" />
|
|
146
|
+
<input
|
|
147
|
+
type="text"
|
|
148
|
+
placeholder="Search..."
|
|
149
|
+
bind:value={searchTerm}
|
|
150
|
+
class="h-8 rounded-md border bg-muted pl-7 pr-2 text-xs outline-none focus:ring-1 focus:ring-ring w-40"
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
142
153
|
{/if}
|
|
143
154
|
</div>
|
|
144
|
-
<div>
|
|
155
|
+
<div class="flex items-center gap-1">
|
|
145
156
|
<Button
|
|
146
157
|
variant="ghost"
|
|
147
158
|
size="sm"
|
|
@@ -28,7 +28,12 @@
|
|
|
28
28
|
let isDragging = $state(false);
|
|
29
29
|
let parseError = $state("");
|
|
30
30
|
let transformedRows = $state<any[]>([]);
|
|
31
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
{
|
|
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>
|