@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
@@ -4,16 +4,17 @@
4
4
  import DataTable from "./dataTable/dataTable.svelte";
5
5
  import Drawer from "./drawer.svelte";
6
6
  import * as Popover from "./ui/popover/index";
7
- import { getCollectionPrimaryField } from "./dataTable/utils";
8
7
  import { getStudioContext } from "../context";
9
- import { ArrowLeft, Link, ChevronDown, Plus } from "lucide-svelte";
8
+ import { ArrowLeft, Link, ChevronDown, Plus, Pencil, Unlink, Trash, RotateCcw, RefreshCw } from "lucide-svelte";
10
9
  import CreateDetailView from "./detailView/create/createDetailView.svelte";
10
+ import UpdateDetailView from "./detailView/update/updateDetailView.svelte";
11
11
 
12
12
  const { ctx, lobb } = getStudioContext();
13
13
 
14
14
  interface Props {
15
15
  collectionField: string;
16
16
  idField: string;
17
+ virtualField: string;
17
18
  targetCollections: string[];
18
19
  entry: Record<string, any>;
19
20
  destructive?: boolean;
@@ -22,39 +23,52 @@
22
23
  let {
23
24
  collectionField,
24
25
  idField,
26
+ virtualField,
25
27
  targetCollections,
26
28
  entry = $bindable(),
27
29
  destructive,
28
30
  }: Props = $props();
29
31
 
32
+
30
33
  const selectedCollection = $derived(entry[collectionField] ?? null);
31
- const selectedId = $derived(entry[idField] ?? null);
34
+ const selectedId = $derived(entry[idField] ?? null);
35
+ const virtualVal = $derived(entry[virtualField] ?? null);
36
+
37
+ let initialId = $state<number | null | undefined>(undefined);
38
+ let unlinked = $state(false);
39
+ onMount(() => { initialId = entry[idField] ?? null; });
40
+
41
+ // State derived from virtual field and real fields
42
+ const isPendingCreate = $derived(virtualVal && typeof virtualVal === 'object' && virtualVal.create);
43
+ const isPendingEdit = $derived(virtualVal && typeof virtualVal === 'object' && virtualVal.update);
44
+ const isStagedUnlink = $derived(unlinked);
45
+ const isStagedDelete = $derived(virtualVal && typeof virtualVal === 'object' && virtualVal.delete === true);
46
+ const hasRealValue = $derived(selectedId != null && !isPendingCreate && !isPendingEdit && !isStagedUnlink && !isStagedDelete);
47
+ const isNewLink = $derived(hasRealValue && initialId !== undefined && initialId == null && selectedId !== initialId);
48
+ const isReplaced = $derived(hasRealValue && initialId !== undefined && initialId != null && selectedId !== initialId);
49
+ const isEmpty = $derived(!unlinked && (!selectedCollection || (selectedId == null && !isPendingCreate && !isPendingEdit && !isStagedDelete)));
50
+
51
+ const bgClass = $derived(
52
+ isPendingCreate ? '!bg-green-500/5 border-green-500/40' :
53
+ isNewLink ? '!bg-blue-500/5 border-blue-500/40' :
54
+ isReplaced ? '!bg-orange-500/5 border-orange-500/40' :
55
+ isPendingEdit ? '!bg-orange-500/5 border-orange-500/40' :
56
+ isStagedUnlink ? '!bg-slate-500/5 border-slate-500/40' :
57
+ isStagedDelete ? '!bg-red-500/5 border-red-500/40' :
58
+ ''
59
+ );
32
60
 
33
- let displayName = $state<string | null>(null);
34
61
  let collectionPopoverOpen = $state(false);
35
62
  let recordDrawerOpen = $state(false);
36
63
  let createDrawerOpen = $state(false);
37
-
38
- onMount(async () => {
39
- if (selectedCollection == null || selectedId == null) return;
40
- try {
41
- const res = await lobb.findOne(selectedCollection, selectedId);
42
- const record = await res.json();
43
- const primaryFieldName = getCollectionPrimaryField(ctx, selectedCollection);
44
- displayName = primaryFieldName ? String(record[primaryFieldName]) : null;
45
- } catch {
46
- displayName = null;
47
- }
48
- });
49
-
50
- $effect(() => {
51
- if (entry[idField] == null) displayName = null;
52
- });
64
+ let editDrawerOpen = $state(false);
65
+ let editValues: Record<string, any> | undefined = $state(undefined);
66
+ let originalSnapshot: { collection: string; id: number } | null = $state(null);
53
67
 
54
68
  function onCollectionChange(col: string) {
55
69
  collectionPopoverOpen = false;
56
70
  if (entry[collectionField] !== col) {
57
- entry = { ...entry, [collectionField]: col, [idField]: null };
71
+ entry = { ...entry, [collectionField]: col, [idField]: null, [virtualField]: undefined };
58
72
  }
59
73
  }
60
74
 
@@ -65,20 +79,60 @@
65
79
  }
