@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.
Files changed (40) hide show
  1. package/dist/components/dataTable/dataTable.svelte +84 -37
  2. package/dist/components/dataTable/dataTable.svelte.d.ts +4 -1
  3. package/dist/components/dataTable/fieldCell.svelte +4 -4
  4. package/dist/components/dataTable/fieldCell.svelte.d.ts +2 -2
  5. package/dist/components/dataTable/header.svelte +13 -14
  6. package/dist/components/dataTable/header.svelte.d.ts +3 -2
  7. package/dist/components/dataTable/polymorphicFieldCell.svelte +3 -3
  8. package/dist/components/dataTable/polymorphicFieldCell.svelte.d.ts +2 -2
  9. package/dist/components/detailView/create/createDetailView.svelte +28 -54
  10. package/dist/components/detailView/create/createDetailView.svelte.d.ts +4 -3
  11. package/dist/components/detailView/create/createDetailViewChildren.svelte +113 -0
  12. package/dist/components/detailView/create/createDetailViewChildren.svelte.d.ts +9 -0
  13. package/dist/components/detailView/create/createManyView.svelte +2 -2
  14. package/dist/components/detailView/update/updateDetailView.svelte +46 -40
  15. package/dist/components/detailView/update/updateDetailView.svelte.d.ts +5 -3
  16. package/dist/components/detailView/update/updateDetailViewButton.svelte +0 -1
  17. package/dist/components/detailView/update/updateDetailViewChildren.svelte +122 -0
  18. package/dist/components/detailView/update/updateDetailViewChildren.svelte.d.ts +10 -0
  19. package/dist/components/detailView/utils.d.ts +1 -2
  20. package/dist/components/importButton.svelte +1 -1
  21. package/dist/components/richTextEditor.svelte +2 -0
  22. package/dist/components/workflowEditor.svelte +6 -4
  23. package/package.json +2 -2
  24. package/src/lib/components/dataTable/dataTable.svelte +84 -37
  25. package/src/lib/components/dataTable/fieldCell.svelte +4 -4
  26. package/src/lib/components/dataTable/header.svelte +13 -14
  27. package/src/lib/components/dataTable/polymorphicFieldCell.svelte +3 -3
  28. package/src/lib/components/detailView/create/createDetailView.svelte +28 -54
  29. package/src/lib/components/detailView/create/createDetailViewChildren.svelte +113 -0
  30. package/src/lib/components/detailView/create/createManyView.svelte +2 -2
  31. package/src/lib/components/detailView/update/updateDetailView.svelte +46 -40
  32. package/src/lib/components/detailView/update/updateDetailViewButton.svelte +0 -1
  33. package/src/lib/components/detailView/update/updateDetailViewChildren.svelte +122 -0
  34. package/src/lib/components/detailView/utils.ts +1 -1
  35. package/src/lib/components/importButton.svelte +1 -1
  36. package/src/lib/components/richTextEditor.svelte +2 -0
  37. package/src/lib/components/workflowEditor.svelte +6 -4
  38. package/dist/components/detailView/update/detailViewChildren.svelte +0 -61
  39. package/dist/components/detailView/update/detailViewChildren.svelte.d.ts +0 -9
  40. 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 { Changes, ChildrenChanges } from "../detailView/utils";
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
- changes = $bindable<ChildrenChanges | undefined>(undefined),
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
- 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
-
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 (!changes) return serverData;
92
+ if (!isRecordingMode) return serverData;
93
93
 
