@lobb-js/studio 0.25.0 → 0.27.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 (39) hide show
  1. package/dist/components/dataTable/dataTable.svelte +77 -14
  2. package/dist/components/dataTable/dataTable.svelte.d.ts +25 -0
  3. package/dist/components/dataTable/header.svelte +88 -24
  4. package/dist/components/dataTable/header.svelte.d.ts +4 -0
  5. package/dist/components/dataTable/listViewChildren.svelte +106 -0
  6. package/dist/components/dataTable/listViewChildren.svelte.d.ts +9 -0
  7. package/dist/components/dataTable/table.svelte +1 -1
  8. package/dist/components/detailView/create/createManyView.svelte +2 -2
  9. package/dist/components/detailView/update/detailViewChildren.svelte +72 -0
  10. package/dist/components/detailView/update/detailViewChildren.svelte.d.ts +14 -0
  11. package/dist/components/detailView/update/updateDetailView.svelte +6 -3
  12. package/dist/components/routes/collections/collections.svelte +44 -23
  13. package/dist/components/routes/data_model/dataModel.svelte +2 -8
  14. package/dist/components/routes/workflows/workflows.svelte +24 -11
  15. package/dist/components/sidebar/sidebar.svelte +12 -5
  16. package/dist/components/sidebar/sidebar.svelte.d.ts +1 -2
  17. package/dist/components/sidebar/sidebarElements.svelte +50 -75
  18. package/dist/components/sidebar/sidebarElements.svelte.d.ts +10 -3
  19. package/dist/utils.js +2 -1
  20. package/package.json +2 -2
  21. package/src/lib/components/dataTable/dataTable.svelte +77 -14
  22. package/src/lib/components/dataTable/header.svelte +88 -24
  23. package/src/lib/components/dataTable/listViewChildren.svelte +106 -0
  24. package/src/lib/components/dataTable/table.svelte +1 -1
  25. package/src/lib/components/detailView/create/createManyView.svelte +2 -2
  26. package/src/lib/components/detailView/update/detailViewChildren.svelte +72 -0
  27. package/src/lib/components/detailView/update/updateDetailView.svelte +6 -3
  28. package/src/lib/components/routes/collections/collections.svelte +44 -23
  29. package/src/lib/components/routes/data_model/dataModel.svelte +2 -8
  30. package/src/lib/components/routes/workflows/workflows.svelte +24 -11
  31. package/src/lib/components/sidebar/sidebar.svelte +12 -5
  32. package/src/lib/components/sidebar/sidebarElements.svelte +50 -75
  33. package/src/lib/utils.ts +2 -1
  34. package/dist/components/dataTable/childRecords.svelte +0 -142
  35. package/dist/components/dataTable/childRecords.svelte.d.ts +0 -9
  36. package/dist/components/detailView/update/children.svelte +0 -96
  37. package/dist/components/detailView/update/children.svelte.d.ts +0 -7
  38. package/src/lib/components/dataTable/childRecords.svelte +0 -142
  39. package/src/lib/components/detailView/update/children.svelte +0 -96
@@ -1,3 +1,17 @@
1
+ <script lang="ts" module>
2
+ export interface ParentContext {
3
+ collectionName: string;
4
+ recordId: string | number;
5
+ }
6
+
7
+ export type RecordOperation =
8
+ | { type: "link"; record: any }
9
+ | { type: "unlink"; id: string | number }
10
+ | { type: "delete"; id: string | number }
11
+ | { type: "create"; record: any }
12
+ | { type: "update"; id: string | number; data: any };
13
+ </script>
14
+
1
15
  <script lang="ts">
2
16
  import _ from "lodash";
3
17
  import { getStudioContext } from "../../context";
@@ -5,9 +19,9 @@
5
19
  import Header from "./header.svelte";
6
20
  import Table, { type TableProps } from "./table.svelte";
7
21
  import { getCollectionColumns, getCollectionParamsFields } from "./utils";