66
80
 
67
81
  function onRecordSelect(record: any) {
68
- const primaryFieldName = getCollectionPrimaryField(ctx, selectedCollection!);
69
- entry = { ...entry, [idField]: record.id };
70
- displayName = primaryFieldName ? String(record[primaryFieldName]) : null;
82
+ entry = { ...entry, [idField]: record.id, [virtualField]: undefined };
71
83
  recordDrawerOpen = false;
72
84
  }
73
85
 
74
86
  async function onPolyCreated(record: any) {
75
- const primaryFieldName = getCollectionPrimaryField(ctx, selectedCollection!);
76
- entry = { ...entry, [idField]: record.id };
77
- displayName = primaryFieldName ? String(record[primaryFieldName]) : null;
87
+ if (!record.id) {
88
+ entry = { ...entry, [virtualField]: { collection: selectedCollection, create: record }, [collectionField]: selectedCollection, [idField]: null };
89
+ } else {
90
+ entry = { ...entry, [idField]: record.id, [virtualField]: undefined };
91
+ }
92
+ }
93
+
94
+ async function openEdit() {
95
+ const res = await lobb.findAll(selectedCollection!, { filter: { id: selectedId }, limit: 1 });
96
+ const result = await res.json();
97
+ editValues = result.data[0];
98
+ editDrawerOpen = true;
99
+ }
100
+
101
+ function handleEditChanges(changes: import('./detailView/utils').Changes) {
102
+ if (Object.keys(changes.data).length === 0) {
103
+ entry = { ...entry, [virtualField]: undefined };
104
+ } else {
105
+ entry = { ...entry, [virtualField]: { collection: selectedCollection, id: selectedId, update: changes.data } };
106
+ }
107
+ }
108
+
109
+ function handleUnlink() {
110
+ if (hasRealValue) {
111
+ originalSnapshot = { collection: selectedCollection!, id: selectedId! };
112
+ unlinked = true;
113
+ entry = { ...entry, [collectionField]: null, [idField]: null, [virtualField]: undefined };
114
+ }
115
+ }
116
+
117
+ async function handleDelete() {
118
+ if (hasRealValue) {
119
+ originalSnapshot = { collection: selectedCollection!, id: selectedId! };
120
+ entry = { ...entry, [virtualField]: { delete: true } };
121
+ }
122
+ }
123
+
124
+ function handleRevert() {
125
+ if (originalSnapshot) {
126
+ entry = { ...entry, [collectionField]: originalSnapshot.collection, [idField]: originalSnapshot.id, [virtualField]: undefined };
127
+ originalSnapshot = null;
128
+ } else {
129
+ entry = { ...entry, [collectionField]: null, [idField]: null, [virtualField]: undefined };
130
+ }
131
+ unlinked = false;
78
132
  }
79
133
  </script>
80
134
 
81
- <div class="flex h-9 w-full items-center gap-1.5 rounded-md border pl-1.5 pr-9 text-xs bg-muted {destructive ? 'border-destructive bg-destructive/10' : ''}">
135
+ <div class="flex h-9 w-full items-center gap-1.5 rounded-md border pl-1.5 pr-9 text-xs bg-muted {bgClass} {destructive ? '!bg-destructive/10 border-destructive' : ''}">
82
136
  <!-- Collection picker -->
83
137
  <Popover.Root bind:open={collectionPopoverOpen}>
84
138
  <Popover.Trigger>
@@ -107,39 +161,32 @@
107
161
  </Popover.Content>
108
162
  </Popover.Root>
109
163
 
110
- <!-- Transparent id input -->
164
+ <!-- ID input (only editable when real value or empty) -->
111
165
  <input
112
166
  placeholder="NULL"
113
167
  type="number"
114
168
  class="min-w-0 flex-1 bg-transparent outline-none text-xs placeholder:text-muted-foreground"
115
- value={selectedId ?? ""}
169
+ value={isStagedUnlink || isStagedDelete ? (virtualVal?.id ?? '') : (selectedId ?? "")}
170
+ disabled={isPendingCreate || isPendingEdit}
116
171
  oninput={onIdChange}
117
172
  />
118
173
 