94
94
  const removedIds = new Set([
95
- ...changes.deleted.map((r) => String(r.id)),
96
- ...changes.unlinked.map((r) => String(r.id)),
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 = changes.updated.find((u) => String(u.id) === String(r.id));
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 changes.linked) {
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 changes.created) {
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 (changes) {
199
- const record = data.find((r: any) => String(r.id) === String(entryId));
200
- if (record) changes.deleted.push($state.snapshot(record));
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 (changes) {
216
- const record = data.find((r: any) => String(r.id) === String(entryId));
217
- if (record) changes.unlinked.push($state.snapshot(record));
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
- {changes}
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={getOrCreateUpdatedSlot(String(entry.id))}
288
- onSuccessfullSave={async () => {
289
- params = { ...params };
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
- tableParams={params}
367
+ refresh={() => { params = { ...params }; }}
321
368
  />
322
369
  {/snippet}
323
370
  {#snippet collapsible(entry)}
@@ -11,15 +11,18 @@ interface Props {
11
11
  filter?: any;
12
12
  searchParams?: Record<string, any>;
13
13
  parentContext?: ParentContext;
14
+ onChanges?: (changes: ChildrenChanges) => void;
14
15
  changes?: ChildrenChanges;
15
16
  showHeader?: boolean;
16
17
  showFooter?: boolean;
17
18
  showImport?: boolean;
18
19
  showDelete?: boolean;
20
+ showEdit?: boolean;
21
+ onDataLoad?: (total: number) => void;
19
22
  tableProps?: Partial<TableProps>;
20
23
  tabs?: CollectionTab[];
21
24
  headerLeft?: Snippet<[]>;
22
25
  }
23
- declare const DataTable: import("svelte").Component<Props, {}, "changes">;
26
+ declare const DataTable: import("svelte").Component<Props, {}, "">;
24
27
  type DataTable = ReturnType<typeof DataTable>;
25
28
  export default DataTable;
@@ -17,7 +17,7 @@
17
17
  fieldName: string;
18
18
  value: any;
19
19
  entry: Record<string, any>;
20
- tableParams?: any;
20
+ refresh?: () => void;
21
21
  }
22
22
 
23
23
  let {
@@ -25,7 +25,7 @@
25
25
  fieldName,
26
26
  value,
27
27
  entry,
28
- tableParams = $bindable(),
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
- bind:tableParams
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
- tableParams = { ...tableParams };
70
+ refresh?.();
71
71
  }}
72
72
  />
73
73
  </div>
@@ -3,8 +3,8 @@ interface Props {
3
3
  fieldName: string;
4
4
  value: any;
5
5
  entry: Record<string, any>;
6
- tableParams?: any;
6
+ refresh?: () => void;
7
7
  }
8
- declare const FieldCell: import("svelte").Component<Props, {}, "tableParams">;
8
+ declare const FieldCell: import("svelte").Component<Props, {}, "">;
9
9
  type FieldCell = ReturnType<typeof FieldCell>;
10
10
  export default FieldCell;
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { getStudioContext } from "../../context";
3
- import type { Changes, ChildrenChanges } from "../detailView/utils";
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
- changes?: ChildrenChanges;
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
- changes,
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 (changes) {
48
- changes.linked.push(record);
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 handleCreateSuccess(snap: any) {
56
- if (changes) {
57
- changes.created.push({ data: snap.data });
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
- changes={changes ? createChanges : undefined}
212
- onSuccessfullSave={handleCreateSuccess}
210
+ onChanges={onCreate ? handleCreate : undefined}
211
+ onSuccessfullSave={onCreate ? undefined : handleCreate}
213
212
  >
214
213
  {headerIsSmall ? "" : "Create"}
215
214
  </CreateDetailViewButton>
@@ -1,4 +1,4 @@
1
- import type { ChildrenChanges } from "../detailView/utils";
1
+ import type { Changes } from "../detailView/utils";
2
2
  import type { ParentContext } from "./dataTable.svelte";
3
3
  import type { Snippet } from "svelte";
4
4
  interface Props {
@@ -6,7 +6,8 @@ interface Props {
6
6
  params: any;
7
7
  selectedRecords: string[];
8
8
  parentContext?: ParentContext;
9
- changes?: ChildrenChanges;
9
+ onLink?: (record: any) => void;
10
+ onCreate?: (changes: Changes) => void;
10
11
  showImport?: boolean;
11
12
  showCreate?: boolean;
12
13
  left?: Snippet<[]>;
@@ -6,14 +6,14 @@
6
6
  collectionField: string;
7
7
  idField: string;
8
8
  entry: Record<string, any>;
9
- tableParams?: any;
9
+ refresh?: () => void;
10
10
  }
11
11
 
12
12
  let {
13
13
  collectionField,
14
14
  idField,
15
15
  entry,
16
- tableParams = $bindable(),
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
- tableParams = { ...tableParams };
37
+ refresh?.();
38
38
  }}
39
39
  />
40
40
  </div>
@@ -2,8 +2,8 @@ interface Props {
2
2
  collectionField: string;
3
3
  idField: string;
4
4
  entry: Record<string, any>;
5
- tableParams?: any;
5
+ refresh?: () => void;
6
6
  }
7
- declare const PolymorphicFieldCell: import("svelte").Component<Props, {}, "tableParams">;
7
+ declare const PolymorphicFieldCell: import("svelte").Component<Props, {}, "">;
8
8
  type PolymorphicFieldCell = ReturnType<typeof PolymorphicFieldCell>;
9
9
  export default PolymorphicFieldCell;
@@ -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
- changes?: import("../utils").Changes | undefined;
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 Children from "./children.svelte";
28
- import { buildChildren, getDefaultEntry } from "../utils";
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
- changes: passedChanges = $bindable<Changes | undefined>(undefined),
43
+ onChanges,
44
44
  }: CreateDetailViewProp = $props();
45
45
 
46
- const isRecordingMode = passedChanges !== undefined;
47
- if (!isRecordingMode) passedChanges = { data: {}, children: {} };
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 (childCollections.includes(key) && Array.isArray(value)) {
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 handleCancel() {
94
- if (isRecordingMode) {
95
- changes.data = {};
96
- changes.children = {};
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
- <Children {collectionName} values={subCollectionsValues} bind:entry={values} />
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">
@@ -1,3 +1,5 @@
1
+ import type { Changes } from "../utils";
2
+ import type { Snippet } from "svelte";
1
3
  interface SubmitButton {
2
4
  text: string;
3
5
  icon: any;
@@ -10,9 +12,8 @@ export interface CreateDetailViewProp {
10
12
  title?: Snippet<[string]>;
11
13
  onSuccessfullSave?: (entry: any) => Promise<void>;
12
14
  onCancel?: () => Promise<void>;
13
- changes?: import("../utils").Changes | undefined;
15
+ onChanges?: (changes: Changes) => void;
14
16
  }
15
- import type { Snippet } from "svelte";
16
- declare const CreateDetailView: import("svelte").Component<CreateDetailViewProp, {}, "changes">;
17
+ declare const CreateDetailView: import("svelte").Component<CreateDetailViewProp, {}, "">;
17
18
  type CreateDetailView = ReturnType<typeof CreateDetailView>;
18
19
  export default CreateDetailView;