@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
|
@@ -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>
|
|
@@ -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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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.
|
|
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="
|
|
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
|
-
|
|
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>;
|