@lobb-js/studio 0.31.0 → 0.32.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 +84 -37
- package/dist/components/dataTable/dataTable.svelte.d.ts +4 -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 +13 -14
- package/dist/components/dataTable/header.svelte.d.ts +3 -2
- package/dist/components/dataTable/polymorphicFieldCell.svelte +3 -3
- package/dist/components/dataTable/polymorphicFieldCell.svelte.d.ts +2 -2
- 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/package.json +2 -2
- package/src/lib/components/dataTable/dataTable.svelte +84 -37
- package/src/lib/components/dataTable/fieldCell.svelte +4 -4
- package/src/lib/components/dataTable/header.svelte +13 -14
- package/src/lib/components/dataTable/polymorphicFieldCell.svelte +3 -3
- 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/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
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
import { showDialog } from "../../actions";
|
|
22
22
|
import UpdateDetailViewButton from "../detailView/update/updateDetailViewButton.svelte";
|
|
23
23
|
import type { Snippet } from "svelte";
|
|
24
|
-
import type {
|
|
24
|
+
import type { ChildrenChanges } from "../detailView/utils";
|
|
25
25
|
import ExtensionsComponents from "../extensionsComponents.svelte";
|
|
26
26
|
import { getExtensionUtils, loadExtensionComponents } from "../../extensions/extensionUtils";
|
|
27
27
|
import { emitEvent } from "../../eventSystem";
|
|
28
|
-
import { onMount } from "svelte";
|
|
28
|
+
import { onMount, 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,11 +37,14 @@
|
|
|
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<[]>;
|
|
@@ -52,16 +55,24 @@
|
|
|
52
55
|
filter,
|
|
53
56
|
searchParams,
|
|
54
57
|
parentContext,
|
|
55
|
-
|
|
58
|
+
onChanges,
|
|
59
|
+
changes,
|
|
56
60
|
showHeader = true,
|
|
57
61
|
showFooter = true,
|
|
58
62
|
showImport = true,
|
|
59
63
|
showDelete = false,
|
|
64
|
+
showEdit = true,
|
|
65
|
+
onDataLoad,
|
|
60
66
|
tableProps,
|
|
61
67
|
tabs,
|
|
62
68
|
headerLeft,
|
|
63
69
|
}: Props = $props();
|
|
64
70
|
|
|
71
|
+
const isRecordingMode = onChanges !== undefined;
|
|
72
|
+
let localChanges = $state<ChildrenChanges>(
|
|
73
|
+
untrack(() => changes) ?? { created: [], updated: [], deleted: [], linked: [], unlinked: [] }
|
|
74
|
+
);
|
|
75
|
+
|
|
65
76
|
// Gate row/header buttons by the current user's permissions:
|
|
66
77
|
// - showUpdate → per-row edit button
|
|
67
78
|
// - showCreate → header's Create + Import buttons (passed to Header)
|
|
@@ -76,40 +87,29 @@
|
|
|
76
87
|
showCreate = create === true;
|
|
77
88
|
});
|
|
78
89
|
|
|
79
|
-
|
|
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
|
-
|
|
89
|
-
// Derives the displayed rows by applying changes on top of server data.
|
|
90
|
-
// This is the single place responsible for optimistic UI — no handler touches data directly.
|
|
90
|
+
// Derives the displayed rows by applying localChanges on top of server data.
|
|
91
91
|
const data = $derived.by(() => {
|
|
92
|
-
if (!
|
|
92
|
+
if (!isRecordingMode) return serverData;
|
|
93
93
|
|
|
94
94
|
const removedIds = new Set([
|
|
95
|
-
...
|
|
96
|
-
...
|
|
95
|
+
...localChanges.deleted.map((r: any) => String(r.id)),
|
|
96
|
+
...localChanges.unlinked.map((r: any) => String(r.id)),
|
|
97
97
|
]);
|
|
98
98
|
|
|
99
99
|
let result = serverData.filter((r: any) => !removedIds.has(String(r.id)));
|
|
100
100
|
|
|
101
101
|
result = result.map((r: any) => {
|
|
102
|
-
const update =
|
|
103
|
-
return update && Object.keys(update.data).length ? { ...r, ...update.data } : r;
|
|
102
|
+
const update = localChanges.updated.find((u) => String(u.id) === String(r.id));
|
|
103
|
+
return update && Object.keys(update.changes.data).length ? { ...r, ...update.changes.data } : r;
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
for (const record of
|
|
106
|
+
for (const record of localChanges.linked) {
|
|
107
107
|
if (!result.some((r: any) => String(r.id) === String(record.id))) {
|
|
108
108
|
result = [...result, record];
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
for (const item of
|
|
112
|
+
for (const item of localChanges.created) {
|
|
113
113
|
result = [...result, { ...item.data, _pending: true }];
|
|
114
114
|
}
|
|
115
115
|
|
|
@@ -189,18 +189,26 @@
|
|
|
189
189
|
const res = await response.json();
|
|
190
190
|
serverData = res.data;
|
|
191
191
|
totalCount = res.meta.totalCount;
|
|
192
|
+
onDataLoad?.(totalCount);
|
|
192
193
|
loading = false;
|
|
193
194
|
}
|
|
194
195
|
|
|
195
196
|
async function handleDelete(entryId: string) {
|
|
196
197
|
const result = await showDialog("Are you sure?", "This will permanently delete the record.");
|
|
197
198
|
if (!result) return;
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
if (isRecordingMode) {
|
|
200
|
+
// If the record was locally linked (not yet in DB), just cancel the link instead of marking for deletion.
|
|
201
|
+
const linkedIdx = localChanges.linked.findIndex((r: any) => String(r.id) === String(entryId));
|
|
202
|
+
if (linkedIdx !== -1) {
|
|
203
|
+
localChanges.linked.splice(linkedIdx, 1);
|
|
204
|
+
} else {
|
|
205
|
+
const record = serverData.find((r: any) => String(r.id) === String(entryId));
|
|
206
|
+
if (record) localChanges.deleted.push($state.snapshot(record));
|
|
207
|
+
}
|
|
208
|
+
onChanges?.($state.snapshot(localChanges));
|
|
201
209
|
} else if (parentContext) {
|
|
202
210
|
serverData = serverData.filter((r: any) => String(r.id) !== String(entryId));
|
|
203
|
-
await lobb.updateOne(parentContext.collectionName, String(parentContext.recordId), {}, { [collectionName]: { delete: [entryId] } });
|
|
211
|
+
await lobb.updateOne(parentContext.collectionName, String(parentContext.recordId), { data: {}, children: { [collectionName]: { delete: [entryId] } } });
|
|
204
212
|
params = { ...params };
|
|
205
213
|
} else {
|
|
206
214
|
serverData = serverData.filter((r: any) => String(r.id) !== String(entryId));
|
|
@@ -212,16 +220,55 @@
|
|
|
212
220
|
async function handleUnlink(entryId: string) {
|
|
213
221
|
const result = await showDialog("Are you sure?", "This will unlink the record without deleting it.");
|
|
214
222
|
if (!result) return;
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
223
|
+
if (isRecordingMode) {
|
|
224
|
+
// If the record was locally linked this session, just cancel the link — net effect is no change.
|
|
225
|
+
const linkedIdx = localChanges.linked.findIndex((r: any) => String(r.id) === String(entryId));
|
|
226
|
+
if (linkedIdx !== -1) {
|
|
227
|
+
localChanges.linked.splice(linkedIdx, 1);
|
|
228
|
+
} else {
|
|
229
|
+
const record = serverData.find((r: any) => String(r.id) === String(entryId));
|
|
230
|
+
if (record) localChanges.unlinked.push($state.snapshot(record));
|
|
231
|
+
}
|
|
232
|
+
onChanges?.($state.snapshot(localChanges));
|
|
218
233
|
} else {
|
|
219
234
|
serverData = serverData.filter((r: any) => String(r.id) !== String(entryId));
|
|
220
|
-
await lobb.updateOne(parentContext!.collectionName, String(parentContext!.recordId), {}, { [collectionName]: { unlink: [entryId] } });
|
|
235
|
+
await lobb.updateOne(parentContext!.collectionName, String(parentContext!.recordId), { data: {}, children: { [collectionName]: { unlink: [entryId] } } });
|
|
221
236
|
params = { ...params };
|
|
222
237
|
}
|
|
223
238
|
}
|
|
224
239
|
|
|
240
|
+
function handleLink(record: any) {
|
|
241
|
+
// If the record was locally unlinked this session, just cancel the unlink — net effect is no change.
|
|
242
|
+
const unlinkedIdx = localChanges.unlinked.findIndex((r: any) => String(r.id) === String(record.id));
|
|
243
|
+
if (unlinkedIdx !== -1) {
|
|
244
|
+
localChanges.unlinked.splice(unlinkedIdx, 1);
|
|
245
|
+
} else if (!localChanges.linked.some((r: any) => String(r.id) === String(record.id))) {
|
|
246
|
+
localChanges.linked.push(record);
|
|
247
|
+
}
|
|
248
|
+
onChanges?.($state.snapshot(localChanges));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
$effect(() => {
|
|
252
|
+
if (isRecordingMode) {
|
|
253
|
+
console.log(`[DataTable:${collectionName}] localChanges:`, $state.snapshot(localChanges));
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
function handleCreate(changes: import("../detailView/utils").Changes) {
|
|
258
|
+
localChanges.created.push({ data: changes.data });
|
|
259
|
+
onChanges?.($state.snapshot(localChanges));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function handleUpdate(id: string, editChanges: import("../detailView/utils").Changes) {
|
|
263
|
+
const existing = localChanges.updated.find((u) => String(u.id) === id);
|
|
264
|
+
if (existing) {
|
|
265
|
+
existing.changes = editChanges;
|
|
266
|
+
} else {
|
|
267
|
+
localChanges.updated.push({ id, changes: editChanges });
|
|
268
|
+
}
|
|
269
|
+
onChanges?.($state.snapshot(localChanges));
|
|
270
|
+
}
|
|
271
|
+
|
|
225
272
|
</script>
|
|
226
273
|
|
|
227
274
|
<div
|
|
@@ -246,7 +293,8 @@
|
|
|
246
293
|
{showImport}
|
|
247
294
|
{showCreate}
|
|
248
295
|
{parentContext}
|
|
249
|
-
{
|
|
296
|
+
onLink={isRecordingMode ? handleLink : undefined}
|
|
297
|
+
onCreate={isRecordingMode ? handleCreate : undefined}
|
|
250
298
|
>
|
|
251
299
|
{#snippet left()}
|
|
252
300
|
{@render headerLeft?.()}
|
|
@@ -277,17 +325,16 @@
|
|
|
277
325
|
{...tableProps}
|
|
278
326
|
rowActions={hasRowActions ? rowActionsSnippet : undefined}>
|
|
279
327
|
{#snippet tools(entry)}
|
|
280
|
-
{#if showUpdate}
|
|
328
|
+
{#if showUpdate && showEdit}
|
|
281
329
|
<UpdateDetailViewButton
|
|
282
330
|
{collectionName}
|
|
283
331
|
recordId={entry.id}
|
|
284
332
|
variant="ghost"
|
|
285
333
|
class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
|
|
286
334
|
Icon={Pencil}
|
|
287
|
-
changes={
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}}
|
|
335
|
+
changes={isRecordingMode ? localChanges.updated.find((u) => String(u.id) === String(entry.id))?.changes : undefined}
|
|
336
|
+
onChanges={isRecordingMode ? (c) => handleUpdate(String(entry.id), c) : undefined}
|
|
337
|
+
onSuccessfullSave={!isRecordingMode ? async () => { params = { ...params }; } : undefined}
|
|
291
338
|
></UpdateDetailViewButton>
|
|
292
339
|
{/if}
|
|
293
340
|
{#if parentContext}
|
|
@@ -317,7 +364,7 @@
|
|
|
317
364
|
fieldName={column.id}
|
|
318
365
|
{value}
|
|
319
366
|
{entry}
|
|
320
|
-
|
|
367
|
+
refresh={() => { params = { ...params }; }}
|
|
321
368
|
/>
|
|
322
369
|
{/snippet}
|
|
323
370
|
{#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,6 +1,6 @@
|
|
|
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
5
|
import { Download, ListRestart, Plus, Trash, Link } from "lucide-svelte";
|
|
6
6
|
import * as Tooltip from "../ui/tooltip";
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
params: any;
|
|
24
24
|
selectedRecords: string[];
|
|
25
25
|
parentContext?: ParentContext;
|
|
26
|
-
|
|
26
|
+
onLink?: (record: any) => void;
|
|
27
|
+
onCreate?: (changes: Changes) => void;
|
|
27
28
|
showImport?: boolean;
|
|
28
29
|
showCreate?: boolean;
|
|
29
30
|
left?: Snippet<[]>;
|
|
@@ -34,27 +35,25 @@
|
|
|
34
35
|
params = $bindable(),
|
|
35
36
|
selectedRecords = $bindable(),
|
|
36
37
|
parentContext,
|
|
37
|
-
|
|
38
|
+
onLink,
|
|
39
|
+
onCreate,
|
|
38
40
|
showImport = true,
|
|
39
41
|
showCreate = false,
|
|
40
42
|
left
|
|
41
43
|
}: Props = $props();
|
|
42
44
|
|
|
43
|
-
// Local changes object for the create form (recording mode only)
|
|
44
|
-
let createChanges = $state<Changes>({ data: {}, children: {} });
|
|
45
|
-
|
|
46
45
|
function handleLink(record: any) {
|
|
47
|
-
if (
|
|
48
|
-
|
|
46
|
+
if (onLink) {
|
|
47
|
+
onLink(record);
|
|
49
48
|
} else if (parentContext) {
|
|
50
|
-
lobb.updateOne(parentContext.collectionName, String(parentContext.recordId), {}, { [collectionName]: { link: [record.id] } });
|
|
49
|
+
lobb.updateOne(parentContext.collectionName, String(parentContext.recordId), { data: {}, children: { [collectionName]: { link: [record.id] } } });
|
|
51
50
|
resetTable();
|
|
52
51
|
}
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
async function
|
|
56
|
-
if (
|
|
57
|
-
|
|
54
|
+
async function handleCreate(snap: any) {
|
|
55
|
+
if (onCreate) {
|
|
56
|
+
onCreate(snap);
|
|
58
57
|
} else {
|
|
59
58
|
resetTable();
|
|
60
59
|
}
|
|
@@ -208,8 +207,8 @@
|
|
|
208
207
|
variant="default"
|
|
209
208
|
class="h-7 px-3 text-xs font-normal"
|
|
210
209
|
Icon={Plus}
|
|
211
|
-
|
|
212
|
-
onSuccessfullSave={
|
|
210
|
+
onChanges={onCreate ? handleCreate : undefined}
|
|
211
|
+
onSuccessfullSave={onCreate ? undefined : handleCreate}
|
|
213
212
|
>
|
|
214
213
|
{headerIsSmall ? "" : "Create"}
|
|
215
214
|
</CreateDetailViewButton>
|
|
@@ -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>
|
|
@@ -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">
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import DataTable from "../../dataTable/dataTable.svelte";
|
|
3
|
+
import { getStudioContext } from "../../../context";
|
|
4
|
+
import { Table, Link, Plus } from "lucide-svelte";
|
|
5
|
+
import { untrack } from "svelte";
|
|
6
|
+
import CreateDetailViewButton from "./createDetailViewButton.svelte";
|
|
7
|
+
import SelectRecord from "../../selectRecord.svelte";
|
|
8
|
+
|
|
9
|
+
const { ctx } = getStudioContext();
|
|
10
|
+
|
|
11
|
+
import type { Changes, ChildrenChanges } from "../utils";
|
|
12
|
+
|
|
13
|
+
interface LocalProp {
|
|
14
|
+
collectionName: string;
|
|
15
|
+
changes?: Changes;
|
|
16
|
+
onChanges?: (children: Changes["children"]) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let { collectionName, changes, onChanges }: LocalProp = $props();
|
|
20
|
+
|
|
21
|
+
const children = (ctx.meta.collections[collectionName]?.children ?? [])
|
|
22
|
+
.filter((c: any) => c.type === "fk" || c.type === "m2m" || c.type === "polymorphic");
|
|
23
|
+
|
|
24
|
+
let localChildren = $state<Changes["children"]>(untrack(() => changes?.children ?? {}));
|
|
25
|
+
|
|
26
|
+
function handleChildChanges(collection: string, updated: ChildrenChanges) {
|
|
27
|
+
localChildren[collection] = updated;
|
|
28
|
+
onChanges?.($state.snapshot(localChildren));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function handleEmptyCreate(collection: string, c: Changes) {
|
|
32
|
+
if (!localChildren[collection]) {
|
|
33
|
+
localChildren[collection] = { created: [], updated: [], deleted: [], linked: [], unlinked: [] };
|
|
34
|
+
}
|
|
35
|
+
localChildren[collection].created.push({ data: c.data });
|
|
36
|
+
onChanges?.($state.snapshot(localChildren));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function handleEmptyLink(collection: string, record: any) {
|
|
40
|
+
if (!localChildren[collection]) {
|
|
41
|
+
localChildren[collection] = { created: [], updated: [], deleted: [], linked: [], unlinked: [] };
|
|
42
|
+
}
|
|
43
|
+
localChildren[collection].linked.push(record);
|
|
44
|
+
onChanges?.($state.snapshot(localChildren));
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
{#if children.length}
|
|
49
|
+
<div class="flex flex-col gap-3 border-t p-4">
|
|
50
|
+
<div class="flex items-center gap-2">
|
|
51
|
+
<Link size="14" class="text-muted-foreground" />
|
|
52
|
+
<span class="text-sm font-medium">Sub Records</span>
|
|
53
|
+
</div>
|
|
54
|
+
{#each children as child}
|
|
55
|
+
{@const localAdditions = (localChildren[child.collection]?.created.length ?? 0) + (localChildren[child.collection]?.linked.length ?? 0)}
|
|
56
|
+
{#if localAdditions === 0}
|
|
57
|
+
<div class="rounded-lg border bg-muted-soft overflow-hidden flex flex-col">
|
|
58
|
+
<div class="flex flex-col items-center justify-center gap-3 py-6 px-4">
|
|
59
|
+
<div class="flex flex-col items-center gap-2 text-center">
|
|
60
|
+
<div class="flex items-center gap-1.5 text-sm font-medium text-muted-foreground">
|
|
61
|
+
<span>No records in</span>
|
|
62
|
+
<span class="rounded-md border bg-muted px-2 py-0.5 text-xs font-normal">{child.collection}</span>
|
|
63
|
+
</div>
|
|
64
|
+
<span class="text-xs text-muted-foreground/70">Create a new record or link an existing one.</span>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="flex gap-2">
|
|
67
|
+
<SelectRecord
|
|
68
|
+
collectionName={child.collection}
|
|
69
|
+
variant="outline"
|
|
70
|
+
class="h-7 px-3 text-xs font-normal"
|
|
71
|
+
Icon={Link}
|
|
72
|
+
onSelect={(r) => handleEmptyLink(child.collection, r)}
|
|
73
|
+
>
|
|
74
|
+
Link
|
|
75
|
+
</SelectRecord>
|
|
76
|
+
<CreateDetailViewButton
|
|
77
|
+
collectionName={child.collection}
|
|
78
|
+
variant="default"
|
|
79
|
+
class="h-7 px-3 text-xs font-normal"
|
|
80
|
+
Icon={Plus}
|
|
81
|
+
onChanges={(c) => handleEmptyCreate(child.collection, c)}
|
|
82
|
+
>
|
|
83
|
+
Create
|
|
84
|
+
</CreateDetailViewButton>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
{:else}
|
|
89
|
+
<div class="rounded-lg border bg-background overflow-hidden flex flex-col max-h-96">
|
|
90
|
+
<DataTable
|
|
91
|
+
collectionName={child.collection}
|
|
92
|
+
searchParams={{ children_of: collectionName, parent_id: -1 }}
|
|
93
|
+
onChanges={(updated) => handleChildChanges(child.collection, updated)}
|
|
94
|
+
changes={localChildren[child.collection]}
|
|
95
|
+
showImport={false}
|
|
96
|
+
showHeader={true}
|
|
97
|
+
showFooter={false}
|
|
98
|
+
showDelete={false}
|
|
99
|
+
showEdit={false}
|
|
100
|
+
tableProps={{ showLastColumnBorder: false, showLastRowBorder: true }}
|
|
101
|
+
>
|
|
102
|
+
{#snippet headerLeft()}
|
|
103
|
+
<div class="flex items-center gap-2 px-1">
|
|
104
|
+
<Table size="14" class="text-muted-foreground" />
|
|
105
|
+
<span class="text-sm font-medium">{child.collection}</span>
|
|
106
|
+
</div>
|
|
107
|
+
{/snippet}
|
|
108
|
+
</DataTable>
|
|
109
|
+
</div>
|
|
110
|
+
{/if}
|
|
111
|
+
{/each}
|
|
112
|
+
</div>
|
|
113
|
+
{/if}
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
class="h-7 px-2 font-normal text-xs"
|
|
146
146
|
Icon={Plus}
|
|
147
147
|
{collectionName}
|
|
148
|
-
|
|
148
|
+
onChanges={(updated) => { addChanges = updated; }}
|
|
149
149
|
showRelatedRecords={true}
|
|
150
150
|
onSuccessfullSave={onRecordAdd}
|
|
151
151
|
values={createValues}
|
|
@@ -200,7 +200,7 @@
|
|
|
200
200
|
class="h-6 w-6 text-muted-foreground hover:bg-transparent p-0"
|
|
201
201
|
Icon={Pencil}
|
|
202
202
|
{collectionName}
|
|
203
|
-
|
|
203
|
+
onChanges={(updated) => { editChanges = updated; }}
|
|
204
204
|
showRelatedRecords={true}
|
|
205
205
|
onSuccessfullSave={(entry) =>
|
|
206
206
|
onRecordOverride(entry, index)}
|