8
- import { Pencil, Trash } from "lucide-svelte";
22
+ import { Pencil, Trash, Unlink } from "lucide-svelte";
9
23
  import * as icons from "lucide-svelte";
10
- import ChildRecords from "./childRecords.svelte";
24
+ import ListViewChildren from "./listViewChildren.svelte";
11
25
  import FieldCell from "./fieldCell.svelte";
12
26
  import Skeleton from "../ui/skeleton/skeleton.svelte";
13
27
  import Button from "../ui/button/button.svelte";
@@ -25,8 +39,12 @@
25
39
  interface Props {
26
40
  collectionName: string;
27
41
  filter?: any;
42
+ searchParams?: Record<string, any>;
43
+ parentContext?: ParentContext;
44
+ onOperation?: (op: RecordOperation) => void;
28
45
  showHeader?: boolean;
29
46
  showFooter?: boolean;
47
+ showImport?: boolean;
30
48
  unifiedBgColor?: "bg-muted/30" | "bg-background";
31
49
  showDelete?: boolean;
32
50
  tableProps?: Partial<TableProps>;
@@ -36,8 +54,12 @@
36
54
  let {
37
55
  collectionName,
38
56
  filter,
57
+ searchParams,
58
+ parentContext,
59
+ onOperation,
39
60
  showHeader = true,
40
61
  showFooter = true,
62
+ showImport = true,
41
63
  unifiedBgColor,
42
64
  showDelete = false,
43
65
  tableProps,
@@ -57,6 +79,7 @@
57
79
  sort: {},
58
80
  limit: "100",
59
81
  page: 1,
82
+ ...searchParams,
60
83
  });
61
84
 
