@lobb-js/studio 0.41.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 +215 -57
- package/dist/components/dataTable/header.svelte +5 -2
- package/dist/components/dataTable/header.svelte.d.ts +1 -0
- package/dist/components/dataTable/listViewChildren.svelte +60 -77
- package/dist/components/dataTable/listViewChildren.svelte.d.ts +1 -1
- package/dist/components/dataTable/table.svelte +18 -77
- package/dist/components/dataTable/table.svelte.d.ts +2 -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 +49 -3
- package/dist/components/detailView/create/createDetailView.svelte.d.ts +1 -0
- 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 +46 -15
- package/dist/components/detailView/update/updateDetailViewButton.svelte +7 -0
- package/dist/components/detailView/update/updateDetailViewButton.svelte.d.ts +1 -0
- package/dist/components/drawer.svelte +16 -2
- package/dist/components/foreingKeyInput.svelte +177 -56
- package/dist/components/foreingKeyInput.svelte.d.ts +1 -1
- package/dist/components/polymorphicInput.svelte +128 -55
- 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 +215 -57
- package/src/lib/components/dataTable/header.svelte +5 -2
- package/src/lib/components/dataTable/listViewChildren.svelte +60 -77
- package/src/lib/components/dataTable/table.svelte +18 -77
- package/src/lib/components/detailView/changeTreeUtils.ts +39 -0
- package/src/lib/components/detailView/create/createDetailView.svelte +49 -3
- 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 +46 -15
- package/src/lib/components/detailView/update/updateDetailViewButton.svelte +7 -0
- package/src/lib/components/drawer.svelte +16 -2
- package/src/lib/components/foreingKeyInput.svelte +177 -56
- package/src/lib/components/polymorphicInput.svelte +128 -55
|
@@ -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 } 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";
|
|
@@ -76,37 +78,80 @@
|
|
|
76
78
|
let localChanges = $state<ChildrenChanges>(
|
|
77
79
|
untrack(() => changes) ?? { created: [], updated: [], deleted: [], linked: [], unlinked: [] }
|
|
78
80
|
);
|
|
81
|
+
// Counter for temporary IDs assigned to locally-created pending records so they
|
|
82
|
+
// can be individually targeted by handleDelete before being committed to the DB.
|
|
83
|
+
let nextTempId = -1;
|
|
79
84
|
|
|
80
85
|
|
|
81
86
|
// Derives the displayed rows by applying localChanges on top of server data.
|
|
87
|
+
// Deleted/unlinked rows stay visible but carry _recordingState so the table
|
|
88
|
+
// can render them with visual staging indicators.
|
|
82
89
|
const data = $derived.by(() => {
|
|
83
90
|
if (!isRecordingMode) return serverData;
|
|
84
91
|
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
const deletedIds = new Set(localChanges.deleted.map((r: any) => String(r.id)));
|
|
93
|
+
const unlinkedIds = new Set(localChanges.unlinked.map((r: any) => String(r.id)));
|
|
94
|
+
|
|
95
|
+
let result = serverData.map((r: any) => {
|
|
96
|
+
const id = String(r.id);
|
|
97
|
+
const update = localChanges.updated.find((u) => String(u.id) === id);
|
|
98
|
+
const hasUpdate = update && (
|
|
99
|
+
Object.keys(update.changes.data).length > 0 ||
|
|
100
|
+
Object.values(update.changes.children).some((ch: any) =>
|
|
101
|
+
ch.created.length || ch.updated.length || ch.deleted.length || ch.linked.length || ch.unlinked.length
|
|
102
|
+
)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
let state: string | undefined;
|
|
106
|
+
if (deletedIds.has(id)) state = 'deleted';
|
|
107
|
+
else if (unlinkedIds.has(id)) state = 'unlinked';
|
|
108
|
+
else if (hasUpdate) state = 'updated';
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
...(hasUpdate ? { ...r, ...update!.changes.data } : r),
|
|
112
|
+
...(state ? { _recordingState: state } : {}),
|
|
113
|
+
};
|
|
95
114
|
});
|
|
96
115
|
|
|
97
116
|
for (const record of localChanges.linked) {
|
|
98
117
|
if (!result.some((r: any) => String(r.id) === String(record.id))) {
|
|
99
|
-
result = [...result, record];
|
|
118
|
+
result = [...result, { ...record, _recordingState: 'linked' }];
|
|
100
119
|
}
|
|
101
120
|
}
|
|
102
121
|
|
|
103
122
|
for (const item of localChanges.created) {
|
|
104
|
-
result = [...result, { ...item.data,
|
|
123
|
+
result = [...result, { ...item.data, id: (item as any)._tempId, _recordingState: 'created' }];
|
|
105
124
|
}
|
|
106
125
|
|
|
107
126
|
return result;
|
|
108
127
|
});
|
|
109
128
|
|
|
129
|
+
// IDs to exclude from the Link picker — records already present in this table
|
|
130
|
+
const excludeIds = $derived([
|
|
131
|
+
...serverData.map((r: any) => r.id),
|
|
132
|
+
...localChanges.linked.map((r: any) => r.id),
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
function onCellClass(entry: any, cellIndex: number): string {
|
|
136
|
+
if (!isRecordingMode) return '';
|
|
137
|
+
const state = entry._recordingState;
|
|
138
|
+
const border = cellIndex === 0 ? {
|
|
139
|
+
deleted: 'border-l-2 border-l-red-500',
|
|
140
|
+
unlinked: 'border-l-2 border-l-slate-500',
|
|
141
|
+
created: 'border-l-2 border-l-green-500',
|
|
142
|
+
linked: 'border-l-2 border-l-blue-500',
|
|
143
|
+
updated: 'border-l-2 border-l-orange-500',
|
|
144
|
+
}[state as string] ?? '' : '';
|
|
145
|
+
const bg: Record<string, string> = {
|
|
146
|
+
deleted: '!bg-red-500/5',
|
|
147
|
+
unlinked: '!bg-slate-500/5',
|
|
148
|
+
created: '!bg-green-500/5',
|
|
149
|
+
linked: '!bg-blue-500/5',
|
|
150
|
+
updated: '!bg-orange-500/5',
|
|
151
|
+
};
|
|
152
|
+
return `${bg[state as string] ?? ''} ${border}`.trim();
|
|
153
|
+
}
|
|
154
|
+
|
|
110
155
|
const hasRowActions = $derived(
|
|
111
156
|
loadExtensionComponents(ctx, "listView.entry.actions", undefined, { collectionName }).length > 0
|
|
112
157
|
);
|
|
@@ -177,6 +222,7 @@
|
|
|
177
222
|
(ctx.meta.collections[collectionName]?.children ?? [])
|
|
178
223
|
.some((c: any) => c.type === "fk" || c.type === "m2m" || c.type === "polymorphic")
|
|
179
224
|
);
|
|
225
|
+
let childrenDrawerEntry = $state<Record<string, any> | null>(null);
|
|
180
226
|
|
|
181
227
|
// requests the data from the server when the params is changed
|
|
182
228
|
$effect(() => {
|
|
@@ -204,16 +250,24 @@
|
|
|
204
250
|
}
|
|
205
251
|
|
|
206
252
|
async function handleDelete(entryId: string) {
|
|
207
|
-
|
|
208
|
-
|
|
253
|
+
if (!isRecordingMode) {
|
|
254
|
+
const result = await showDialog("Are you sure?", "This will permanently delete the record.");
|
|
255
|
+
if (!result) return;
|
|
256
|
+
}
|
|
209
257
|
if (isRecordingMode) {
|
|
210
258
|
// If the record was locally linked (not yet in DB), just cancel the link instead of marking for deletion.
|
|
211
259
|
const linkedIdx = localChanges.linked.findIndex((r: any) => String(r.id) === String(entryId));
|
|
212
260
|
if (linkedIdx !== -1) {
|
|
213
261
|
localChanges.linked.splice(linkedIdx, 1);
|
|
214
262
|
} else {
|
|
215
|
-
|
|
216
|
-
|
|
263
|
+
// If it's a locally-created pending record (has a negative tempId), remove it from created.
|
|
264
|
+
const createdIdx = localChanges.created.findIndex((item: any) => String(item._tempId) === String(entryId));
|
|
265
|
+
if (createdIdx !== -1) {
|
|
266
|
+
localChanges.created.splice(createdIdx, 1);
|
|
267
|
+
} else {
|
|
268
|
+
const record = serverData.find((r: any) => String(r.id) === String(entryId));
|
|
269
|
+
if (record) localChanges.deleted.push($state.snapshot(record));
|
|
270
|
+
}
|
|
217
271
|
}
|
|
218
272
|
onChanges?.($state.snapshot(localChanges));
|
|
219
273
|
} else if (parentContext) {
|
|
@@ -228,8 +282,10 @@
|
|
|
228
282
|
}
|
|
229
283
|
|
|
230
284
|
async function handleUnlink(entryId: string) {
|
|
231
|
-
|
|
232
|
-
|
|
285
|
+
if (!isRecordingMode) {
|
|
286
|
+
const result = await showDialog("Are you sure?", "This will unlink the record without deleting it.");
|
|
287
|
+
if (!result) return;
|
|
288
|
+
}
|
|
233
289
|
if (isRecordingMode) {
|
|
234
290
|
// If the record was locally linked this session, just cancel the link — net effect is no change.
|
|
235
291
|
const linkedIdx = localChanges.linked.findIndex((r: any) => String(r.id) === String(entryId));
|
|
@@ -265,14 +321,63 @@
|
|
|
265
321
|
});
|
|
266
322
|
|
|
267
323
|
function handleCreate(changes: import("../detailView/utils").Changes) {
|
|
268
|
-
localChanges.created.push({ data: changes.data });
|
|
324
|
+
(localChanges.created as any[]).push({ data: changes.data, _tempId: nextTempId-- });
|
|
325
|
+
onChanges?.($state.snapshot(localChanges));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const hasAnyChanges = $derived(
|
|
329
|
+
localChanges.created.length > 0 ||
|
|
330
|
+
localChanges.updated.length > 0 ||
|
|
331
|
+
localChanges.deleted.length > 0 ||
|
|
332
|
+
localChanges.linked.length > 0 ||
|
|
333
|
+
localChanges.unlinked.length > 0
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
function handleRevertRow(entryId: string) {
|
|
337
|
+
const id = String(entryId);
|
|
338
|
+
const createdIdx = localChanges.created.findIndex((item: any) => String(item._tempId) === id);
|
|
339
|
+
if (createdIdx !== -1) localChanges.created.splice(createdIdx, 1);
|
|
340
|
+
const linkedIdx = localChanges.linked.findIndex((r: any) => String(r.id) === id);
|
|
341
|
+
if (linkedIdx !== -1) localChanges.linked.splice(linkedIdx, 1);
|
|
342
|
+
const deletedIdx = localChanges.deleted.findIndex((r: any) => String(r.id) === id);
|
|
343
|
+
if (deletedIdx !== -1) localChanges.deleted.splice(deletedIdx, 1);
|
|
344
|
+
const unlinkedIdx = localChanges.unlinked.findIndex((r: any) => String(r.id) === id);
|
|
345
|
+
if (unlinkedIdx !== -1) localChanges.unlinked.splice(unlinkedIdx, 1);
|
|
346
|
+
const updatedIdx = localChanges.updated.findIndex((u: any) => String(u.id) === id);
|
|
347
|
+
if (updatedIdx !== -1) localChanges.updated.splice(updatedIdx, 1);
|
|
348
|
+
onChanges?.($state.snapshot(localChanges));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function handleRevertAll() {
|
|
352
|
+
localChanges.created = [];
|
|
353
|
+
localChanges.updated = [];
|
|
354
|
+
localChanges.deleted = [];
|
|
355
|
+
localChanges.linked = [];
|
|
356
|
+
localChanges.unlinked = [];
|
|
357
|
+
onChanges?.($state.snapshot(localChanges));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function handleEditPending(tempId: string, editChanges: import("../detailView/utils").Changes) {
|
|
361
|
+
const createdIdx = localChanges.created.findIndex((item: any) => String(item._tempId) === tempId);
|
|
362
|
+
if (createdIdx !== -1) {
|
|
363
|
+
localChanges.created[createdIdx] = {
|
|
364
|
+
...localChanges.created[createdIdx],
|
|
365
|
+
data: { ...localChanges.created[createdIdx].data, ...editChanges.data },
|
|
366
|
+
};
|
|
367
|
+
}
|
|
269
368
|
onChanges?.($state.snapshot(localChanges));
|
|
270
369
|
}
|
|
271
370
|
|
|
272
371
|
function handleUpdate(id: string, editChanges: import("../detailView/utils").Changes) {
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
372
|
+
const isEmpty = Object.keys(editChanges.data).length === 0 &&
|
|
373
|
+
Object.values(editChanges.children).every((ch: any) =>
|
|
374
|
+
!ch.created.length && !ch.updated.length && !ch.deleted.length && !ch.linked.length && !ch.unlinked.length
|
|
375
|
+
);
|
|
376
|
+
const existingIdx = localChanges.updated.findIndex((u) => String(u.id) === id);
|
|
377
|
+
if (isEmpty) {
|
|
378
|
+
if (existingIdx !== -1) localChanges.updated.splice(existingIdx, 1);
|
|
379
|
+
} else if (existingIdx !== -1) {
|
|
380
|
+
localChanges.updated[existingIdx].changes = editChanges;
|
|
276
381
|
} else {
|
|
277
382
|
localChanges.updated.push({ id, changes: editChanges });
|
|
278
383
|
}
|
|
@@ -306,9 +411,22 @@
|
|
|
306
411
|
{parentContext}
|
|
307
412
|
onLink={isRecordingMode ? handleLink : undefined}
|
|
308
413
|
onCreate={isRecordingMode ? handleCreate : undefined}
|
|
414
|
+
{excludeIds}
|
|
309
415
|
>
|
|
310
416
|
{#snippet left()}
|
|
311
417
|
{@render headerLeft?.()}
|
|
418
|
+
{#if isRecordingMode && hasAnyChanges}
|
|
419
|
+
<Button
|
|
420
|
+
class="h-6 px-2 font-normal text-xs text-muted-foreground"
|
|
421
|
+
variant="ghost"
|
|
422
|
+
size="sm"
|
|
423
|
+
Icon={RotateCcw}
|
|
424
|
+
onclick={handleRevertAll}
|
|
425
|
+
title="Revert all changes"
|
|
426
|
+
>
|
|
427
|
+
Revert all
|
|
428
|
+
</Button>
|
|
429
|
+
{/if}
|
|
312
430
|
{/snippet}
|
|
313
431
|
</Header>
|
|
314
432
|
{/if}
|
|
@@ -338,7 +456,6 @@
|
|
|
338
456
|
<Table
|
|
339
457
|
{data}
|
|
340
458
|
{columns}
|
|
341
|
-
showCollapsible={doesCollectionHasChildren}
|
|
342
459
|
selectByColumn="id"
|
|
343
460
|
showLastRowBorder={true}
|
|
344
461
|
showLastColumnBorder={true}
|
|
@@ -346,40 +463,70 @@
|
|
|
346
463
|
bind:selectedRecords
|
|
347
464
|
bind:tableWidth={dataTableWidth}
|
|
348
465
|
{...tableProps}
|
|
349
|
-
rowActions={hasRowActions ? rowActionsSnippet : undefined}
|
|
350
|
-
{
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
<UpdateDetailViewButton
|
|
354
|
-
{collectionName}
|
|
355
|
-
recordId={entry.id}
|
|
356
|
-
variant="ghost"
|
|
357
|
-
class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
|
|
358
|
-
Icon={Pencil}
|
|
359
|
-
changes={isRecordingMode ? localChanges.updated.find((u) => String(u.id) === String(entry.id))?.changes : undefined}
|
|
360
|
-
onChanges={isRecordingMode ? (c) => handleUpdate(String(entry.id), c) : undefined}
|
|
361
|
-
onSuccessfullSave={!isRecordingMode ? async () => { params = { ...params }; } : undefined}
|
|
362
|
-
></UpdateDetailViewButton>
|
|
363
|
-
</CanAccess>
|
|
364
|
-
{/if}
|
|
365
|
-
{#if parentContext}
|
|
466
|
+
rowActions={hasRowActions ? rowActionsSnippet : undefined}
|
|
467
|
+
onCellClass={isRecordingMode ? onCellClass : undefined}>
|
|
468
|
+
{#snippet preTools(entry)}
|
|
469
|
+
{#if doesCollectionHasChildren}
|
|
366
470
|
<Button
|
|
367
471
|
class="h-6 w-6 text-muted-foreground hover:bg-transparent"
|
|
368
472
|
variant="ghost"
|
|
369
473
|
size="icon"
|
|
370
|
-
onclick={() =>
|
|
371
|
-
Icon={
|
|
372
|
-
title="
|
|
474
|
+
onclick={() => { childrenDrawerEntry = entry; }}
|
|
475
|
+
Icon={Network}
|
|
476
|
+
title="Show children"
|
|
477
|
+
aria-label={`Show children of record ${entry.id}`}
|
|
373
478
|
></Button>
|
|
374
479
|
{/if}
|
|
375
|
-
|
|
480
|
+
{/snippet}
|
|
481
|
+
{#snippet tools(entry)}
|
|
482
|
+
{#if entry._recordingState !== 'deleted' && entry._recordingState !== 'unlinked'}
|
|
483
|
+
{#if showEdit}
|
|
484
|
+
{@const isPending = entry._recordingState === 'created'}
|
|
485
|
+
<CanAccess collection={collectionName} action="update">
|
|
486
|
+
<UpdateDetailViewButton
|
|
487
|
+
{collectionName}
|
|
488
|
+
recordId={entry.id}
|
|
489
|
+
variant="ghost"
|
|
490
|
+
class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
|
|
491
|
+
Icon={Pencil}
|
|
492
|
+
aria-label={`Edit record ${entry.id}`}
|
|
493
|
+
values={isPending ? entry : undefined}
|
|
494
|
+
changes={isRecordingMode && !isPending ? localChanges.updated.find((u) => String(u.id) === String(entry.id))?.changes : undefined}
|
|
495
|
+
onChanges={isPending ? (c) => handleEditPending(String(entry.id), c) : isRecordingMode ? (c) => handleUpdate(String(entry.id), c) : undefined}
|
|
496
|
+
onSuccessfullSave={!isRecordingMode ? async () => { params = { ...params }; } : undefined}
|
|
497
|
+
></UpdateDetailViewButton>
|
|
498
|
+
</CanAccess>
|
|
499
|
+
{/if}
|
|
500
|
+
{#if parentContext}
|
|
501
|
+
<Button
|
|
502
|
+
class="h-6 w-6 text-muted-foreground hover:bg-transparent"
|
|
503
|
+
variant="ghost"
|
|
504
|
+
size="icon"
|
|
505
|
+
onclick={() => handleUnlink(entry.id)}
|
|
506
|
+
Icon={Unlink}
|
|
507
|
+
title="Remove from this entry"
|
|
508
|
+
></Button>
|
|
509
|
+
{/if}
|
|
510
|
+
{#if showDelete}
|
|
511
|
+
<Button
|
|
512
|
+
class="h-6 w-6 text-muted-foreground hover:bg-transparent"
|
|
513
|
+
variant="ghost"
|
|
514
|
+
size="icon"
|
|
515
|
+
onclick={() => handleDelete(entry.id)}
|
|
516
|
+
Icon={Trash}
|
|
517
|
+
title="Delete permanently"
|
|
518
|
+
></Button>
|
|
519
|
+
{/if}
|
|
520
|
+
{/if}
|
|
521
|
+
{#if isRecordingMode && entry._recordingState}
|
|
376
522
|
<Button
|
|
377
523
|
class="h-6 w-6 text-muted-foreground hover:bg-transparent"
|
|
378
524
|
variant="ghost"
|
|
379
525
|
size="icon"
|
|
380
|
-
onclick={() =>
|
|
381
|
-
Icon={
|
|
382
|
-
title="
|
|
526
|
+
onclick={() => handleRevertRow(entry.id)}
|
|
527
|
+
Icon={RotateCcw}
|
|
528
|
+
title="Revert changes"
|
|
529
|
+
aria-label={`Revert changes for record ${entry.id}`}
|
|
383
530
|
></Button>
|
|
384
531
|
{/if}
|
|
385
532
|
{/snippet}
|
|
@@ -392,15 +539,6 @@
|
|
|
392
539
|
refresh={() => { params = { ...params }; }}
|
|
393
540
|
/>
|
|
394
541
|
{/snippet}
|
|
395
|
-
{#snippet collapsible(entry)}
|
|
396
|
-
<ListViewChildren
|
|
397
|
-
{collectionName}
|
|
398
|
-
recordId={entry.id}
|
|
399
|
-
width={dataTableWidth > dataTableContainerWidth
|
|
400
|
-
? dataTableContainerWidth
|
|
401
|
-
: dataTableWidth}
|
|
402
|
-
/>
|
|
403
|
-
{/snippet}
|
|
404
542
|
</Table>
|
|
405
543
|
{/if}
|
|
406
544
|
</div>
|
|
@@ -414,3 +552,23 @@
|
|
|
414
552
|
/>
|
|
415
553
|
{/if}
|
|
416
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}
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
showFilter?: boolean;
|
|
29
29
|
loading?: boolean;
|
|
30
30
|
left?: Snippet<[]>;
|
|
31
|
+
excludeIds?: (string | number)[];
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
let {
|
|
@@ -40,7 +41,8 @@
|
|
|
40
41
|
showImport = true,
|
|
41
42
|
showFilter = true,
|
|
42
43
|
loading = false,
|
|
43
|
-
left
|
|
44
|
+
left,
|
|
45
|
+
excludeIds = [],
|
|
44
46
|
}: Props = $props();
|
|
45
47
|
|
|
46
48
|
function handleLink(record: any) {
|
|
@@ -169,13 +171,14 @@
|
|
|
169
171
|
{collectionName}
|
|
170
172
|
refresh={() => { params = { ...params }; }}
|
|
171
173
|
/>
|
|
172
|
-
{#if parentContext}
|
|
174
|
+
{#if parentContext || onLink}
|
|
173
175
|
<SelectRecord
|
|
174
176
|
{collectionName}
|
|
175
177
|
variant="outline"
|
|
176
178
|
size="sm"
|
|
177
179
|
Icon={Link}
|
|
178
180
|
onSelect={handleLink}
|
|
181
|
+
additionalFilter={excludeIds.length > 0 ? { $and: excludeIds.map(id => ({ id: { $ne: id } })) } : {}}
|
|
179
182
|
>
|
|
180
183
|
{headerIsSmall ? "" : "Link"}
|
|
181
184
|
</SelectRecord>
|
|
@@ -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>
|