@lobb-js/studio 0.31.0 → 0.33.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/actions.d.ts +4 -0
- package/dist/applyStudioTheme.d.ts +2 -0
- package/dist/applyStudioTheme.js +36 -0
- package/dist/components/Studio.svelte +2 -0
- package/dist/components/canAccess.svelte +52 -0
- package/dist/components/canAccess.svelte.d.ts +10 -0
- package/dist/components/dataTable/dataTable.svelte +124 -58
- package/dist/components/dataTable/dataTable.svelte.d.ts +8 -1
- package/dist/components/dataTable/fieldCell.svelte +4 -4
- package/dist/components/dataTable/fieldCell.svelte.d.ts +2 -2
- package/dist/components/dataTable/header.svelte +33 -33
- package/dist/components/dataTable/header.svelte.d.ts +3 -3
- package/dist/components/dataTable/polymorphicFieldCell.svelte +3 -3
- package/dist/components/dataTable/polymorphicFieldCell.svelte.d.ts +2 -2
- package/dist/components/dataTablePopup/dataTablePopup.svelte +3 -0
- package/dist/components/dataTablePopup/dataTablePopup.svelte.d.ts +4 -0
- package/dist/components/detailView/create/createDetailView.svelte +28 -54
- package/dist/components/detailView/create/createDetailView.svelte.d.ts +4 -3
- package/dist/components/detailView/create/createDetailViewChildren.svelte +113 -0
- package/dist/components/detailView/create/createDetailViewChildren.svelte.d.ts +9 -0
- package/dist/components/detailView/create/createManyView.svelte +2 -2
- package/dist/components/detailView/update/updateDetailView.svelte +46 -40
- package/dist/components/detailView/update/updateDetailView.svelte.d.ts +5 -3
- package/dist/components/detailView/update/updateDetailViewButton.svelte +0 -1
- package/dist/components/detailView/update/updateDetailViewChildren.svelte +122 -0
- package/dist/components/detailView/update/updateDetailViewChildren.svelte.d.ts +10 -0
- package/dist/components/detailView/utils.d.ts +1 -2
- package/dist/components/importButton.svelte +1 -1
- package/dist/components/richTextEditor.svelte +2 -0
- package/dist/components/workflowEditor.svelte +6 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/store.types.d.ts +7 -0
- package/package.json +3 -3
- package/src/lib/actions.ts +1 -0
- package/src/lib/applyStudioTheme.ts +38 -0
- package/src/lib/components/Studio.svelte +2 -0
- package/src/lib/components/canAccess.svelte +52 -0
- package/src/lib/components/dataTable/dataTable.svelte +124 -58
- package/src/lib/components/dataTable/fieldCell.svelte +4 -4
- package/src/lib/components/dataTable/header.svelte +33 -33
- package/src/lib/components/dataTable/polymorphicFieldCell.svelte +3 -3
- package/src/lib/components/dataTablePopup/dataTablePopup.svelte +3 -0
- package/src/lib/components/detailView/create/createDetailView.svelte +28 -54
- package/src/lib/components/detailView/create/createDetailViewChildren.svelte +113 -0
- package/src/lib/components/detailView/create/createManyView.svelte +2 -2
- package/src/lib/components/detailView/update/updateDetailView.svelte +46 -40
- package/src/lib/components/detailView/update/updateDetailViewButton.svelte +0 -1
- package/src/lib/components/detailView/update/updateDetailViewChildren.svelte +122 -0
- package/src/lib/components/detailView/utils.ts +1 -1
- package/src/lib/components/importButton.svelte +1 -1
- package/src/lib/components/richTextEditor.svelte +2 -0
- package/src/lib/components/workflowEditor.svelte +6 -4
- package/src/lib/index.ts +2 -0
- package/src/lib/store.types.ts +6 -0
- package/dist/components/detailView/update/detailViewChildren.svelte +0 -61
- package/dist/components/detailView/update/detailViewChildren.svelte.d.ts +0 -9
- package/src/lib/components/detailView/update/detailViewChildren.svelte +0 -61
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import Header from "./header.svelte";
|
|
14
14
|
import Table, { type TableProps } from "./table.svelte";
|
|
15
15
|
import { getCollectionColumns, getCollectionParamsFields } from "./utils";
|
|
16
|
+
import CanAccess from "../canAccess.svelte";
|
|
16
17
|
import { Pencil, Trash, Unlink } from "lucide-svelte";
|
|
17
18
|
import ListViewChildren from "./listViewChildren.svelte";
|
|
18
19
|
import FieldCell from "./fieldCell.svelte";
|
|
@@ -21,11 +22,10 @@
|
|
|
21
22
|
import { showDialog } from "../../actions";
|
|
22
23
|
import UpdateDetailViewButton from "../detailView/update/updateDetailViewButton.svelte";
|
|
23
24
|
import type { Snippet } from "svelte";
|
|
24
|
-
import type {
|
|
25
|
+
import type { ChildrenChanges } from "../detailView/utils";
|
|
25
26
|
import ExtensionsComponents from "../extensionsComponents.svelte";
|
|
26
27
|
import { getExtensionUtils, loadExtensionComponents } from "../../extensions/extensionUtils";
|
|
27
|
-
import {
|
|
28
|
-
import { onMount } from "svelte";
|
|
28
|
+
import { untrack } from "svelte";
|
|
29
29
|
import Tabs from "./dataTableTabs.svelte";
|
|
30
30
|
import { fade } from "svelte/transition";
|
|
31
31
|
import type { CollectionTab } from "../../store.types";
|
|
@@ -37,14 +37,18 @@
|
|
|
37
37
|
filter?: any;
|
|
38
38
|
searchParams?: Record<string, any>;
|
|
39
39
|
parentContext?: ParentContext;
|
|
40
|
+
onChanges?: (changes: ChildrenChanges) => void;
|
|
40
41
|
changes?: ChildrenChanges;
|
|
41
42
|
showHeader?: boolean;
|
|
42
43
|
showFooter?: boolean;
|
|
43
44
|
showImport?: boolean;
|
|
44
45
|
showDelete?: boolean;
|
|
46
|
+
showEdit?: boolean;
|
|
47
|
+
onDataLoad?: (total: number) => void;
|
|
45
48
|
tableProps?: Partial<TableProps>;
|
|
46
49
|
tabs?: CollectionTab[];
|
|
47
50
|
headerLeft?: Snippet<[]>;
|
|
51
|
+
view?: { id: string; [key: string]: any };
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
let {
|
|
@@ -52,64 +56,49 @@
|
|
|
52
56
|
filter,
|
|
53
57
|
searchParams,
|
|
54
58
|
parentContext,
|
|
55
|
-
|
|
59
|
+
onChanges,
|
|
60
|
+
changes,
|
|
56
61
|
showHeader = true,
|
|
57
62
|
showFooter = true,
|
|
58
63
|
showImport = true,
|
|
59
64
|
showDelete = false,
|
|
65
|
+
showEdit = true,
|
|
66
|
+
onDataLoad,
|
|
60
67
|
tableProps,
|
|
61
68
|
tabs,
|
|
62
69
|
headerLeft,
|
|
70
|
+
view,
|
|
63
71
|
}: Props = $props();
|
|
64
72
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
let showCreate = $state(false);
|
|
70
|
-
onMount(async () => {
|
|
71
|
-
const [update, create] = await Promise.all([
|
|
72
|
-
emitEvent({ lobb, ctx }, "auth.canAccess", { collection: collectionName, action: "update" }),
|
|
73
|
-
emitEvent({ lobb, ctx }, "auth.canAccess", { collection: collectionName, action: "create" }),
|
|
74
|
-
]);
|
|
75
|
-
showUpdate = update === true;
|
|
76
|
-
showCreate = create === true;
|
|
77
|
-
});
|
|
73
|
+
const isRecordingMode = onChanges !== undefined;
|
|
74
|
+
let localChanges = $state<ChildrenChanges>(
|
|
75
|
+
untrack(() => changes) ?? { created: [], updated: [], deleted: [], linked: [], unlinked: [] }
|
|
76
|
+
);
|
|
78
77
|
|
|
79
|
-
function getOrCreateUpdatedSlot(recordId: string): Changes | undefined {
|
|
80
|
-
if (!changes) return undefined;
|
|
81
|
-
let slot = changes.updated.find((u) => String(u.id) === String(recordId));
|
|
82
|
-
if (!slot) {
|
|
83
|
-
slot = { id: recordId, data: {}, children: {} };
|
|
84
|
-
changes.updated.push(slot);
|
|
85
|
-
}
|
|
86
|
-
return slot;
|
|
87
|
-
}
|
|
88
78
|
|
|
89
|
-
// Derives the displayed rows by applying
|
|
90
|
-
// This is the single place responsible for optimistic UI — no handler touches data directly.
|
|
79
|
+
// Derives the displayed rows by applying localChanges on top of server data.
|
|
91
80
|
const data = $derived.by(() => {
|
|
92
|
-
if (!
|
|
81
|
+
if (!isRecordingMode) return serverData;
|
|
93
82
|
|
|
94
83
|
const removedIds = new Set([
|
|
95
|
-
...
|
|
96
|
-
...
|
|
84
|
+
...localChanges.deleted.map((r: any) => String(r.id)),
|
|
85
|
+
...localChanges.unlinked.map((r: any) => String(r.id)),
|
|
97
86
|
]);
|
|
98
87
|
|
|
99
88
|
let result = serverData.filter((r: any) => !removedIds.has(String(r.id)));
|
|
100
89
|
|
|
101
90
|
result = result.map((r: any) => {
|
|
102
|
-
const update =
|
|
103
|
-
return update && Object.keys(update.data).length ? { ...r, ...update.data } : r;
|
|
91
|
+
const update = localChanges.updated.find((u) => String(u.id) === String(r.id));
|
|
92
|
+
return update && Object.keys(update.changes.data).length ? { ...r, ...update.changes.data } : r;
|
|
104
93
|
});
|
|
105
94
|
|
|
106
|
-
for (const record of
|
|
95
|
+
for (const record of localChanges.linked) {
|
|
107
96
|
if (!result.some((r: any) => String(r.id) === String(record.id))) {
|
|
108
97
|
result = [...result, record];
|
|
109
98
|
}
|
|
110
99
|
}
|
|
111
100
|
|
|
112
|
-
for (const item of
|
|
101
|
+
for (const item of localChanges.created) {
|
|
113
102
|
result = [...result, { ...item.data, _pending: true }];
|
|
114
103
|
}
|
|
115
104
|
|
|
@@ -122,6 +111,23 @@
|
|
|
122
111
|
|
|
123
112
|
let activeTabFilter = $state<any>(undefined);
|
|
124
113
|
|
|
114
|
+
// Named-view lookup: when a `view` prop is supplied, resolve the
|
|
115
|
+
// matching `dataTable.view.<view.id>` registration. Exact key match,
|
|
116
|
+
// no `when` predicate — the caller already picked the view they want.
|
|
117
|
+
const customViewComponent = $derived.by(() => {
|
|
118
|
+
if (!view?.id) return null;
|
|
119
|
+
const key = `dataTable.view.${view.id}`;
|
|
120
|
+
for (const ext of Object.values(ctx.extensions ?? {})) {
|
|
121
|
+
const components = (ext as any)?.components ?? {};
|
|
122
|
+
const entry = components[key];
|
|
123
|
+
if (!entry) continue;
|
|
124
|
+
return entry && typeof entry === "object" && "component" in entry
|
|
125
|
+
? entry.component
|
|
126
|
+
: entry;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
});
|
|
130
|
+
|
|
125
131
|
// Canonicalize the incoming filter so values like `{ status: "Open" }`
|
|
126
132
|
// become `{ status: { $eq: "Open" } }`. The Filter UI and the server
|
|
127
133
|
// both expect operator objects, so doing this once at the boundary
|
|
@@ -189,18 +195,26 @@
|
|
|
189
195
|
const res = await response.json();
|
|
190
196
|
serverData = res.data;
|
|
191
197
|
totalCount = res.meta.totalCount;
|
|
198
|
+
onDataLoad?.(totalCount);
|
|
192
199
|
loading = false;
|
|
193
200
|
}
|
|
194
201
|
|
|
195
202
|
async function handleDelete(entryId: string) {
|
|
196
203
|
const result = await showDialog("Are you sure?", "This will permanently delete the record.");
|
|
197
204
|
if (!result) return;
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
205
|
+
if (isRecordingMode) {
|
|
206
|
+
// If the record was locally linked (not yet in DB), just cancel the link instead of marking for deletion.
|
|
207
|
+
const linkedIdx = localChanges.linked.findIndex((r: any) => String(r.id) === String(entryId));
|
|
208
|
+
if (linkedIdx !== -1) {
|
|
209
|
+
localChanges.linked.splice(linkedIdx, 1);
|
|
210
|
+
} else {
|
|
211
|
+
const record = serverData.find((r: any) => String(r.id) === String(entryId));
|
|
212
|
+
if (record) localChanges.deleted.push($state.snapshot(record));
|
|
213
|
+
}
|
|
214
|
+
onChanges?.($state.snapshot(localChanges));
|
|
201
215
|
} else if (parentContext) {
|
|
202
216
|
serverData = serverData.filter((r: any) => String(r.id) !== String(entryId));
|
|
203
|
-
await lobb.updateOne(parentContext.collectionName, String(parentContext.recordId), {}, { [collectionName]: { delete: [entryId] } });
|
|
217
|
+
await lobb.updateOne(parentContext.collectionName, String(parentContext.recordId), { data: {}, children: { [collectionName]: { delete: [entryId] } } });
|
|
204
218
|
params = { ...params };
|
|
205
219
|
} else {
|
|
206
220
|
serverData = serverData.filter((r: any) => String(r.id) !== String(entryId));
|
|
@@ -212,16 +226,55 @@
|
|
|
212
226
|
async function handleUnlink(entryId: string) {
|
|
213
227
|
const result = await showDialog("Are you sure?", "This will unlink the record without deleting it.");
|
|
214
228
|
if (!result) return;
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
229
|
+
if (isRecordingMode) {
|
|
230
|
+
// If the record was locally linked this session, just cancel the link — net effect is no change.
|
|
231
|
+
const linkedIdx = localChanges.linked.findIndex((r: any) => String(r.id) === String(entryId));
|
|
232
|
+
if (linkedIdx !== -1) {
|
|
233
|
+
localChanges.linked.splice(linkedIdx, 1);
|
|
234
|
+
} else {
|
|
235
|
+
const record = serverData.find((r: any) => String(r.id) === String(entryId));
|
|
236
|
+
if (record) localChanges.unlinked.push($state.snapshot(record));
|
|
237
|
+
}
|
|
238
|
+
onChanges?.($state.snapshot(localChanges));
|
|
218
239
|
} else {
|
|
219
240
|
serverData = serverData.filter((r: any) => String(r.id) !== String(entryId));
|
|
220
|
-
await lobb.updateOne(parentContext!.collectionName, String(parentContext!.recordId), {}, { [collectionName]: { unlink: [entryId] } });
|
|
241
|
+
await lobb.updateOne(parentContext!.collectionName, String(parentContext!.recordId), { data: {}, children: { [collectionName]: { unlink: [entryId] } } });
|
|
221
242
|
params = { ...params };
|
|
222
243
|
}
|
|
223
244
|
}
|
|
224
245
|
|
|
246
|
+
function handleLink(record: any) {
|
|
247
|
+
// If the record was locally unlinked this session, just cancel the unlink — net effect is no change.
|
|
248
|
+
const unlinkedIdx = localChanges.unlinked.findIndex((r: any) => String(r.id) === String(record.id));
|
|
249
|
+
if (unlinkedIdx !== -1) {
|
|
250
|
+
localChanges.unlinked.splice(unlinkedIdx, 1);
|
|
251
|
+
} else if (!localChanges.linked.some((r: any) => String(r.id) === String(record.id))) {
|
|
252
|
+
localChanges.linked.push(record);
|
|
253
|
+
}
|
|
254
|
+
onChanges?.($state.snapshot(localChanges));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
$effect(() => {
|
|
258
|
+
if (isRecordingMode) {
|
|
259
|
+
console.log(`[DataTable:${collectionName}] localChanges:`, $state.snapshot(localChanges));
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
function handleCreate(changes: import("../detailView/utils").Changes) {
|
|
264
|
+
localChanges.created.push({ data: changes.data });
|
|
265
|
+
onChanges?.($state.snapshot(localChanges));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function handleUpdate(id: string, editChanges: import("../detailView/utils").Changes) {
|
|
269
|
+
const existing = localChanges.updated.find((u) => String(u.id) === id);
|
|
270
|
+
if (existing) {
|
|
271
|
+
existing.changes = editChanges;
|
|
272
|
+
} else {
|
|
273
|
+
localChanges.updated.push({ id, changes: editChanges });
|
|
274
|
+
}
|
|
275
|
+
onChanges?.($state.snapshot(localChanges));
|
|
276
|
+
}
|
|
277
|
+
|
|
225
278
|
</script>
|
|
226
279
|
|
|
227
280
|
<div
|
|
@@ -244,9 +297,9 @@
|
|
|
244
297
|
{collectionName}
|
|
245
298
|
bind:selectedRecords
|
|
246
299
|
{showImport}
|
|
247
|
-
{showCreate}
|
|
248
300
|
{parentContext}
|
|
249
|
-
{
|
|
301
|
+
onLink={isRecordingMode ? handleLink : undefined}
|
|
302
|
+
onCreate={isRecordingMode ? handleCreate : undefined}
|
|
250
303
|
>
|
|
251
304
|
{#snippet left()}
|
|
252
305
|
{@render headerLeft?.()}
|
|
@@ -263,6 +316,18 @@
|
|
|
263
316
|
<Skeleton class="h-8 w-[80%]" />
|
|
264
317
|
<Skeleton class="h-8 w-[60%]" />
|
|
265
318
|
</div>
|
|
319
|
+
{:else if customViewComponent}
|
|
320
|
+
{@const CustomView = customViewComponent}
|
|
321
|
+
<CustomView
|
|
322
|
+
{collectionName}
|
|
323
|
+
{data}
|
|
324
|
+
{columns}
|
|
325
|
+
bind:params
|
|
326
|
+
{loading}
|
|
327
|
+
refresh={() => { params = { ...params }; }}
|
|
328
|
+
{view}
|
|
329
|
+
utils={getExtensionUtils(lobb, ctx)}
|
|
330
|
+
/>
|
|
266
331
|
{:else}
|
|
267
332
|
<Table
|
|
268
333
|
{data}
|
|
@@ -277,18 +342,19 @@
|
|
|
277
342
|
{...tableProps}
|
|
278
343
|
rowActions={hasRowActions ? rowActionsSnippet : undefined}>
|
|
279
344
|
{#snippet tools(entry)}
|
|
280
|
-
{#if
|
|
281
|
-
<
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
345
|
+
{#if showEdit}
|
|
346
|
+
<CanAccess collection={collectionName} action="update">
|
|
347
|
+
<UpdateDetailViewButton
|
|
348
|
+
{collectionName}
|
|
349
|
+
recordId={entry.id}
|
|
350
|
+
variant="ghost"
|
|
351
|
+
class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
|
|
352
|
+
Icon={Pencil}
|
|
353
|
+
changes={isRecordingMode ? localChanges.updated.find((u) => String(u.id) === String(entry.id))?.changes : undefined}
|
|
354
|
+
onChanges={isRecordingMode ? (c) => handleUpdate(String(entry.id), c) : undefined}
|
|
355
|
+
onSuccessfullSave={!isRecordingMode ? async () => { params = { ...params }; } : undefined}
|
|
356
|
+
></UpdateDetailViewButton>
|
|
357
|
+
</CanAccess>
|
|
292
358
|
{/if}
|
|
293
359
|
{#if parentContext}
|
|
294
360
|
<Button
|
|
@@ -317,7 +383,7 @@
|
|
|
317
383
|
fieldName={column.id}
|
|
318
384
|
{value}
|
|
319
385
|
{entry}
|
|
320
|
-
|
|
386
|
+
refresh={() => { params = { ...params }; }}
|
|
321
387
|
/>
|
|
322
388
|
{/snippet}
|
|
323
389
|
{#snippet collapsible(entry)}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
fieldName: string;
|
|
18
18
|
value: any;
|
|
19
19
|
entry: Record<string, any>;
|
|
20
|
-
|
|
20
|
+
refresh?: () => void;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
let {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
fieldName,
|
|
26
26
|
value,
|
|
27
27
|
entry,
|
|
28
|
-
|
|
28
|
+
refresh,
|
|
29
29
|
}: Props = $props();
|
|
30
30
|
|
|
31
31
|
const field = getField(ctx, fieldName, collectionName);
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
collectionField={polymorphicRelation.from.collection_field}
|
|
50
50
|
idField={polymorphicRelation.from.id_field}
|
|
51
51
|
{entry}
|
|
52
|
-
|
|
52
|
+
{refresh}
|
|
53
53
|
/>
|
|
54
54
|
{:else if isRefrenceField}
|
|
55
55
|
{#if value?.id && value.id !== 0}
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
|
|
68
68
|
Icon={ExternalLink}
|
|
69
69
|
onSuccessfullSave={async () => {
|
|
70
|
-
|
|
70
|
+
refresh?.();
|
|
71
71
|
}}
|
|
72
72
|
/>
|
|
73
73
|
</div>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getStudioContext } from "../../context";
|
|
3
|
-
import type { Changes
|
|
3
|
+
import type { Changes } from "../detailView/utils";
|
|
4
4
|
import type { ParentContext } from "./dataTable.svelte";
|
|
5
|
+
import CanAccess from "../canAccess.svelte";
|
|
5
6
|
import { Download, ListRestart, Plus, Trash, Link } from "lucide-svelte";
|
|
6
7
|
import * as Tooltip from "../ui/tooltip";
|
|
7
8
|
import LlmButton from "../LlmButton.svelte";
|
|
@@ -23,9 +24,9 @@
|
|
|
23
24
|
params: any;
|
|
24
25
|
selectedRecords: string[];
|
|
25
26
|
parentContext?: ParentContext;
|
|
26
|
-
|
|
27
|
+
onLink?: (record: any) => void;
|
|
28
|
+
onCreate?: (changes: Changes) => void;
|
|
27
29
|
showImport?: boolean;
|
|
28
|
-
showCreate?: boolean;
|
|
29
30
|
left?: Snippet<[]>;
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -34,27 +35,24 @@
|
|
|
34
35
|
params = $bindable(),
|
|
35
36
|
selectedRecords = $bindable(),
|
|
36
37
|
parentContext,
|
|
37
|
-
|
|
38
|
+
onLink,
|
|
39
|
+
onCreate,
|
|
38
40
|
showImport = true,
|
|
39
|
-
showCreate = false,
|
|
40
41
|
left
|
|
41
42
|
}: Props = $props();
|
|
42
43
|
|
|
43
|
-
// Local changes object for the create form (recording mode only)
|
|
44
|
-
let createChanges = $state<Changes>({ data: {}, children: {} });
|
|
45
|
-
|
|
46
44
|
function handleLink(record: any) {
|
|
47
|
-
if (
|
|
48
|
-
|
|
45
|
+
if (onLink) {
|
|
46
|
+
onLink(record);
|
|
49
47
|
} else if (parentContext) {
|
|
50
|
-
lobb.updateOne(parentContext.collectionName, String(parentContext.recordId), {}, { [collectionName]: { link: [record.id] } });
|
|
48
|
+
lobb.updateOne(parentContext.collectionName, String(parentContext.recordId), { data: {}, children: { [collectionName]: { link: [record.id] } } });
|
|
51
49
|
resetTable();
|
|
52
50
|
}
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
async function
|
|
56
|
-
if (
|
|
57
|
-
|
|
53
|
+
async function handleCreate(snap: any) {
|
|
54
|
+
if (onCreate) {
|
|
55
|
+
onCreate(snap);
|
|
58
56
|
} else {
|
|
59
57
|
resetTable();
|
|
60
58
|
}
|
|
@@ -169,21 +167,23 @@
|
|
|
169
167
|
>
|
|
170
168
|
{headerIsSmall ? "" : "Refresh"}
|
|
171
169
|
</Button>
|
|
172
|
-
{#if showImport
|
|
173
|
-
<
|
|
174
|
-
<Tooltip.
|
|
175
|
-
<Tooltip.
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
170
|
+
{#if showImport}
|
|
171
|
+
<CanAccess collection={collectionName} action="create">
|
|
172
|
+
<Tooltip.Provider delayDuration={0}>
|
|
173
|
+
<Tooltip.Root>
|
|
174
|
+
<Tooltip.Trigger>
|
|
175
|
+
<ImportButton
|
|
176
|
+
{collectionName}
|
|
177
|
+
variant="outline"
|
|
178
|
+
class="h-7 px-2 text-xs font-normal"
|
|
179
|
+
Icon={Download}
|
|
180
|
+
onSuccessfullSave={() => (params = { ...params })}
|
|
181
|
+
/>
|
|
182
|
+
</Tooltip.Trigger>
|
|
183
|
+
<Tooltip.Content>Import</Tooltip.Content>
|
|
184
|
+
</Tooltip.Root>
|
|
185
|
+
</Tooltip.Provider>
|
|
186
|
+
</CanAccess>
|
|
187
187
|
{/if}
|
|
188
188
|
<ExtensionsComponents
|
|
189
189
|
name="listView.header.actions"
|
|
@@ -202,17 +202,17 @@
|
|
|
202
202
|
{headerIsSmall ? "" : "Link"}
|
|
203
203
|
</SelectRecord>
|
|
204
204
|
{/if}
|
|
205
|
-
{
|
|
205
|
+
<CanAccess collection={collectionName} action="create">
|
|
206
206
|
<CreateDetailViewButton
|
|
207
207
|
{collectionName}
|
|
208
208
|
variant="default"
|
|
209
209
|
class="h-7 px-3 text-xs font-normal"
|
|
210
210
|
Icon={Plus}
|
|
211
|
-
|
|
212
|
-
onSuccessfullSave={
|
|
211
|
+
onChanges={onCreate ? handleCreate : undefined}
|
|
212
|
+
onSuccessfullSave={onCreate ? undefined : handleCreate}
|
|
213
213
|
>
|
|
214
214
|
{headerIsSmall ? "" : "Create"}
|
|
215
215
|
</CreateDetailViewButton>
|
|
216
|
-
|
|
216
|
+
</CanAccess>
|
|
217
217
|
</div>
|
|
218
218
|
</div>
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
collectionField: string;
|
|
7
7
|
idField: string;
|
|
8
8
|
entry: Record<string, any>;
|
|
9
|
-
|
|
9
|
+
refresh?: () => void;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
let {
|
|
13
13
|
collectionField,
|
|
14
14
|
idField,
|
|
15
15
|
entry,
|
|
16
|
-
|
|
16
|
+
refresh,
|
|
17
17
|
}: Props = $props();
|
|
18
18
|
|
|
19
19
|
const targetCollection = $derived(entry[collectionField] ?? null);
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
|
|
35
35
|
Icon={ExternalLink}
|
|
36
36
|
onSuccessfullSave={async () => {
|
|
37
|
-
|
|
37
|
+
refresh?.();
|
|
38
38
|
}}
|
|
39
39
|
/>
|
|
40
40
|
</div>
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
showFooter?: boolean;
|
|
20
20
|
tableProps?: Partial<TableProps>;
|
|
21
21
|
tabs?: CollectionTab[];
|
|
22
|
+
view?: { id: string; [key: string]: any };
|
|
22
23
|
onClose?: () => void;
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -32,6 +33,7 @@
|
|
|
32
33
|
showFooter = true,
|
|
33
34
|
tableProps,
|
|
34
35
|
tabs,
|
|
36
|
+
view,
|
|
35
37
|
onClose,
|
|
36
38
|
}: Props = $props();
|
|
37
39
|
|
|
@@ -78,6 +80,7 @@
|
|
|
78
80
|
{showFooter}
|
|
79
81
|
{tableProps}
|
|
80
82
|
{tabs}
|
|
83
|
+
{view}
|
|
81
84
|
/>
|
|
82
85
|
</div>
|
|
83
86
|
</div>
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
|
+
import type { Changes } from "../utils";
|
|
3
|
+
import type { Snippet } from "svelte";
|
|
4
|
+
|
|
2
5
|
interface SubmitButton {
|
|
3
6
|
text: string;
|
|
4
7
|
icon: any;
|
|
@@ -12,7 +15,7 @@
|
|
|
12
15
|
title?: Snippet<[string]>;
|
|
13
16
|
onSuccessfullSave?: (entry: any) => Promise<void>;
|
|
14
17
|
onCancel?: () => Promise<void>;
|
|
15
|
-
|
|
18
|
+
onChanges?: (changes: Changes) => void;
|
|
16
19
|
}
|
|
17
20
|
</script>
|
|
18
21
|
|
|
@@ -24,11 +27,8 @@
|
|
|
24
27
|
import { untrack } from "svelte";
|
|
25
28
|
|
|
26
29
|
const { lobb, ctx } = getStudioContext();
|
|
27
|
-
import
|
|
28
|
-
import {
|
|
29
|
-
import type { Changes } from "../utils";
|
|
30
|
-
import { getChangedProperties } from "../../../utils";
|
|
31
|
-
import type { Snippet } from "svelte";
|
|
30
|
+
import CreateDetailViewChildren from "./createDetailViewChildren.svelte";
|
|
31
|
+
import { getDefaultEntry } from "../utils";
|
|
32
32
|
import DetailView from "../detailView.svelte";
|
|
33
33
|
import Drawer from "../../drawer.svelte";
|
|
34
34
|
|
|
@@ -40,80 +40,53 @@
|
|
|
40
40
|
onSuccessfullSave,
|
|
41
41
|
title,
|
|
42
42
|
submitButton,
|
|
43
|
-
|
|
43
|
+
onChanges,
|
|
44
44
|
}: CreateDetailViewProp = $props();
|
|
45
45
|
|
|
46
|
-
const isRecordingMode =
|
|
47
|
-
|
|
48
|
-
const changes = passedChanges as Changes;
|
|
46
|
+
const isRecordingMode = onChanges !== undefined;
|
|
47
|
+
let changes = $state<Changes>({ data: {}, children: {} });
|
|
49
48
|
|
|
50
49
|
const fieldNames = Object.keys(ctx.meta.collections[collectionName].fields);
|
|
51
50
|
let values = $state(getDefaultEntry(ctx, fieldNames, collectionName, passedValues));
|
|
52
51
|
let fieldsErrors: Record<string, any> = $state({});
|
|
53
52
|
|
|
54
|
-
const childCollections = ctx.meta.relations
|
|
55
|
-
.filter((r) => r.to.collection === collectionName)
|
|
56
|
-
.map((r) => (r as any).from.collection);
|
|
57
|
-
|
|
58
|
-
const subCollectionsValues: Record<string, any> = {};
|
|
59
|
-
for (const col of childCollections) {
|
|
60
|
-
if (passedValues[col]) subCollectionsValues[col] = passedValues[col];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
53
|
$effect(() => {
|
|
64
54
|
const snap = $state.snapshot(values);
|
|
65
|
-
|
|
66
55
|
untrack(() => {
|
|
67
56
|
const data: Record<string, any> = {};
|
|
68
|
-
const children: Record<string, any> = {};
|
|
69
|
-
|
|
70
57
|
for (const [key, value] of Object.entries(snap)) {
|
|
71
|
-
if (
|
|
72
|
-
children[key] = {
|
|
73
|
-
created: (value as any[]).filter((r) => !r.id).map((r) => ({ data: r })),
|
|
74
|
-
updated: [],
|
|
75
|
-
deleted: [],
|
|
76
|
-
linked: (value as any[]).filter((r) => r.id).map((r) => r.id),
|
|
77
|
-
unlinked: [],
|
|
78
|
-
};
|
|
79
|
-
} else if (value !== null && value !== undefined && value !== '') {
|
|
58
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
80
59
|
data[key] = value;
|
|
81
60
|
}
|
|
82
61
|
}
|
|
83
|
-
|
|
84
62
|
changes.data = data;
|
|
85
|
-
changes.children = children;
|
|
86
|
-
|
|
87
|
-
if (!isRecordingMode) {
|
|
88
|
-
console.log(`[${collectionName}] changes:`, $state.snapshot(changes));
|
|
89
|
-
}
|
|
90
63
|
});
|
|
91
64
|
});
|
|
92
65
|
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
66
|
+
function buildPayload(changes: Changes): { data: Record<string, any>; children?: Record<string, any> } {
|
|
67
|
+
const result: Record<string, any> = {};
|
|
68
|
+
for (const [collection, ops] of Object.entries(changes.children)) {
|
|
69
|
+
const hasOps = ops.created.length || ops.linked.length;
|
|
70
|
+
if (!hasOps) continue;
|
|
71
|
+
result[collection] = {
|
|
72
|
+
...(ops.created.length ? { create: ops.created.map((op) => op.data) } : {}),
|
|
73
|
+
...(ops.linked.length ? { link: ops.linked.map((r) => r.id) } : {}),
|
|
74
|
+
};
|
|
97
75
|
}
|
|
76
|
+
const children = Object.keys(result).length ? result : undefined;
|
|
77
|
+
return { data: changes.data, ...(children ? { children } : {}) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function handleCancel() {
|
|
98
81
|
onCancel?.();
|
|
99
82
|
}
|
|
100
83
|
|
|
101
84
|
async function handleSave() {
|
|
102
85
|
const snap = $state.snapshot(changes);
|
|
103
|
-
|
|
104
|
-
const children = buildChildren(ctx, collectionName, { ...snap.data, ...Object.fromEntries(
|
|
105
|
-
Object.entries(snap.children).map(([col, ops]) => [
|
|
106
|
-
col,
|
|
107
|
-
[
|
|
108
|
-
...(ops.created.map((op) => op.data)),
|
|
109
|
-
...(ops.linked.map((id) => ({ id }))),
|
|
110
|
-
]
|
|
111
|
-
])
|
|
112
|
-
)});
|
|
113
|
-
|
|
114
|
-
const response = await lobb.createOne(collectionName, snap.data, children, undefined, isRecordingMode);
|
|
86
|
+
const response = await lobb.createOne(collectionName, buildPayload(snap), undefined, isRecordingMode);
|
|
115
87
|
|
|
116
88
|
if (response.status === 204) {
|
|
89
|
+
onChanges?.(snap);
|
|
117
90
|
if (onSuccessfullSave) await onSuccessfullSave(snap);
|
|
118
91
|
toast.success(`The record was successfully created`);
|
|
119
92
|
handleCancel();
|
|
@@ -133,6 +106,7 @@
|
|
|
133
106
|
}
|
|
134
107
|
}
|
|
135
108
|
|
|
109
|
+
onChanges?.(snap);
|
|
136
110
|
if (onSuccessfullSave) await onSuccessfullSave(snap);
|
|
137
111
|
toast.success(`The record was successfully created`);
|
|
138
112
|
onCancel?.();
|
|
@@ -161,7 +135,7 @@
|
|
|
161
135
|
<div class="flex-1 overflow-y-auto">
|
|
162
136
|
<DetailView {collectionName} bind:entry={values} {fieldsErrors} />
|
|
163
137
|
{#if showRelatedRecords}
|
|
164
|
-
<
|
|
138
|
+
<CreateDetailViewChildren {collectionName} {changes} onChanges={(children) => { changes.children = children; }} />
|
|
165
139
|
{/if}
|
|
166
140
|
</div>
|
|
167
141
|
<div class="flex h-12 items-center justify-end gap-2 border-t px-4">
|