62
85
  $effect(() => {
@@ -73,10 +96,9 @@
73
96
  );
74
97
  let dataTableContainerWidth: number = $state(0);
75
98
  let dataTableWidth: number = $state(0);
76
- const doesCollectionHasChildren = Boolean(
77
- ctx.meta.relations.find(
78
- (relation) => relation.to.collection === collectionName,
79
- ),
99
+ const doesCollectionHasChildren = $derived(
100
+ (ctx.meta.collections[collectionName]?.children ?? [])
101
+ .some((c: any) => c.type === "fk" || c.type === "m2m" || c.type === "polymorphic")
80
102
  );
81
103
 
82
104
  // requests the data from the server when the params is changed
@@ -107,14 +129,44 @@
107
129
  loading = false;
108
130
  }
109
131
 
132
+ // Internal handler: updates data optimistically then calls onOperation
133
+ function applyOperation(op: RecordOperation) {
134
+ if (op.type === "link") {
135
+ data = [...data, op.record];
136
+ } else if (op.type === "unlink" || op.type === "delete") {
137
+ data = data.filter((r: any) => String(r.id) !== String(op.id));
138
+ } else if (op.type === "create") {
139
+ data = [...data, { ...op.record, _pending: true }];
140
+ } else if (op.type === "update") {
141
+ data = data.map((r: any) => String(r.id) === String(op.id) ? { ...r, ...op.data } : r);
142
+ }
143
+ onOperation?.(op);
144
+ }
145
+
110
146
  async function handleDelete(entryId: string) {
111
- const result = await showDialog(
112
- "Are you sure?",
113
- "This will delete the record you selected.",
114
- );
147
+ const result = await showDialog("Are you sure?", "This will permanently delete the record.");
115
148
  if (result) {
116
- await lobb.deleteOne(collectionName, entryId);
117
- params = { ...params };
149
+ if (onOperation) {
150
+ applyOperation({ type: "delete", id: entryId });
151
+ } else if (parentContext) {
152
+ await lobb.updateOne(parentContext.collectionName, String(parentContext.recordId), {}, { [collectionName]: { delete: [entryId] } });
153
+ params = { ...params };
154
+ } else {
155
+ await lobb.deleteOne(collectionName, entryId);
156
+ params = { ...params };
157
+ }
158
+ }
159
+ }
160
+
161
+ async function handleUnlink(entryId: string) {
162
+ const result = await showDialog("Are you sure?", "This will unlink the record without deleting it.");
163
+ if (result) {
164
+ if (onOperation) {
165
+ applyOperation({ type: "unlink", id: entryId });
166
+ } else {
167
+ await lobb.updateOne(parentContext!.collectionName, String(parentContext!.recordId), {}, { [collectionName]: { unlink: [entryId] } });
168
+ params = { ...params };
169
+ }
118
170
  }
119
171
  }
120
172
 
@@ -157,7 +209,7 @@
157
209
  {/snippet}
158
210
 
159
211
  {#if showHeader}
160
- <Header bind:params {collectionName} bind:selectedRecords>
212
+ <Header bind:params {collectionName} bind:selectedRecords {parentContext} {showImport} onOperation={onOperation ? applyOperation : undefined}>
161
213
  {#snippet left()}
162
214
  {@render headerLeft?.()}
163
215
  {/snippet}
@@ -198,6 +250,16 @@
198
250
  params = { ...params };
199
251
  }}
200
252
  ></UpdateDetailViewButton>
253
+ {#if parentContext}
254
+ <Button
255
+ class="h-6 w-6 text-muted-foreground hover:bg-transparent"
256
+ variant="ghost"
257
+ size="icon"
258
+ onclick={() => handleUnlink(entry.id)}
259
+ Icon={Unlink}
260
+ title="Remove from this entry"
261
+ ></Button>
262
+ {/if}
201
263
  {#if showDelete}
202
264
  <Button
203
265
  class="h-6 w-6 text-muted-foreground hover:bg-transparent"
@@ -205,6 +267,7 @@
205
267
  size="icon"
206
268
  onclick={() => handleDelete(entry.id)}
207
269
  Icon={Trash}
270
+ title="Delete permanently"
208
271
  ></Button>
209
272
  {/if}
210
273
  {#await getWorkflowTools($state.snapshot(entry))}
@@ -232,7 +295,7 @@
232
295
  />
233
296
  {/snippet}
234
297
  {#snippet collapsible(entry)}
235
- <ChildRecords
298
+ <ListViewChildren
236
299
  {collectionName}
237
300
  recordId={entry.id}
238
301
  width={dataTableWidth > dataTableContainerWidth
@@ -1,10 +1,35 @@
1
+ export interface ParentContext {
2
+ collectionName: string;
3
+ recordId: string | number;
4
+ }
5
+ export type RecordOperation = {
6
+ type: "link";
7
+ record: any;
8
+ } | {
9
+ type: "unlink";
10
+ id: string | number;
11
+ } | {
12
+ type: "delete";
13
+ id: string | number;
14
+ } | {
15
+ type: "create";
16
+ record: any;
17
+ } | {
18
+ type: "update";
19
+ id: string | number;
20
+ data: any;
21
+ };
1
22
  import { type TableProps } from "./table.svelte";
2
23
  import type { Snippet } from "svelte";
3
24
  interface Props {
4
25
  collectionName: string;
5
26
  filter?: any;
27
+ searchParams?: Record<string, any>;
28
+ parentContext?: ParentContext;
29
+ onOperation?: (op: RecordOperation) => void;
6
30
  showHeader?: boolean;
7
31
  showFooter?: boolean;
32
+ showImport?: boolean;
8
33
  unifiedBgColor?: "bg-muted/30" | "bg-background";
9
34
  showDelete?: boolean;
10
35
  tableProps?: Partial<TableProps>;
@@ -2,7 +2,7 @@
2
2
  import { getStudioContext } from "../../context";
3
3
 
4
4
  const { lobb, ctx } = getStudioContext();
5
- import { Download, ListRestart, Plus, Trash } from "lucide-svelte";
5
+ import { Download, ListRestart, Plus, Trash, Link } from "lucide-svelte";
6
6
  import * as Tooltip from "../ui/tooltip";
7
7
  import LlmButton from "../LlmButton.svelte";
8
8
  import FilterButton from "./filterButton.svelte";
@@ -11,14 +11,19 @@
11
11
  import ImportButton from "../importButton.svelte";
12
12
  import { showDialog } from "../../actions";
13
13
  import CreateDetailViewButton from "../detailView/create/createDetailViewButton.svelte";
14
+ import SelectRecord from "../selectRecord.svelte";
14
15
  import ExtensionsComponents from "../extensionsComponents.svelte";
15
16
  import { getExtensionUtils } from "../../extensions/extensionUtils";
16
17
  import type { Snippet } from "svelte";
18
+ import type { ParentContext, RecordOperation } from "./dataTable.svelte";
17
19
 
18
20
  interface Props {
19
21
  collectionName: string;
20
22
  params: any;
21
23
  selectedRecords: string[];
24
+ parentContext?: ParentContext;
25
+ onOperation?: (op: RecordOperation) => void;
26
+ showImport?: boolean;
22
27
  left?: Snippet<[]>;
23
28
  }
24
29
 
@@ -26,9 +31,42 @@
26
31
  collectionName,
27
32
  params = $bindable(),
28
33
  selectedRecords = $bindable(),
34
+ parentContext,
35
+ onOperation,
36
+ showImport = true,
29
37
  left
30
38
  }: Props = $props();
31
39
 
40
+ async function handleLink(selected: any) {
41
+ if (!parentContext) return;
42
+ if (onOperation) {
43
+ onOperation({ type: "link", record: selected });
44
+ } else {
45
+ await lobb.updateOne(
46
+ parentContext.collectionName,
47
+ String(parentContext.recordId),
48
+ {},
49
+ { [collectionName]: { link: [selected.id] } },
50
+ );
51
+ resetTable();
52
+ }
53
+ }
54
+
55
+ async function handleChildCreate(formData: any) {
56
+ if (!parentContext) return;
57
+ if (onOperation) {
58
+ onOperation({ type: "create", record: formData });
59
+ } else {
60
+ await lobb.updateOne(
61
+ parentContext.collectionName,
62
+ String(parentContext.recordId),
63
+ {},
64
+ { [collectionName]: { create: [formData] } },
65
+ );
66
+ resetTable();
67
+ }
68
+ }
69
+
32
70
  let headerWidth: number = $state(0);
33
71
  let headerIsSmall: boolean = $derived(headerWidth < 560);
34
72
 
@@ -138,34 +176,60 @@
138
176
  >
139
177
  {headerIsSmall ? "" : "Refresh"}
140
178
  </Button>
141
- <Tooltip.Provider delayDuration={0}>
142
- <Tooltip.Root>
143
- <Tooltip.Trigger>
144
- <ImportButton
145
- {collectionName}
146
- variant="outline"
147
- class="h-7 px-2 text-xs font-normal"
148
- Icon={Download}
149
- onSuccessfullSave={() => (params = { ...params })}
150
- />
151
- </Tooltip.Trigger>
152
- <Tooltip.Content>Import</Tooltip.Content>
153
- </Tooltip.Root>
154
- </Tooltip.Provider>
179
+ {#if showImport}
180
+ <Tooltip.Provider delayDuration={0}>
181
+ <Tooltip.Root>
182
+ <Tooltip.Trigger>
183
+ <ImportButton
184
+ {collectionName}
185
+ variant="outline"
186
+ class="h-7 px-2 text-xs font-normal"
187
+ Icon={Download}
188
+ onSuccessfullSave={() => (params = { ...params })}
189
+ />
190
+ </Tooltip.Trigger>
191
+ <Tooltip.Content>Import</Tooltip.Content>
192
+ </Tooltip.Root>
193
+ </Tooltip.Provider>
194
+ {/if}
155
195
  <ExtensionsComponents
156
196
  name="listView.header.actions"
157
197
  utils={getExtensionUtils(lobb, ctx)}
158
198
  {collectionName}
159
199
  refresh={() => { params = { ...params }; }}
160
200
  />
161
- <CreateDetailViewButton
162
- {collectionName}
163
- variant="default"
164
- class="h-7 px-3 text-xs font-normal"
165
- Icon={Plus}
166
- onSuccessfullSave={() => (params = { ...params })}
167
- >
168
- {headerIsSmall ? "" : "Create"}
169
- </CreateDetailViewButton>
201
+ {#if parentContext}
202
+ {#if parentContext}
203
+ <SelectRecord
204
+ {collectionName}
205
+ variant="outline"
206
+ class="h-7 px-3 text-xs font-normal"
207
+ Icon={Link}
208
+ onSelect={handleLink}
209
+ >
210
+ {headerIsSmall ? "" : "Link"}
211
+ </SelectRecord>
212
+ {/if}
213
+ <CreateDetailViewButton
214
+ {collectionName}
215
+ variant="default"
216
+ class="h-7 px-3 text-xs font-normal"
217
+ Icon={Plus}
218
+ rollback={true}
219
+ onSuccessfullSave={handleChildCreate}
220
+ >
221
+ {headerIsSmall ? "" : "Create"}
222
+ </CreateDetailViewButton>
223
+ {:else}
224
+ <CreateDetailViewButton
225
+ {collectionName}
226
+ variant="default"
227
+ class="h-7 px-3 text-xs font-normal"
228
+ Icon={Plus}
229
+ onSuccessfullSave={() => (params = { ...params })}
230
+ >
231
+ {headerIsSmall ? "" : "Create"}
232
+ </CreateDetailViewButton>
233
+ {/if}
170
234
  </div>
171
235
  </div>
@@ -1,8 +1,12 @@
1
1
  import type { Snippet } from "svelte";
2
+ import type { ParentContext, RecordOperation } from "./dataTable.svelte";
2
3
  interface Props {
3
4
  collectionName: string;
4
5
  params: any;
5
6
  selectedRecords: string[];
7
+ parentContext?: ParentContext;
8
+ onOperation?: (op: RecordOperation) => void;
9
+ showImport?: boolean;
6
10
  left?: Snippet<[]>;
7
11
  }
8
12
  declare const Header: import("svelte").Component<Props, {}, "params" | "selectedRecords">;
@@ -0,0 +1,106 @@
1
+ <script lang="ts">
2
+ import { getStudioContext } from "../../context";
3
+ import { ChevronRight, Table, Plus, Link, ArrowLeftRight, GitFork } from "lucide-svelte";
4
+ import DataTable from "./dataTable.svelte";
5
+ import CreateDetailViewButton from "../detailView/create/createDetailViewButton.svelte";
6
+ import ExtensionsComponents from "../extensionsComponents.svelte";
7
+ import { getExtensionUtils } from "../../extensions/extensionUtils";
8
+
9
+ const { ctx, lobb } = getStudioContext();
10
+
11
+ interface Props {
12
+ collectionName: string;
13
+ recordId: string;
14
+ width: number;
15
+ unifiedBgColor?: "bg-muted/30" | "bg-background";
16
+ }
17
+
18
+ let { collectionName, recordId, width, unifiedBgColor }: Props = $props();
19
+
20
+ const children = (ctx.meta.collections[collectionName]?.children ?? [])
21
+ .filter((c: any) => c.type === "fk" || c.type === "m2m" || c.type === "polymorphic");
22
+
23
+ let expandedRows: boolean[] = $state(new Array(children.length).fill(false));
24
+ let refreshDataTable = $state(true);
25
+ let tableHeaderWidth = $state(0);
26
+ </script>
27
+
28
+ <div class="flex" style="width: {width}px;">
29
+ <div
30
+ class="flex justify-center border-r {unifiedBgColor ? unifiedBgColor : 'bg-background'}"
31
+ style="width: 40px"
32
+ ></div>
33
+ <div class="flex-1 flex flex-col">
34
+ {#each children as child, index}
35
+ {@const lastRow = children.length - 1 === index}
36
+ <div class="overflow-hidden {unifiedBgColor ? unifiedBgColor : 'bg-background'}">
37
+ <div
38
+ bind:clientWidth={tableHeaderWidth}
39
+ class="flex justify-between items-center gap-2 text-sm h-10 {expandedRows[index] || !lastRow ? 'border-b' : ''}"
40
+ >
41
+ <button
42
+ class="flex gap-2 px-2 flex-1 h-full items-center"
43
+ onclick={() => { expandedRows[index] = !expandedRows[index]; }}
44
+ >
45
+ <ChevronRight
46
+ size="17.5"
47
+ class="text-muted-foreground transition-transform"
48
+ style={expandedRows[index] ? "transform: rotate(90deg);" : "transform: rotate(0deg);"}
49
+ />
50
+ <Table size="17.5" class="text-muted-foreground" />
51
+ <div class="text-muted-foreground">{child.collection}</div>
52
+ {#if child.type === "fk"}
53
+ <span title="Direct (FK)"><Link size="13" class="text-muted-foreground/50" /></span>
54
+ {:else if child.type === "m2m"}
55
+ <span title="Many to Many"><ArrowLeftRight size="13" class="text-muted-foreground/50" /></span>
56
+ {:else if child.type === "polymorphic"}
57
+ <span title="Polymorphic"><GitFork size="13" class="text-muted-foreground/50" /></span>
58
+ {/if}
59
+ </button>
60
+ {#if child.type === "fk"}
61
+ <div class="flex items-center px-2">
62
+ <CreateDetailViewButton
63
+ collectionName={child.collection}
64
+ variant="ghost"
65
+ class="h-7 px-3 text-xs font-normal"
66
+ Icon={Plus}
67
+ values={{ [child.field]: { id: recordId } }}
68
+ onSuccessfullSave={async () => { refreshDataTable = !refreshDataTable; }}
69
+ >
70
+ Create
71
+ </CreateDetailViewButton>
72
+ </div>
73
+ {/if}
74
+ </div>
75
+ {#if expandedRows[index]}
76
+ <div class="flex max-h-96 overflow-auto {lastRow ? '' : 'border-b'}">
77
+ <div
78
+ class="border-r {unifiedBgColor ? unifiedBgColor : ''}"
79
+ style="width: 100vw; max-width: 40px"
80
+ ></div>
81
+ <div class="flex-1" style="width: {tableHeaderWidth - 40}px;">
82
+ {#key refreshDataTable}
83
+ <ExtensionsComponents
84
+ name="listView.entry.children.{child.collection}"
85
+ collectionName={child.collection}
86
+ searchParams={{ children_of: collectionName, parent_id: recordId }}
87
+ utils={getExtensionUtils(lobb, ctx)}
88
+ >
89
+ <DataTable
90
+ collectionName={child.collection}
91
+ searchParams={{ children_of: collectionName, parent_id: recordId }}
92
+ showHeader={false}
93
+ showFooter={false}
94
+ showDelete={child.type === "fk"}
95
+ {unifiedBgColor}
96
+ tableProps={{ showLastRowBorder: false, showLastColumnBorder: false, showCheckboxes: false }}
97
+ />
98
+ </ExtensionsComponents>
99
+ {/key}
100
+ </div>
101
+ </div>
102
+ {/if}
103
+ </div>
104
+ {/each}
105
+ </div>
106
+ </div>
@@ -0,0 +1,9 @@
1
+ interface Props {
2
+ collectionName: string;
3
+ recordId: string;
4
+ width: number;
5
+ unifiedBgColor?: "bg-muted/30" | "bg-background";
6
+ }
7
+ declare const ListViewChildren: import("svelte").Component<Props, {}, "">;
8
+ type ListViewChildren = ReturnType<typeof ListViewChildren>;
9
+ export default ListViewChildren;
@@ -172,7 +172,7 @@
172
172
  <div
173
173
  style="
174
174
  display: grid;
175
- grid-template-columns: minmax(auto, 7.5rem) repeat({columnsLength - 1}, minmax(auto, 15rem)){rowActionsExists ? ' minmax(auto, 7.5rem)' : ''};
175
+ grid-template-columns: minmax(auto, 10rem) repeat({columnsLength - 1}, minmax(auto, 15rem)){rowActionsExists ? ' minmax(auto, 7.5rem)' : ''};
176
176
  grid-template-rows: 2.5rem;
177
177
  "
178
178
  >
@@ -15,7 +15,7 @@
15
15
  import SelectRecord from "../../../components/selectRecord.svelte";
16
16
  import FieldCell from "../../../components/dataTable/fieldCell.svelte";
17
17
  import SubRecords from "./subRecords.svelte";
18
- import ChildRecords from "../../../components/dataTable/childRecords.svelte";
18
+ import ListViewChildren from "../../../components/dataTable/listViewChildren.svelte";
19
19
  import { getStudioContext } from "../../../context";
20
20
 
21
21
  const { ctx } = getStudioContext();
@@ -232,7 +232,7 @@
232
232
  {/snippet}
233
233
  {#snippet collapsible(entry, index)}
234
234
  {#if entry.id}
235
- <ChildRecords
235
+ <ListViewChildren
236
236
  {collectionName}
237
237
  recordId={entry.id}
238
238
  width={tableWidth}
@@ -0,0 +1,72 @@
1
+ <script lang="ts">
2
+ import DataTable, { type RecordOperation } from "../../../components/dataTable/dataTable.svelte";
3
+ import { getStudioContext } from "../../../context";
4
+ import { Table, Link } from "lucide-svelte";
5
+
6
+ const { ctx } = getStudioContext();
7
+
8
+ type PendingOps = {
9
+ link?: (string | number)[];
10
+ unlink?: (string | number)[];
11
+ delete?: (string | number)[];
12
+ create?: any[];
13
+ };
14
+
15
+ interface LocalProp {
16
+ collectionName: string;
17
+ entry: any;
18
+ pendingChildren?: Record<string, PendingOps>;
19
+ }
20
+
21
+ let { collectionName, entry, pendingChildren = $bindable({}) }: LocalProp = $props();
22
+
23
+ function makeHandler(collection: string) {
24
+ return (op: RecordOperation) => {
25
+ if (!pendingChildren[collection]) pendingChildren[collection] = {};
26
+ const c = pendingChildren[collection];
27
+ if (op.type === "link") {
28
+ (c.link ??= []).push(op.record.id);
29
+ } else if (op.type === "unlink") {
30
+ (c.unlink ??= []).push(op.id);
31
+ } else if (op.type === "delete") {
32
+ (c.delete ??= []).push(op.id);
33
+ } else if (op.type === "create") {
34
+ (c.create ??= []).push(op.record);
35
+ }
36
+ };
37
+ }
38
+
39
+ const children = (ctx.meta.collections[collectionName]?.children ?? [])
40
+ .filter((c: any) => c.type === "fk" || c.type === "m2m" || c.type === "polymorphic");
41
+ </script>
42
+
43
+ {#if children.length}
44
+ <div class="flex flex-col gap-3 border-t p-4">
45
+ <div class="flex items-center gap-2">
46
+ <Link size="14" class="text-muted-foreground" />
47
+ <span class="text-sm font-medium">Sub Records</span>
48
+ </div>
49
+ {#each children as child}
50
+ <div class="rounded-lg border bg-background overflow-hidden flex flex-col max-h-96">
51
+ <DataTable
52
+ collectionName={child.collection}
53
+ searchParams={{ children_of: collectionName, parent_id: entry.id }}
54
+ parentContext={{ collectionName, recordId: entry.id }}
55
+ onOperation={makeHandler(child.collection)}
56
+ showImport={false}
57
+ showHeader={true}
58
+ showFooter={true}
59
+ showDelete={child.type === "fk" || child.type === "m2m"}
60
+ tableProps={{ showLastColumnBorder: false, showLastRowBorder: true }}
61
+ >
62
+ {#snippet headerLeft()}
63
+ <div class="flex items-center gap-2 px-1">
64
+ <Table size="14" class="text-muted-foreground" />
65
+ <span class="text-sm font-medium">{child.collection}</span>
66
+ </div>
67
+ {/snippet}
68
+ </DataTable>
69
+ </div>
70
+ {/each}
71
+ </div>
72
+ {/if}
@@ -0,0 +1,14 @@
1
+ type PendingOps = {
2
+ link?: (string | number)[];
3
+ unlink?: (string | number)[];
4
+ delete?: (string | number)[];
5
+ create?: any[];
6
+ };
7
+ interface LocalProp {
8
+ collectionName: string;
9
+ entry: any;
10
+ pendingChildren?: Record<string, PendingOps>;
11
+ }
12
+ declare const DetailViewChildren: import("svelte").Component<LocalProp, {}, "pendingChildren">;
13
+ type DetailViewChildren = ReturnType<typeof DetailViewChildren>;
14
+ export default DetailViewChildren;
@@ -29,7 +29,7 @@
29
29
  const { lobb, ctx } = getStudioContext();
30
30
  import { calculateDrawerWidth, getChangedProperties } from "../../../utils";
31
31
  import { getField, getFieldIcon } from "../../dataTable/utils";
32
- import Children from "../update/children.svelte";
32
+ import DetailViewChildren from "../update/detailViewChildren.svelte";
33
33
  import type { Snippet } from "svelte";
34
34
  import { getDefaultEntry, parseDetailViewValues, serializeEntry } from "../utils";
35
35
  import FieldInput from "../fieldInput.svelte";
@@ -57,15 +57,18 @@
57
57
  getChangedProperties(initialEntry, $state.snapshot(entry)),
58
58
  );
59
59
  let fieldsErrors: Record<string, any> = $state({});
60
+ let pendingChildren = $state<Record<string, any>>({});
60
61
 
61
62
  async function handleSave() {
62
63
  delete localEntry.id;
63
64
  localEntry = serializeEntry(ctx, collectionName, localEntry);
64
65
 
66
+ const children = Object.keys(pendingChildren).length ? pendingChildren : undefined;
65
67
  const response = await lobb.updateOne(
66
68
  collectionName,
67
69
  recordId,
68
70
  localEntry,
71
+ children,
69
72
  );
70
73
 
71
74
  if (!response.bodyUsed) {
@@ -145,7 +148,7 @@
145
148
  {/each}
146
149
  </div>
147
150
  {#if showRelatedRecords}
148
- <Children {collectionName} {entry} />
151
+ <DetailViewChildren {collectionName} {entry} bind:pendingChildren />
149
152
  {/if}
150
153
  </div>
151
154
  <div class="flex h-12 items-center justify-end gap-2 border-t px-4">
@@ -163,7 +166,7 @@
163
166
  class="h-7 px-3 text-xs font-normal"
164
167
  Icon={submitButton?.icon ? submitButton.icon : Pencil}
165
168
  onclick={handleSave}
166
- disabled={!Object.keys(localEntry).length}
169
+ disabled={!Object.keys(localEntry).length && !Object.keys(pendingChildren).length}
167
170
  >
168
171
  {submitButton?.text ? submitButton.text : "Update"}
169
172
  </Button>