@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.
Files changed (58) hide show
  1. package/dist/actions.d.ts +4 -0
  2. package/dist/applyStudioTheme.d.ts +2 -0
  3. package/dist/applyStudioTheme.js +36 -0
  4. package/dist/components/Studio.svelte +2 -0
  5. package/dist/components/canAccess.svelte +52 -0
  6. package/dist/components/canAccess.svelte.d.ts +10 -0
  7. package/dist/components/dataTable/dataTable.svelte +124 -58
  8. package/dist/components/dataTable/dataTable.svelte.d.ts +8 -1
  9. package/dist/components/dataTable/fieldCell.svelte +4 -4
  10. package/dist/components/dataTable/fieldCell.svelte.d.ts +2 -2
  11. package/dist/components/dataTable/header.svelte +33 -33
  12. package/dist/components/dataTable/header.svelte.d.ts +3 -3
  13. package/dist/components/dataTable/polymorphicFieldCell.svelte +3 -3
  14. package/dist/components/dataTable/polymorphicFieldCell.svelte.d.ts +2 -2
  15. package/dist/components/dataTablePopup/dataTablePopup.svelte +3 -0
  16. package/dist/components/dataTablePopup/dataTablePopup.svelte.d.ts +4 -0
  17. package/dist/components/detailView/create/createDetailView.svelte +28 -54
  18. package/dist/components/detailView/create/createDetailView.svelte.d.ts +4 -3
  19. package/dist/components/detailView/create/createDetailViewChildren.svelte +113 -0
  20. package/dist/components/detailView/create/createDetailViewChildren.svelte.d.ts +9 -0
  21. package/dist/components/detailView/create/createManyView.svelte +2 -2
  22. package/dist/components/detailView/update/updateDetailView.svelte +46 -40
  23. package/dist/components/detailView/update/updateDetailView.svelte.d.ts +5 -3
  24. package/dist/components/detailView/update/updateDetailViewButton.svelte +0 -1
  25. package/dist/components/detailView/update/updateDetailViewChildren.svelte +122 -0
  26. package/dist/components/detailView/update/updateDetailViewChildren.svelte.d.ts +10 -0
  27. package/dist/components/detailView/utils.d.ts +1 -2
  28. package/dist/components/importButton.svelte +1 -1
  29. package/dist/components/richTextEditor.svelte +2 -0
  30. package/dist/components/workflowEditor.svelte +6 -4
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +2 -0
  33. package/dist/store.types.d.ts +7 -0
  34. package/package.json +3 -3
  35. package/src/lib/actions.ts +1 -0
  36. package/src/lib/applyStudioTheme.ts +38 -0
  37. package/src/lib/components/Studio.svelte +2 -0
  38. package/src/lib/components/canAccess.svelte +52 -0
  39. package/src/lib/components/dataTable/dataTable.svelte +124 -58
  40. package/src/lib/components/dataTable/fieldCell.svelte +4 -4
  41. package/src/lib/components/dataTable/header.svelte +33 -33
  42. package/src/lib/components/dataTable/polymorphicFieldCell.svelte +3 -3
  43. package/src/lib/components/dataTablePopup/dataTablePopup.svelte +3 -0
  44. package/src/lib/components/detailView/create/createDetailView.svelte +28 -54
  45. package/src/lib/components/detailView/create/createDetailViewChildren.svelte +113 -0
  46. package/src/lib/components/detailView/create/createManyView.svelte +2 -2
  47. package/src/lib/components/detailView/update/updateDetailView.svelte +46 -40
  48. package/src/lib/components/detailView/update/updateDetailViewButton.svelte +0 -1
  49. package/src/lib/components/detailView/update/updateDetailViewChildren.svelte +122 -0
  50. package/src/lib/components/detailView/utils.ts +1 -1
  51. package/src/lib/components/importButton.svelte +1 -1
  52. package/src/lib/components/richTextEditor.svelte +2 -0
  53. package/src/lib/components/workflowEditor.svelte +6 -4
  54. package/src/lib/index.ts +2 -0
  55. package/src/lib/store.types.ts +6 -0
  56. package/dist/components/detailView/update/detailViewChildren.svelte +0 -61
  57. package/dist/components/detailView/update/detailViewChildren.svelte.d.ts +0 -9
  58. 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 { Changes, ChildrenChanges } from "../detailView/utils";
25
+ import type { ChildrenChanges } from "../detailView/utils";
25
26
  import ExtensionsComponents from "../extensionsComponents.svelte";
26
27
  import { getExtensionUtils, loadExtensionComponents } from "../../extensions/extensionUtils";
27
- import { emitEvent } from "../../eventSystem";
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
- changes = $bindable<ChildrenChanges | undefined>(undefined),
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
- // Gate row/header buttons by the current user's permissions:
66
- // - showUpdate → per-row edit button
67
- // - showCreate header's Create + Import buttons (passed to Header)
68
- let showUpdate = $state(false);
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 changes on top of server data.
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 (!changes) return serverData;
81
+ if (!isRecordingMode) return serverData;
93
82
 
94
83
  const removedIds = new Set([
95
- ...changes.deleted.map((r) => String(r.id)),
96
- ...changes.unlinked.map((r) => String(r.id)),
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 = changes.updated.find((u) => String(u.id) === String(r.id));
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 changes.linked) {
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 changes.created) {
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 (changes) {
199
- const record = data.find((r: any) => String(r.id) === String(entryId));
200
- if (record) changes.deleted.push($state.snapshot(record));
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 (changes) {
216
- const record = data.find((r: any) => String(r.id) === String(entryId));
217
- if (record) changes.unlinked.push($state.snapshot(record));
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
- {changes}
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 showUpdate}
281
- <UpdateDetailViewButton
282
- {collectionName}
283
- recordId={entry.id}
284
- variant="ghost"
285
- class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
286
- Icon={Pencil}
287
- changes={getOrCreateUpdatedSlot(String(entry.id))}
288
- onSuccessfullSave={async () => {
289
- params = { ...params };
290
- }}
291
- ></UpdateDetailViewButton>
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
- tableParams={params}
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
- 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,7 +1,8 @@
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
+ 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
- changes?: ChildrenChanges;
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
- changes,
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 (changes) {
48
- changes.linked.push(record);
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 handleCreateSuccess(snap: any) {
56
- if (changes) {
57
- changes.created.push({ data: snap.data });
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 && showCreate}
173
- <Tooltip.Provider delayDuration={0}>
174
- <Tooltip.Root>
175
- <Tooltip.Trigger>
176
- <ImportButton
177
- {collectionName}
178
- variant="outline"
179
- class="h-7 px-2 text-xs font-normal"
180
- Icon={Download}
181
- onSuccessfullSave={() => (params = { ...params })}
182
- />
183
- </Tooltip.Trigger>
184
- <Tooltip.Content>Import</Tooltip.Content>
185
- </Tooltip.Root>
186
- </Tooltip.Provider>
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
- {#if showCreate}
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
- changes={changes ? createChanges : undefined}
212
- onSuccessfullSave={handleCreateSuccess}
211
+ onChanges={onCreate ? handleCreate : undefined}
212
+ onSuccessfullSave={onCreate ? undefined : handleCreate}
213
213
  >
214
214
  {headerIsSmall ? "" : "Create"}
215
215
  </CreateDetailViewButton>
216
- {/if}
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
- 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>
@@ -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
- 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">