@lobb-js/studio 0.42.0 → 0.43.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 (32) hide show
  1. package/dist/components/confirmationDialog/confirmationDialog.svelte +1 -1
  2. package/dist/components/dataTable/dataTable.svelte +42 -16
  3. package/dist/components/dataTable/listViewChildren.svelte +60 -77
  4. package/dist/components/dataTable/listViewChildren.svelte.d.ts +1 -1
  5. package/dist/components/dataTable/table.svelte +8 -56
  6. package/dist/components/dataTable/table.svelte.d.ts +1 -2
  7. package/dist/components/detailView/changeTreeUtils.d.ts +7 -0
  8. package/dist/components/detailView/changeTreeUtils.js +47 -0
  9. package/dist/components/detailView/create/createDetailView.svelte +17 -13
  10. package/dist/components/detailView/detailView.svelte +7 -2
  11. package/dist/components/detailView/detailView.svelte.d.ts +1 -0
  12. package/dist/components/detailView/fieldInput.svelte +10 -9
  13. package/dist/components/detailView/fieldInput.svelte.d.ts +1 -0
  14. package/dist/components/detailView/update/updateDetailView.svelte +22 -18
  15. package/dist/components/drawer.svelte +15 -2
  16. package/dist/components/foreingKeyInput.svelte +163 -68
  17. package/dist/components/foreingKeyInput.svelte.d.ts +1 -1
  18. package/dist/components/polymorphicInput.svelte +112 -63
  19. package/dist/components/polymorphicInput.svelte.d.ts +1 -0
  20. package/package.json +2 -2
  21. package/src/lib/components/confirmationDialog/confirmationDialog.svelte +1 -1
  22. package/src/lib/components/dataTable/dataTable.svelte +42 -16
  23. package/src/lib/components/dataTable/listViewChildren.svelte +60 -77
  24. package/src/lib/components/dataTable/table.svelte +8 -56
  25. package/src/lib/components/detailView/changeTreeUtils.ts +39 -0
  26. package/src/lib/components/detailView/create/createDetailView.svelte +17 -13
  27. package/src/lib/components/detailView/detailView.svelte +7 -2
  28. package/src/lib/components/detailView/fieldInput.svelte +10 -9
  29. package/src/lib/components/detailView/update/updateDetailView.svelte +22 -18
  30. package/src/lib/components/drawer.svelte +15 -2
  31. package/src/lib/components/foreingKeyInput.svelte +163 -68
  32. package/src/lib/components/polymorphicInput.svelte +112 -63
@@ -17,7 +17,7 @@
17
17
  <AlertDialog.Content>
18
18
  <AlertDialog.Header>
19
19
  <AlertDialog.Title>{title}</AlertDialog.Title>
20
- <AlertDialog.Description class="whitespace-pre-line">
20
+ <AlertDialog.Description class="whitespace-pre-wrap font-mono text-xs">
21
21
  {description}
22
22
  </AlertDialog.Description>
23
23
  </AlertDialog.Header>
@@ -14,8 +14,10 @@
14
14
  import Table, { type TableProps } from "./table.svelte";
15
15
  import { getCollectionColumns, getCollectionParamsFields } from "./utils";
16
16
  import CanAccess from "../canAccess.svelte";
17
- import { Pencil, Trash, Unlink, RotateCcw } from "lucide-svelte";
17
+ import { Pencil, Trash, Unlink, RotateCcw, Network } from "lucide-svelte";
18
18
  import ListViewChildren from "./listViewChildren.svelte";
19
+ import Drawer from "../drawer.svelte";
20
+ import { ArrowLeft } from "lucide-svelte";
19
21
  import FieldCell from "./fieldCell.svelte";
20
22
  import Skeleton from "../ui/skeleton/skeleton.svelte";
21
23
  import Button from "../ui/button/button.svelte";
@@ -135,17 +137,17 @@
135
137
  const state = entry._recordingState;