119
- <!-- Primary field badge -->
120
- {#if displayName}
121
- <div class="flex shrink-0 items-center bg-background rounded-full border h-6 px-3 shadow-sm">
122
- {displayName}
123
- </div>
124
- {/if}
125
-
126
- <!-- Create / Select record buttons -->
127
- {#if selectedCollection}
128
- <Button
129
- class="h-6 shrink-0 px-2 font-normal text-xs"
130
- variant="outline"
131
- onclick={() => (createDrawerOpen = true)}
132
- >
174
+ <!-- Action buttons -->
175
+ {#if isStagedUnlink || isStagedDelete || isPendingCreate || isPendingEdit}
176
+ <Button class="h-5 w-5 px-0 shrink-0 hover:bg-transparent text-muted-foreground" variant="ghost" Icon={RotateCcw} onclick={handleRevert} title="Revert"></Button>
177
+ {:else if hasRealValue}
178
+ <Button class="h-5 w-5 px-0 shrink-0 hover:bg-transparent text-muted-foreground" variant="ghost" Icon={Trash} onclick={handleDelete} title="Delete record"></Button>
179
+ <Button class="h-5 w-5 px-0 shrink-0 hover:bg-transparent text-muted-foreground" variant="ghost" Icon={Unlink} onclick={handleUnlink} title="Unlink"></Button>
180
+ <Button class="h-5 w-5 px-0 shrink-0 hover:bg-transparent text-muted-foreground" variant="ghost" Icon={Pencil} onclick={openEdit} title="Edit record"></Button>
181
+ <Button class="h-5 w-5 px-0 shrink-0 hover:bg-transparent text-muted-foreground" variant="ghost" Icon={RefreshCw} onclick={() => (recordDrawerOpen = true)} title="Replace"></Button>
182
+ {:else if selectedCollection && !isPendingCreate}
183
+ <Button class="h-6 shrink-0 px-2 font-normal text-xs" variant="outline" onclick={() => (createDrawerOpen = true)}>
133
184
  <Plus size="13" />
134
185
  Create
135
186
  </Button>
136
- <Button
137
- class="h-6 shrink-0 px-2 font-normal text-xs"
138
- variant="outline"
139
- onclick={() => (recordDrawerOpen = true)}
140
- >
187
+ <Button class="h-6 shrink-0 px-2 font-normal text-xs" variant="outline" onclick={() => (recordDrawerOpen = true)}>
141
188
  <Link size="13" />
142
- Select
189
+ Link
143
190
  </Button>
144
191
  {/if}
145
192
  </div>
@@ -147,26 +194,17 @@
147
194
  {#if recordDrawerOpen}
148
195
  <Drawer onHide={async () => { recordDrawerOpen = false }}>
149
196
  <div class="flex h-12 items-center gap-4 border-b px-4">
150
- <Button
151
- variant="outline"
152
- onclick={() => (recordDrawerOpen = false)}
153
- class="h-8 w-8 rounded-full text-xs font-normal"
154
- Icon={ArrowLeft}
155
- />
197
+ <Button variant="outline" onclick={() => (recordDrawerOpen = false)} class="h-8 w-8 rounded-full text-xs font-normal" Icon={ArrowLeft} />
156
198
  <div class="flex items-center gap-2">
157
199
  <div class="text-sm">Select record from</div>
158
- <span class="rounded-md border bg-muted px-2 py-0.5 text-sm">
159
- {selectedCollection}
160
- </span>
200
+ <span class="rounded-md border bg-muted px-2 py-0.5 text-sm">{selectedCollection}</span>
161
201
  </div>
162
202
  </div>
163
203
  <div class="flex-1 overflow-y-auto bg-muted">
164
204
  <DataTable
165
205
  collectionName={selectedCollection!}
166
- tableProps={{
167
- showCheckboxes: false,
168
- select: { onSelect: onRecordSelect },
169
- }}
206
+ filter={selectedId != null ? { id: { $ne: selectedId } } : undefined}
207
+ tableProps={{ showCheckboxes: false, select: { onSelect: onRecordSelect } }}
170
208
  />
171
209
  </div>
172
210
  </Drawer>
@@ -176,6 +214,17 @@
176
214
  <CreateDetailView
177
215
  collectionName={selectedCollection}
178
216
  onCreated={onPolyCreated}
217
+ onChanges={() => {}}
179
218
  onCancel={async () => { createDrawerOpen = false; }}
180
219
  />
181
220
  {/if}
221
+
222
+ {#if editDrawerOpen && editValues && selectedCollection}
223
+ <UpdateDetailView
224
+ collectionName={selectedCollection}
225
+ recordId={String(editValues.id)}
226
+ values={editValues}
227
+ onChanges={handleEditChanges}
228
+ onCancel={async () => { editDrawerOpen = false; editValues = undefined; }}
229
+ />
230
+ {/if}
@@ -1,6 +1,7 @@
1
1
  interface Props {
2
2
  collectionField: string;
3
3
  idField: string;
4
+ virtualField: string;
4
5
  targetCollections: string[];
5
6
  entry: Record<string, any>;
6
7
  destructive?: boolean;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lobb-js/studio",
3
3
  "license": "UNLICENSED",
4
- "version": "0.42.0",
4
+ "version": "0.43.0",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -45,7 +45,7 @@
45
45
  "postpublish": "./scripts/postpublish.sh"
46
46
  },
47
47
  "devDependencies": {
48
- "@lobb-js/core": "^0.37.1",
48
+ "@lobb-js/core": "^0.38.0",
49
49
  "@chromatic-com/storybook": "^4.1.2",
50
50
  "@playwright/test": "^1.60.0",
51
51
  "@storybook/addon-a11y": "^10.0.1",
@@ -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>