@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.
- package/dist/components/confirmationDialog/confirmationDialog.svelte +1 -1
- package/dist/components/dataTable/dataTable.svelte +42 -16
- package/dist/components/dataTable/listViewChildren.svelte +60 -77
- package/dist/components/dataTable/listViewChildren.svelte.d.ts +1 -1
- package/dist/components/dataTable/table.svelte +8 -56
- package/dist/components/dataTable/table.svelte.d.ts +1 -2
- package/dist/components/detailView/changeTreeUtils.d.ts +7 -0
- package/dist/components/detailView/changeTreeUtils.js +47 -0
- package/dist/components/detailView/create/createDetailView.svelte +17 -13
- package/dist/components/detailView/detailView.svelte +7 -2
- package/dist/components/detailView/detailView.svelte.d.ts +1 -0
- package/dist/components/detailView/fieldInput.svelte +10 -9
- package/dist/components/detailView/fieldInput.svelte.d.ts +1 -0
- package/dist/components/detailView/update/updateDetailView.svelte +22 -18
- package/dist/components/drawer.svelte +15 -2
- package/dist/components/foreingKeyInput.svelte +163 -68
- package/dist/components/foreingKeyInput.svelte.d.ts +1 -1
- package/dist/components/polymorphicInput.svelte +112 -63
- package/dist/components/polymorphicInput.svelte.d.ts +1 -0
- package/package.json +2 -2
- package/src/lib/components/confirmationDialog/confirmationDialog.svelte +1 -1
- package/src/lib/components/dataTable/dataTable.svelte +42 -16
- package/src/lib/components/dataTable/listViewChildren.svelte +60 -77
- package/src/lib/components/dataTable/table.svelte +8 -56
- package/src/lib/components/detailView/changeTreeUtils.ts +39 -0
- package/src/lib/components/detailView/create/createDetailView.svelte +17 -13
- package/src/lib/components/detailView/detailView.svelte +7 -2
- package/src/lib/components/detailView/fieldInput.svelte +10 -9
- package/src/lib/components/detailView/update/updateDetailView.svelte +22 -18
- package/src/lib/components/drawer.svelte +15 -2
- package/src/lib/components/foreingKeyInput.svelte +163 -68
- 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
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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 ? '
|
|
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
|
-
<!--
|
|
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
|
-
<!--
|
|
120
|
-
{#if
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobb-js/studio",
|
|
3
3
|
"license": "UNLICENSED",
|
|
4
|
-
"version": "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.
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
145
|
-
unlinked: '!bg-
|
|
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-
|
|
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 {
|
|
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
|
|
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
|
|
23
|
-
let
|
|
24
|
-
let
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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>
|