136
138
  const border = cellIndex === 0 ? {
137
139
  deleted: 'border-l-2 border-l-red-500',
138
- unlinked: 'border-l-2 border-l-orange-500',
140
+ unlinked: 'border-l-2 border-l-slate-500',
139
141
  created: 'border-l-2 border-l-green-500',
140
142
  linked: 'border-l-2 border-l-blue-500',
141
- updated: 'border-l-2 border-l-amber-500',
143
+ updated: 'border-l-2 border-l-orange-500',
142
144
  }[state as string] ?? '' : '';
143
145
  const bg: Record<string, string> = {
144
- deleted: '!bg-red-500/5 opacity-50',
145
- unlinked: '!bg-orange-500/5 opacity-50',
146
+ deleted: '!bg-red-500/5',
147
+ unlinked: '!bg-slate-500/5',
146
148
  created: '!bg-green-500/5',
147
149
  linked: '!bg-blue-500/5',
148
- updated: '!bg-amber-500/5',
150
+ updated: '!bg-orange-500/5',
149
151
  };
150
152
  return `${bg[state as string] ?? ''} ${border}`.trim();
151
153
  }
@@ -220,6 +222,7 @@
220
222
  (ctx.meta.collections[collectionName]?.children ?? [])
221
223
  .some((c: any) => c.type === "fk" || c.type === "m2m" || c.type === "polymorphic")
222
224
  );
225
+ let childrenDrawerEntry = $state<Record<string, any> | null>(null);
223
226
 
224
227
  // requests the data from the server when the params is changed
