@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)}
@@ -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>
@@ -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>
@@ -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>
@@ -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">
@@ -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
- changes={addChanges}
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
- changes={editChanges}
203
+ onChanges={(updated) => { editChanges = updated; }}
204
204
  showRelatedRecords={true}
205
205
  onSuccessfullSave={(entry) =>
206
206
  onRecordOverride(entry, index)}