225
228
  $effect(() => {
@@ -453,7 +456,6 @@
453
456
  <Table
454
457
  {data}
455
458
  {columns}
456
- showCollapsible={doesCollectionHasChildren}
457
459
  selectByColumn="id"
458
460
  showLastRowBorder={true}
459
461
  showLastColumnBorder={true}
@@ -463,6 +465,19 @@
463
465
  {...tableProps}
464
466
  rowActions={hasRowActions ? rowActionsSnippet : undefined}
465
467
  onCellClass={isRecordingMode ? onCellClass : undefined}>
468
+ {#snippet preTools(entry)}
469
+ {#if doesCollectionHasChildren}
470
+ <Button
471
+ class="h-6 w-6 text-muted-foreground hover:bg-transparent"
472
+ variant="ghost"
473
+ size="icon"
474
+ onclick={() => { childrenDrawerEntry = entry; }}
475
+ Icon={Network}
476
+ title="Show children"
477
+ aria-label={`Show children of record ${entry.id}`}
478
+ ></Button>
479
+ {/if}
480
+ {/snippet}
466
481
  {#snippet tools(entry)}
467
482
  {#if entry._recordingState !== 'deleted' && entry._recordingState !== 'unlinked'}
468
483
  {#if showEdit}
@@ -524,15 +539,6 @@
524
539
  refresh={() => { params = { ...params }; }}
525
540
  />
526
541
  {/snippet}
527
- {#snippet collapsible(entry)}
528
- <ListViewChildren
529
- {collectionName}
530
- recordId={entry.id}
531
- width={dataTableWidth > dataTableContainerWidth
532
- ? dataTableContainerWidth
533
- : dataTableWidth}
534
- />
535
- {/snippet}
536
542
  </Table>
537
543
  {/if}
538
544
  </div>
@@ -546,3 +552,23 @@
546
552
  />
547
553
  {/if}
548
554
  </div>
555
+
556
+ {#if childrenDrawerEntry}
557
+ <Drawer position="bottom" onHide={async () => { childrenDrawerEntry = null; }}>
558
+ <div class="flex h-12 items-center gap-4 border-b px-4 shrink-0">
559
+ <Button
560
+ variant="outline"
561
+ onclick={() => { childrenDrawerEntry = null; }}
562
+ class="h-8 w-8 rounded-full text-xs font-normal"
563
+ Icon={ArrowLeft}
564
+ ></Button>
565
+ <div class="flex items-center gap-2 text-sm">
566
+ <span>Children of</span>
567
+ <span class="rounded-md border bg-muted px-2 py-0.5">{collectionName} #{childrenDrawerEntry.id}</span>
568
+ </div>
569
+ </div>
570
+ <div class="flex-1 overflow-y-auto">
571
+ <ListViewChildren {collectionName} recordId={String(childrenDrawerEntry.id)} />
572
+ </div>
573
+ </Drawer>
574
+ {/if}
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { getStudioContext } from "../../context";
3
- import { ChevronRight, Table, Plus, Link, ArrowLeftRight, GitFork } from "lucide-svelte";
3
+ import { Table, Plus } from "lucide-svelte";
4
4
  import DataTable from "./dataTable.svelte";
5
5
  import CreateDetailViewButton from "../detailView/create/createDetailViewButton.svelte";
6
6
  import ExtensionsComponents from "../extensionsComponents.svelte";
@@ -11,93 +11,76 @@
11
11
  interface Props {
12
12
  collectionName: string;
13
13
  recordId: string;
14
- width: number;
14
+ width?: number;
15
15
  }
16
16
 
17
- let { collectionName, recordId, width }: Props = $props();
17
+ let { collectionName, recordId, width = 0 }: Props = $props();
18
18
 
19
19
  const children = (ctx.meta.collections[collectionName]?.children ?? [])
20
20
  .filter((c: any) => c.type === "fk" || c.type === "m2m" || c.type === "polymorphic");
21
21
 
22
- let expandedRows: boolean[] = $state(new Array(children.length).fill(false));
23
- let refreshDataTable = $state(true);
24
- let tableHeaderWidth = $state(0);
22
+ let activeTab = $state(children[0]?.collection ?? '');
23
+ let refreshKey = $state(0);
24
+ let counts = $state<Record<string, number>>({});
25
+
26
+ const activeChild = $derived(children.find((c: any) => c.collection === activeTab));
25
27
  </script>
26
28
 
27
- <div class="flex" style="width: {width}px;">
28
- <div
29
- class="flex justify-center border-r bg-background"
30
- style="width: 40px"
31
- ></div>
32
- <div class="flex-1 flex flex-col">
33
- {#each children as child, index}
34
- {@const lastRow = children.length - 1 === index}
35
- <div class="overflow-hidden bg-background">
36
- <div
37
- bind:clientWidth={tableHeaderWidth}
38
- class="flex justify-between items-center gap-2 text-sm h-10 {expandedRows[index] || !lastRow ? 'border-b' : ''}"
29
+ <div class="flex flex-col h-full overflow-hidden">
30
+ <!-- Tab bar -->
31
+ <div class="flex border-b shrink-0 overflow-x-auto">
32
+ {#each children as child}
33
+ <button
34
+ class="flex items-center gap-1.5 px-4 h-10 text-xs font-medium whitespace-nowrap border-b-2 transition-colors
35
+ {activeTab === child.collection
36
+ ? 'border-foreground text-foreground'
37
+ : 'border-transparent text-muted-foreground hover:text-foreground'}"
38
+ onclick={() => { activeTab = child.collection; }}
39
+ >
40
+ <Table size="11" class="opacity-50" />
41
+ {child.collection}
42
+ {#if counts[child.collection] !== undefined}
43
+ <span class="rounded-full bg-muted px-1.5 py-0.5 text-[0.65rem] text-muted-foreground">{counts[child.collection]}</span>
44
+ {/if}
45
+ </button>
46
+ {/each}
47
+ <!-- Create button for FK children -->
48
+ {#if activeChild?.type === "fk"}
49
+ <div class="ml-auto flex items-center px-2">
50
+ <CreateDetailViewButton
51
+ collectionName={activeChild.collection}
52
+ variant="ghost"
53
+ size="sm"
54
+ Icon={Plus}
55
+ values={{ [activeChild.field]: recordId }}
56
+ onSuccessfullSave={async () => { refreshKey++; }}
39
57
  >
40
- <button
41
- class="flex gap-2 px-2 flex-1 h-full items-center"
42
- onclick={() => { expandedRows[index] = !expandedRows[index]; }}
58
+ Create
59
+ </CreateDetailViewButton>
60
+ </div>
61
+ {/if}
62
+ </div>
63
+
64
+ <!-- Tab content — render all but hide inactive so counts load for all tabs -->
65
+ <div class="flex-1 overflow-hidden relative">
66
+ {#each children as child}
67
+ <div class="absolute inset-0 {activeTab === child.collection ? '' : 'invisible pointer-events-none'}">
68
+ {#key refreshKey}
69
+ <ExtensionsComponents
70
+ name="listView.entry.children.{child.collection}"
71
+ collectionName={child.collection}
72
+ searchParams={{ children_of: collectionName, parent_id: recordId }}
73
+ utils={getExtensionUtils(lobb, ctx)}
43
74
  >
44
- <ChevronRight
45
- size="17.5"
46
- class="text-muted-foreground transition-transform"
47
- style={expandedRows[index] ? "transform: rotate(90deg);" : "transform: rotate(0deg);"}
75
+ <DataTable
76
+ collectionName={child.collection}
77
+ searchParams={{ children_of: collectionName, parent_id: recordId }}
78
+ showDelete={child.type === "fk" || child.type === "m2m"}
79
+ tableProps={{ showLastRowBorder: true, showLastColumnBorder: true }}
80
+ onDataLoad={(total) => { counts[child.collection] = total; }}
48
81
  />
49
- <Table size="17.5" class="text-muted-foreground" />
50
- <div class="text-muted-foreground">{child.collection}</div>
51
- {#if child.type === "fk"}
52
- <span title="Direct (FK)"><Link size="13" class="text-muted-foreground/50" /></span>
53
- {:else if child.type === "m2m"}
54
- <span title="Many to Many"><ArrowLeftRight size="13" class="text-muted-foreground/50" /></span>
55
- {:else if child.type === "polymorphic"}
56
- <span title="Polymorphic"><GitFork size="13" class="text-muted-foreground/50" /></span>
57
- {/if}
58
- </button>
59
- {#if child.type === "fk"}
60
- <div class="flex items-center px-2">
61
- <CreateDetailViewButton
62
- collectionName={child.collection}
63
- variant="ghost"
64
- size="sm"
65
- Icon={Plus}
66
- values={{ [child.field]: recordId }}
67
- onSuccessfullSave={async () => { refreshDataTable = !refreshDataTable; }}
68
- >
69
- Create
70
- </CreateDetailViewButton>
71
- </div>
72
- {/if}
73
- </div>
74
- {#if expandedRows[index]}
75
- <div class="flex max-h-96 overflow-auto {lastRow ? '' : 'border-b'}">
76
- <div
77
- class="border-r"
78
- style="width: 100vw; max-width: 40px"
79
- ></div>
80
- <div class="flex-1" style="width: {tableHeaderWidth - 40}px;">
81
- {#key refreshDataTable}
82
- <ExtensionsComponents
83
- name="listView.entry.children.{child.collection}"
84
- collectionName={child.collection}
85
- searchParams={{ children_of: collectionName, parent_id: recordId }}
86
- utils={getExtensionUtils(lobb, ctx)}
87
- >
88
- <DataTable
89
- collectionName={child.collection}
90
- searchParams={{ children_of: collectionName, parent_id: recordId }}
91
- showHeader={false}
92
- showFooter={false}
93
- showDelete={child.type === "fk"}
94
- tableProps={{ showLastRowBorder: false, showLastColumnBorder: false, showCheckboxes: false }}
95
- />
96
- </ExtensionsComponents>
97
- {/key}
98
- </div>
99
- </div>
100
- {/if}
82
+ </ExtensionsComponents>
83
+ {/key}
101
84
  </div>
102
85
  {/each}
103
86
  </div>
@@ -1,7 +1,7 @@
1
1
  interface Props {
2
2
  collectionName: string;
3
3
  recordId: string;
4
- width: number;
4
+ width?: number;
5
5
  }
6
6
  declare const ListViewChildren: import("svelte").Component<Props, {}, "">;
7
7
  type ListViewChildren = ReturnType<typeof ListViewChildren>;
@@ -15,8 +15,6 @@
15
15
  export interface TableProps {
16
16
  data: Entry[];
17
17
  columns?: Column[];
18
- showCollapsible?: boolean;
19
-
20
18
  // sorting
21
19
  sort?: Record<string, "asc" | "desc">;
22
20
  localSorting?: boolean;
@@ -33,10 +31,9 @@
33
31
 
34
32
  // snippets
35
33
  overrideCell?: Snippet<[any, Column, Entry]>;
34
+ preTools?: Snippet<[Entry, number]>;
36
35
  tools?: Snippet<[Entry, number]>;
37
36
  rowActions?: Snippet<[Entry, number]>;
38
- collapsible?: Snippet<[Entry, number]>;
39
-
40
37
  // other
41
38
  parentWidth?: number;
42
39
  select?: Select;
@@ -51,7 +48,6 @@
51
48
  import {
52
49
  ArrowDownNarrowWide,
53
50
  ArrowUpWideNarrow,
54
- ChevronRight,
55
51
  CircleOff,
56
52
  } from "lucide-svelte";
57
53
  import Checkbox from "../ui/checkbox/checkbox.svelte";
@@ -66,7 +62,6 @@
66
62
  id: key,
67
63
  };
68
64
  }),
69
- showCollapsible = false,
70
65
  sort = $bindable({}),
71
66
  localSorting = false,
72
67
  selectedRecords = $bindable(),
@@ -77,18 +72,16 @@
77
72
  headerBorderTop = false,
78
73
  parentWidth,
79
74
  overrideCell,
75
+ preTools,
80
76
  tools,
81
77
  rowActions,
82
- collapsible,
83
78
  select,
84
79
  tableWidth = $bindable(),
85
80
  onCellClass,
86
81
  }: TableProps = $props();
87
82
 
88
- let expandedRows: boolean[] = $state(new Array(data.length).fill(false));
89
-
90
83
  // calculate columns count
91
- const toolsExists = selectedRecords || tools ? 1 : 0;
84
+ const toolsExists = selectedRecords || tools || preTools ? 1 : 0;
92
85
  const rowActionsExists = $derived(rowActions ? 1 : 0);
93
86
  const columnsLength = columns.length + toolsExists;
94
87
 
@@ -179,7 +172,7 @@
179
172
  grid-template-rows: 2.5rem;
180
173
  "
181
174
  >
182
- {#if selectedRecords || tools}
175
+ {#if selectedRecords || tools || preTools}
183
176
  <div
184
177
  bind:clientWidth={columnsWidths[0]}
185
178
  class="
@@ -190,10 +183,6 @@
190
183
  bg-muted/50
191
184
  "
192
185
  >
193
- <!-- collapsable toggle -->
194
- {#if showCollapsible}
195
- <div class="w-[20px]"></div>
196
- {/if}
197
186
  {#if selectedRecords && showCheckboxes}
198
187
  <Checkbox
199
188
  class="border-muted-foreground hover:border-foreground"
@@ -249,25 +238,12 @@
249
238
  {#each data as entry, index}
250
239
  {@const isDisabled = Boolean(entry.__disabled)}
251
240
  {@const lastRow = data.length - 1 === index}
252
- {#if selectedRecords || tools}
241
+ {#if selectedRecords || tools || preTools}
253
242
  <div
254
243
  class="sticky left-0 flex items-center p-2.5 text-xs h-10 bg-card border-r gap-2 {onCellClass?.(entry, 0) ?? ''}"
255
244
  >
256
- <!-- collapsable toggle -->
257
- {#if showCollapsible}
258
- <Button
259
- variant="ghost"
260
- class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent transition-transform"
261
- style={expandedRows[index]
262
- ? "transform: rotate(90deg);"
263
- : "transform: rotate(0deg);"}
264
- Icon={ChevronRight}
265
- onclick={() => {
266
- expandedRows[index] = !expandedRows[index];
267
- expandedRows = [...expandedRows];
268
- }}
269
- disabled={isDisabled}
270
- ></Button>
245
+ {#if preTools && !isDisabled}
246
+ {@render preTools(entry, index)}
271
247
  {/if}
272
248
  {#if selectedRecords && showCheckboxes}
273
249
  <Checkbox
@@ -309,31 +285,7 @@
309
285
  {@render rowActions?.(entry, index)}
310
286
  </div>
311
287
  {/if}
312
- <!-- nested data -->
313
- <div
314
- style="grid-column: span {columnsLength + rowActionsExists};"
315
- class="
316
- {!showLastColumnBorder ? '' : 'border-r'}
317
- {lastRow && !showLastRowBorder ? '' : 'border-b'}
318
- "
319
- >
320
- <div
321
- style="
322
- {parentWidth ? `width: ${parentWidth}px` : ''};
323
- max-width: 100vw;
324
- {expandedRows[index] ? '' : 'height: 0px;'}
325
- "
326
- class="
327
- sticky left-0 top-0 overflow-auto bg-muted
328
-
329
- {expandedRows[index] ? 'border-t' : ''}
330
- "
331
- >
332
- {#if collapsible && expandedRows[index]}
333
- {@render collapsible(entry, index)}
334
- {/if}
335
- </div>
336
- </div>
288
+ <div style="grid-column: span {columnsLength + rowActionsExists};" class="{!showLastColumnBorder ? '' : 'border-r'} {lastRow && !showLastRowBorder ? '' : 'border-b'}"></div>
337
289
  {/each}
338
290
  {/if}
339
291
  </div>
@@ -11,7 +11,6 @@ interface Select {
11
11
  export interface TableProps {
12
12
  data: Entry[];
13
13
  columns?: Column[];
14
- showCollapsible?: boolean;
15
14
  sort?: Record<string, "asc" | "desc">;
16
15
  localSorting?: boolean;
17
16
  selectedRecords?: Array<any>;
@@ -21,9 +20,9 @@ export interface TableProps {
21
20
  showLastColumnBorder?: boolean;
22
21
  headerBorderTop?: boolean;
23
22
  overrideCell?: Snippet<[any, Column, Entry]>;
23
+ preTools?: Snippet<[Entry, number]>;
24
24
  tools?: Snippet<[Entry, number]>;
25
25
  rowActions?: Snippet<[Entry, number]>;
26
- collapsible?: Snippet<[Entry, number]>;
27
26
  parentWidth?: number;
28
27
  select?: Select;
29
28
  tableWidth?: number;
@@ -0,0 +1,7 @@
1
+ import type { Changes } from "./utils";
2
+ export interface TreeNode {
3
+ label: string;
4
+ children: TreeNode[];
5
+ }
6
+ export declare function buildChangeTree(c: Changes, fieldLabel?: string): TreeNode[];
7
+ export declare function renderTree(nodes: TreeNode[], prefix?: string): string[];
@@ -0,0 +1,47 @@
1
+ export function buildChangeTree(c, fieldLabel = 'changed') {
2
+ const nodes = [];
3
+ let fieldCount = 0;
4
+ for (const [fieldName, val] of Object.entries(c.data ?? {})) {
5
+ if (val && typeof val === 'object' && !Array.isArray(val)) {
6
+ const v = val;
7
+ const t = v.collection ?? fieldName;
8
+ if (v.delete === true)
9
+ nodes.push({ label: `${t}: inline delete`, children: [] });
10
+ else if (v.update)
11
+ nodes.push({ label: `${t}: inline edit`, children: [] });
12
+ else if (v.create)
13
+ nodes.push({ label: `${t}: inline create`, children: [] });
14
+ }
15
+ else {
16
+ fieldCount++;
17
+ }
18
+ }
19
+ if (fieldCount > 0)
20
+ nodes.unshift({ label: `${fieldCount} field${fieldCount > 1 ? 's' : ''} ${fieldLabel}`, children: [] });
21
+ for (const [col, ch] of Object.entries(c.children ?? {})) {
22
+ const kids = [];
23
+ for (let i = 0; i < ch.created.length; i++)
24
+ kids.push({ label: 'new record', children: [] });
25
+ for (const r of ch.linked)
26
+ kids.push({ label: `#${r.id} linked`, children: [] });
27
+ for (const u of ch.updated)
28
+ kids.push({ label: `#${u.id}`, children: buildChangeTree(u.changes) });
29
+ for (const r of ch.deleted)
30
+ kids.push({ label: `#${r.id} deleted`, children: [] });
31
+ for (const r of ch.unlinked)
32
+ kids.push({ label: `#${r.id} unlinked`, children: [] });
33
+ if (kids.length)
34
+ nodes.push({ label: col, children: kids });
35
+ }
36
+ return nodes;
37
+ }
38
+ export function renderTree(nodes, prefix = '') {
39
+ const lines = [];
40
+ nodes.forEach((node, i) => {
41
+ const last = i === nodes.length - 1;
42
+ lines.push(`${prefix}${last ? '└── ' : '├── '}${node.label}`);
43
+ if (node.children.length)
44
+ lines.push(...renderTree(node.children, prefix + (last ? ' ' : '│ ')));
45
+ });
46
+ return lines;
47
+ }
@@ -61,19 +61,15 @@
61
61
  const hasChildChanges = $derived(
62
62
  Object.values(changes.children).some((ch: ChildrenChanges) =>
63
63
  ch.created.length || ch.linked.length
64
+ ) ||
65
+ Object.values(changes.data).some((val: any) =>
66
+ val && typeof val === 'object' && !Array.isArray(val) && (val.create || val.update || val.delete)
64
67
  )
65
68
  );
66
69
 
67
- const changeSummaryLines = $derived.by(() => {
68
- const lines: string[] = [];
69
- const fieldCount = Object.keys(changes.data).length;
70
- if (fieldCount > 0) lines.push(`${fieldCount} field${fieldCount > 1 ? 's' : ''} filled`);
71
- for (const [col, ch] of Object.entries(changes.children) as [string, ChildrenChanges][]) {
72
- if (ch.created.length) lines.push(`${ch.created.length} created in ${col}`);
73
- if (ch.linked.length) lines.push(`${ch.linked.length} linked in ${col}`);
74
- }
75
- return lines;
76
- });
70
+ import { buildChangeTree, renderTree } from "../changeTreeUtils";
71
+
72
+ const changeSummaryLines = $derived(renderTree(buildChangeTree(changes, 'filled')));
77
73
 
78
74
  const fieldNames = Object.keys(ctx.meta.collections[collectionName].fields);
79
75
  let values = $state(getDefaultEntry(ctx, fieldNames, collectionName, passedValues));
@@ -92,7 +88,15 @@
92
88
  });
93
89
  });
94
90
 
91
+ function cleanFkField(val: any): any {
92
+ if (!val || typeof val !== 'object' || Array.isArray(val)) return val;
93
+ if (val.id && val.update) return { ...(val.collection ? { collection: val.collection } : {}), update: val.update };
94
+ return val;
95
+ }
96
+
95
97
  function buildPayload(changes: Changes): { data: Record<string, any>; children?: Record<string, any> } {
98
+ const data: Record<string, any> = {};
99
+ for (const [key, val] of Object.entries(changes.data)) data[key] = cleanFkField(val);
96
100
  const result: Record<string, any> = {};
97
101
  for (const [collection, ops] of Object.entries(changes.children)) {
98
102
  const hasOps = ops.created.length || ops.linked.length;
@@ -103,7 +107,7 @@
103
107
  };
104
108
  }
105
109
  const children = Object.keys(result).length ? result : undefined;
106
- return { data: changes.data, ...(children ? { children } : {}) };
110
+ return { data, ...(children ? { children } : {}) };
107
111
  }
108
112
 
109
113
  function handleCancel() {
@@ -114,7 +118,7 @@
114
118
  if (!isRecordingMode && hasChildChanges && changeSummaryLines.length > 0) {
115
119
  const confirmed = await showDialog(
116
120
  "Confirm changes",
117
- changeSummaryLines.map(l => `• ${l}`).join('\n')
121
+ changeSummaryLines.join('\n')
118
122
  );
119
123
  if (!confirmed) return;
120
124
  }
@@ -126,7 +130,7 @@
126
130
  onChanges?.(snap);
127
131
  if (onSuccessfullSave) await onSuccessfullSave(snap);
128
132
  await onCreated?.(snap.data);
129
- toast.success(`The record was successfully created`);
133
+ if (!isRecordingMode) toast.success(`The record was successfully created`);
130
134
  handleCancel();
131
135
  return;
132
136
  }
@@ -12,12 +12,14 @@
12
12
  collectionName: string;
13
13
  entry: Record<string, any>;
14
14
  fieldsErrors?: Record<string, string[]>;
15
+ changedFields?: string[];
15
16
  }
16
17
 
17
18
  let {
18
19
  collectionName,
19
20
  entry = $bindable(),
20
21
  fieldsErrors = {},
22
+ changedFields = [],
21
23
  }: Props = $props();
22
24
 
23
25
  const { lobb, ctx } = getStudioContext();
@@ -31,13 +33,15 @@
31
33
  );
32
34
  </script>
33
35
 
34
- <div class="flex flex-col gap-4 p-4">
36
+ <div class="grid grid-cols-2 gap-4 p-4">
35
37
  {#each fieldNames as fieldName}
36
38
  {#if !ctx.meta.collections[collectionName].fields[fieldName]?.ui?.hidden}
37
39
  {@const field = getField(ctx, fieldName, collectionName)}
38
40
  {@const FieldIcon = getFieldIcon(ctx, fieldName, collectionName)}
39
41
  {@const description = ctx.meta.collections[collectionName].fields[fieldName]?.description}
40
- <div class="flex flex-col gap-2">
42
+ {@const fieldDef = ctx.meta.collections[collectionName].fields[fieldName]}
43
+ {@const isFullWidth = field.type === "text" || field.type === "polymorphic" || fieldDef?.ui?.input?.type === "richtext" || fieldDef?.ui?.span === 2}
44
+ <div class="flex flex-col gap-2 {isFullWidth ? 'col-span-2' : 'col-span-1'}">
41
45
  <div class="flex flex-1 items-end justify-between gap-2 text-xs">
42
46
  <div class="flex items-center gap-1.5">
43
47
  <ExtensionsComponents
@@ -80,6 +84,7 @@
80
84
  bind:value={entry[fieldName]}
81
85
  bind:entry
82
86
  errorMessages={fieldsErrors[fieldName]}
87
+ changed={changedFields.includes(fieldName)}
83
88
  />
84
89
  </div>
85
90
  {/if}
@@ -2,6 +2,7 @@ interface Props {
2
2
  collectionName: string;
3
3
  entry: Record<string, any>;
4
4
  fieldsErrors?: Record<string, string[]>;
5
+ changedFields?: string[];
5
6
  }
6
7
  declare const DetailView: import("svelte").Component<Props, {}, "entry">;
7
8
  type DetailView = ReturnType<typeof DetailView>;