@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
|
@@ -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,14 +31,16 @@
|
|
|
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;
|
|
43
40
|
tableWidth?: number;
|
|
41
|
+
|
|
42
|
+
// recording mode row visuals — cellIndex 0 = tools cell, 1+ = data/action cells
|
|
43
|
+
onCellClass?: (entry: Entry, cellIndex: number) => string;
|
|
44
44
|
}
|
|
45
45
|
</script>
|
|
46
46
|
|
|
@@ -48,7 +48,6 @@
|
|
|
48
48
|
import {
|
|
49
49
|
ArrowDownNarrowWide,
|
|
50
50
|
ArrowUpWideNarrow,
|
|
51
|
-
ChevronRight,
|
|
52
51
|
CircleOff,
|
|
53
52
|
} from "lucide-svelte";
|
|
54
53
|
import Checkbox from "../ui/checkbox/checkbox.svelte";
|
|
@@ -63,7 +62,6 @@
|
|
|
63
62
|
id: key,
|
|
64
63
|
};
|
|
65
64
|
}),
|
|
66
|
-
showCollapsible = false,
|
|
67
65
|
sort = $bindable({}),
|
|
68
66
|
localSorting = false,
|
|
69
67
|
selectedRecords = $bindable(),
|
|
@@ -74,17 +72,16 @@
|
|
|
74
72
|
headerBorderTop = false,
|
|
75
73
|
parentWidth,
|
|
76
74
|
overrideCell,
|
|
75
|
+
preTools,
|
|
77
76
|
tools,
|
|
78
77
|
rowActions,
|
|
79
|
-
collapsible,
|
|
80
78
|
select,
|
|
81
79
|
tableWidth = $bindable(),
|
|
80
|
+
onCellClass,
|
|
82
81
|
}: TableProps = $props();
|
|
83
82
|
|
|
84
|
-
let expandedRows: boolean[] = $state(new Array(data.length).fill(false));
|
|
85
|
-
|
|
86
83
|
// calculate columns count
|
|
87
|
-
const toolsExists = selectedRecords || tools ? 1 : 0;
|
|
84
|
+
const toolsExists = selectedRecords || tools || preTools ? 1 : 0;
|
|
88
85
|
const rowActionsExists = $derived(rowActions ? 1 : 0);
|
|
89
86
|
const columnsLength = columns.length + toolsExists;
|
|
90
87
|
|
|
@@ -175,7 +172,7 @@
|
|
|
175
172
|
grid-template-rows: 2.5rem;
|
|
176
173
|
"
|
|
177
174
|
>
|
|
178
|
-
{#if selectedRecords || tools}
|
|
175
|
+
{#if selectedRecords || tools || preTools}
|
|
179
176
|
<div
|
|
180
177
|
bind:clientWidth={columnsWidths[0]}
|
|
181
178
|
class="
|
|
@@ -183,13 +180,9 @@
|
|
|
183
180
|
flex items-center p-2.5 text-xs h-10
|
|
184
181
|
border-r border-b gap-2
|
|
185
182
|
{headerBorderTop ? 'border-t' : ''}
|
|
186
|
-
bg-muted
|
|
183
|
+
bg-muted/50
|
|
187
184
|
"
|
|
188
185
|
>
|
|
189
|
-
<!-- collapsable toggle -->
|
|
190
|
-
{#if showCollapsible}
|
|
191
|
-
<div class="w-[20px]"></div>
|
|
192
|
-
{/if}
|
|
193
186
|
{#if selectedRecords && showCheckboxes}
|
|
194
187
|
<Checkbox
|
|
195
188
|
class="border-muted-foreground hover:border-foreground"
|
|
@@ -208,7 +201,7 @@
|
|
|
208
201
|
class="
|
|
209
202
|
sticky top-0 z-10
|
|
210
203
|
flex items-center p-2.5 text-xs h-10
|
|
211
|
-
bg-muted
|
|
204
|
+
bg-muted/50
|
|
212
205
|
{lastColumn && !showLastColumnBorder ? '' : 'border-r'}
|
|
213
206
|
border-b gap-2
|
|
214
207
|
{headerBorderTop ? 'border-t' : ''}
|
|
@@ -235,7 +228,7 @@
|
|
|
235
228
|
class="
|
|
236
229
|
sticky top-0 right-0 z-20
|
|
237
230
|
flex items-center p-2.5 h-10
|
|
238
|
-
bg-muted
|
|
231
|
+
bg-muted/50
|
|
239
232
|
border-l border-b
|
|
240
233
|
{headerBorderTop ? 'border-t' : ''}
|
|
241
234
|
"
|
|
@@ -245,30 +238,12 @@
|
|
|
245
238
|
{#each data as entry, index}
|
|
246
239
|
{@const isDisabled = Boolean(entry.__disabled)}
|
|
247
240
|
{@const lastRow = data.length - 1 === index}
|
|
248
|
-
{#if selectedRecords || tools}
|
|
241
|
+
{#if selectedRecords || tools || preTools}
|
|
249
242
|
<div
|
|
250
|
-
class="
|
|
251
|
-
sticky left-0
|
|
252
|
-
flex items-center p-2.5 text-xs h-10
|
|
253
|
-
bg-card
|
|
254
|
-
border-r gap-2
|
|
255
|
-
"
|
|
243
|
+
class="sticky left-0 flex items-center p-2.5 text-xs h-10 bg-card border-r gap-2 {onCellClass?.(entry, 0) ?? ''}"
|
|
256
244
|
>
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
<Button
|
|
260
|
-
variant="ghost"
|
|
261
|
-
class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent transition-transform"
|
|
262
|
-
style={expandedRows[index]
|
|
263
|
-
? "transform: rotate(90deg);"
|
|
264
|
-
: "transform: rotate(0deg);"}
|
|
265
|
-
Icon={ChevronRight}
|
|
266
|
-
onclick={() => {
|
|
267
|
-
expandedRows[index] = !expandedRows[index];
|
|
268
|
-
expandedRows = [...expandedRows];
|
|
269
|
-
}}
|
|
270
|
-
disabled={isDisabled}
|
|
271
|
-
></Button>
|
|
245
|
+
{#if preTools && !isDisabled}
|
|
246
|
+
{@render preTools(entry, index)}
|
|
272
247
|
{/if}
|
|
273
248
|
{#if selectedRecords && showCheckboxes}
|
|
274
249
|
<Checkbox
|
|
@@ -294,12 +269,7 @@
|
|
|
294
269
|
onclick={() => {
|
|
295
270
|
select?.onSelect(entry);
|
|
296
271
|
}}
|
|
297
|
-
class="
|
|
298
|
-
flex items-center p-2.5 text-xs h-10 text-nowrap overflow-clip
|
|
299
|
-
{select ? 'cursor-pointer hover:bg-accent' : ''}
|
|
300
|
-
bg-card
|
|
301
|
-
{lastColumn && !showLastColumnBorder ? '' : 'border-r'}
|
|
302
|
-
"
|
|
272
|
+
class="flex items-center p-2.5 text-xs h-10 text-nowrap overflow-clip bg-card {select ? 'cursor-pointer hover:bg-accent' : ''} {lastColumn && !showLastColumnBorder ? '' : 'border-r'} {onCellClass?.(entry, index + 1) ?? ''}"
|
|
303
273
|
>
|
|
304
274
|
{#if overrideCell}
|
|
305
275
|
{@render overrideCell(fieldValue, column, entry)}
|
|
@@ -310,41 +280,12 @@
|
|
|
310
280
|
{/each}
|
|
311
281
|
{#if rowActions}
|
|
312
282
|
<div
|
|
313
|
-
class="
|
|
314
|
-
sticky right-0 z-10
|
|
315
|
-
flex items-center p-2.5 text-xs h-10
|
|
316
|
-
border-l gap-2
|
|
317
|
-
bg-card
|
|
318
|
-
"
|
|
283
|
+
class="sticky right-0 z-10 flex items-center p-2.5 text-xs h-10 border-l gap-2 bg-card {onCellClass?.(entry, columns.length + 1) ?? ''}"
|
|
319
284
|
>
|
|
320
285
|
{@render rowActions?.(entry, index)}
|
|
321
286
|
</div>
|
|
322
287
|
{/if}
|
|
323
|
-
|
|
324
|
-
<div
|
|
325
|
-
style="grid-column: span {columnsLength + rowActionsExists};"
|
|
326
|
-
class="
|
|
327
|
-
{!showLastColumnBorder ? '' : 'border-r'}
|
|
328
|
-
{lastRow && !showLastRowBorder ? '' : 'border-b'}
|
|
329
|
-
"
|
|
330
|
-
>
|
|
331
|
-
<div
|
|
332
|
-
style="
|
|
333
|
-
{parentWidth ? `width: ${parentWidth}px` : ''};
|
|
334
|
-
max-width: 100vw;
|
|
335
|
-
{expandedRows[index] ? '' : 'height: 0px;'}
|
|
336
|
-
"
|
|
337
|
-
class="
|
|
338
|
-
sticky left-0 top-0 overflow-auto bg-muted
|
|
339
|
-
|
|
340
|
-
{expandedRows[index] ? 'border-t' : ''}
|
|
341
|
-
"
|
|
342
|
-
>
|
|
343
|
-
{#if collapsible && expandedRows[index]}
|
|
344
|
-
{@render collapsible(entry, index)}
|
|
345
|
-
{/if}
|
|
346
|
-
</div>
|
|
347
|
-
</div>
|
|
288
|
+
<div style="grid-column: span {columnsLength + rowActionsExists};" class="{!showLastColumnBorder ? '' : 'border-r'} {lastRow && !showLastRowBorder ? '' : 'border-b'}"></div>
|
|
348
289
|
{/each}
|
|
349
290
|
{/if}
|
|
350
291
|
</div>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Changes, ChildrenChanges } from "./utils";
|
|
2
|
+
|
|
3
|
+
export interface TreeNode { label: string; children: TreeNode[]; }
|
|
4
|
+
|
|
5
|
+
export function buildChangeTree(c: Changes, fieldLabel = 'changed'): TreeNode[] {
|
|
6
|
+
const nodes: TreeNode[] = [];
|
|
7
|
+
let fieldCount = 0;
|
|
8
|
+
for (const [fieldName, val] of Object.entries(c.data ?? {})) {
|
|
9
|
+
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
10
|
+
const v = val as any;
|
|
11
|
+
const t = v.collection ?? fieldName;
|
|
12
|
+
if (v.delete === true) nodes.push({ label: `${t}: inline delete`, children: [] });
|
|
13
|
+
else if (v.update) nodes.push({ label: `${t}: inline edit`, children: [] });
|
|
14
|
+
else if (v.create) nodes.push({ label: `${t}: inline create`, children: [] });
|
|
15
|
+
} else { fieldCount++; }
|
|
16
|
+
}
|
|
17
|
+
if (fieldCount > 0) nodes.unshift({ label: `${fieldCount} field${fieldCount > 1 ? 's' : ''} ${fieldLabel}`, children: [] });
|
|
18
|
+
for (const [col, ch] of Object.entries(c.children ?? {}) as [string, ChildrenChanges][]) {
|
|
19
|
+
const kids: TreeNode[] = [];
|
|
20
|
+
for (let i = 0; i < ch.created.length; i++) kids.push({ label: 'new record', children: [] });
|
|
21
|
+
for (const r of ch.linked) kids.push({ label: `#${(r as any).id} linked`, children: [] });
|
|
22
|
+
for (const u of ch.updated) kids.push({ label: `#${u.id}`, children: buildChangeTree(u.changes) });
|
|
23
|
+
for (const r of ch.deleted) kids.push({ label: `#${(r as any).id} deleted`, children: [] });
|
|
24
|
+
for (const r of ch.unlinked) kids.push({ label: `#${(r as any).id} unlinked`, children: [] });
|
|
25
|
+
if (kids.length) nodes.push({ label: col, children: kids });
|
|
26
|
+
}
|
|
27
|
+
return nodes;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function renderTree(nodes: TreeNode[], prefix = ''): string[] {
|
|
31
|
+
const lines: string[] = [];
|
|
32
|
+
nodes.forEach((node, i) => {
|
|
33
|
+
const last = i === nodes.length - 1;
|
|
34
|
+
lines.push(`${prefix}${last ? '└── ' : '├── '}${node.label}`);
|
|
35
|
+
if (node.children.length)
|
|
36
|
+
lines.push(...renderTree(node.children, prefix + (last ? ' ' : '│ ')));
|
|
37
|
+
});
|
|
38
|
+
return lines;
|
|
39
|
+
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
submitButton?: SubmitButton;
|
|
15
15
|
title?: Snippet<[string]>;
|
|
16
16
|
onSuccessfullSave?: (entry: any) => Promise<void>;
|
|
17
|
+
onCreated?: (record: any) => Promise<void>;
|
|
17
18
|
onCancel?: () => Promise<void>;
|
|
18
19
|
onChanges?: (changes: Changes) => void;
|
|
19
20
|
}
|
|
@@ -25,6 +26,8 @@
|
|
|
25
26
|
import { getStudioContext } from "../../../context";
|
|
26
27
|
import { toast } from "svelte-sonner";
|
|
27
28
|
import { untrack } from "svelte";
|
|
29
|
+
import type { ChildrenChanges } from "../utils";
|
|
30
|
+
import { showDialog } from "../../../actions";
|
|
28
31
|
|
|
29
32
|
const { lobb, ctx } = getStudioContext();
|
|
30
33
|
import CreateDetailViewChildren from "./createDetailViewChildren.svelte";
|
|
@@ -38,6 +41,7 @@
|
|
|
38
41
|
showRelatedRecords = true,
|
|
39
42
|
onCancel,
|
|
40
43
|
onSuccessfullSave,
|
|
44
|
+
onCreated,
|
|
41
45
|
title,
|
|
42
46
|
submitButton,
|
|
43
47
|
onChanges,
|
|
@@ -46,6 +50,27 @@
|
|
|
46
50
|
const isRecordingMode = onChanges !== undefined;
|
|
47
51
|
let changes = $state<Changes>({ data: {}, children: {} });
|
|
48
52
|
|
|
53
|
+
const totalChangeCount = $derived.by(() => {
|
|
54
|
+
let count = Object.keys(changes.data).length;
|
|
55
|
+
for (const ch of Object.values(changes.children) as ChildrenChanges[]) {
|
|
56
|
+
count += ch.created.length + ch.updated.length + ch.deleted.length + ch.linked.length + ch.unlinked.length;
|
|
57
|
+
}
|
|
58
|
+
return count;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const hasChildChanges = $derived(
|
|
62
|
+
Object.values(changes.children).some((ch: ChildrenChanges) =>
|
|
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)
|
|
67
|
+
)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
import { buildChangeTree, renderTree } from "../changeTreeUtils";
|
|
71
|
+
|
|
72
|
+
const changeSummaryLines = $derived(renderTree(buildChangeTree(changes, 'filled')));
|
|
73
|
+
|
|
49
74
|
const fieldNames = Object.keys(ctx.meta.collections[collectionName].fields);
|
|
50
75
|
let values = $state(getDefaultEntry(ctx, fieldNames, collectionName, passedValues));
|
|
51
76
|
let fieldsErrors: Record<string, any> = $state({});
|
|
@@ -63,7 +88,15 @@
|
|
|
63
88
|
});
|
|
64
89
|
});
|
|
65
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
|
+
|
|
66
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);
|
|
67
100
|
const result: Record<string, any> = {};
|
|
68
101
|
for (const [collection, ops] of Object.entries(changes.children)) {
|
|
69
102
|
const hasOps = ops.created.length || ops.linked.length;
|
|
@@ -74,7 +107,7 @@
|
|
|
74
107
|
};
|
|
75
108
|
}
|
|
76
109
|
const children = Object.keys(result).length ? result : undefined;
|
|
77
|
-
return { data
|
|
110
|
+
return { data, ...(children ? { children } : {}) };
|
|
78
111
|
}
|
|
79
112
|
|
|
80
113
|
function handleCancel() {
|
|
@@ -82,17 +115,27 @@
|
|
|
82
115
|
}
|
|
83
116
|
|
|
84
117
|
async function handleSave() {
|
|
118
|
+
if (!isRecordingMode && hasChildChanges && changeSummaryLines.length > 0) {
|
|
119
|
+
const confirmed = await showDialog(
|
|
120
|
+
"Confirm changes",
|
|
121
|
+
changeSummaryLines.join('\n')
|
|
122
|
+
);
|
|
123
|
+
if (!confirmed) return;
|
|
124
|
+
}
|
|
125
|
+
|
|
85
126
|
const snap = $state.snapshot(changes);
|
|
86
127
|
const response = await lobb.createOne(collectionName, buildPayload(snap), undefined, isRecordingMode);
|
|
87
128
|
|
|
88
129
|
if (response.status === 204) {
|
|
89
130
|
onChanges?.(snap);
|
|
90
131
|
if (onSuccessfullSave) await onSuccessfullSave(snap);
|
|
91
|
-
|
|
132
|
+
await onCreated?.(snap.data);
|
|
133
|
+
if (!isRecordingMode) toast.success(`The record was successfully created`);
|
|
92
134
|
handleCancel();
|
|
93
135
|
return;
|
|
94
136
|
}
|
|
95
137
|
|
|
138
|
+
let createdRecord: any = null;
|
|
96
139
|
if (!response.bodyUsed) {
|
|
97
140
|
const result = await response.json();
|
|
98
141
|
if (response.status >= 400) {
|
|
@@ -104,10 +147,12 @@
|
|
|
104
147
|
return;
|
|
105
148
|
}
|
|
106
149
|
}
|
|
150
|
+
createdRecord = result.data ?? result;
|
|
107
151
|
}
|
|
108
152
|
|
|
109
153
|
onChanges?.(snap);
|
|
110
154
|
if (onSuccessfullSave) await onSuccessfullSave(snap);
|
|
155
|
+
await onCreated?.(createdRecord ?? snap.data);
|
|
111
156
|
toast.success(`The record was successfully created`);
|
|
112
157
|
onCancel?.();
|
|
113
158
|
}
|
|
@@ -152,9 +197,10 @@
|
|
|
152
197
|
variant="default"
|
|
153
198
|
size="sm"
|
|
154
199
|
Icon={submitButton?.icon ? submitButton.icon : Plus}
|
|
200
|
+
aria-label={submitButton?.text ?? "Create record"}
|
|
155
201
|
onclick={handleSave}
|
|
156
202
|
>
|
|
157
|
-
{submitButton?.text ?
|
|
203
|
+
{submitButton?.text ?? "Create"}{totalChangeCount > 0 ? ` (${totalChangeCount})` : ''}
|
|
158
204
|
</Button>
|
|
159
205
|
</div>
|
|
160
206
|
</div>
|
|
@@ -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}
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
value: any;
|
|
25
25
|
errorMessages?: string[];
|
|
26
26
|
entry?: Record<string, any>;
|
|
27
|
+
changed?: boolean;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
let {
|
|
@@ -32,6 +33,7 @@
|
|
|
32
33
|
value = $bindable(),
|
|
33
34
|
errorMessages = [],
|
|
34
35
|
entry = $bindable(),
|
|
36
|
+
changed = false,
|
|
35
37
|
}: Props = $props();
|
|
36
38
|
|
|
37
39
|
const ui_input =
|
|
@@ -44,6 +46,7 @@
|
|
|
44
46
|
const isDisabled = field.key === 'id' || Boolean(ui?.disabled)
|
|
45
47
|
const disabledClasses = "pointer-events-none opacity-50";
|
|
46
48
|
const destructive: boolean = $derived(!isDisabled && Boolean(errorMessages.length));
|
|
49
|
+
const changedClass = $derived(changed && !destructive ? '!bg-orange-500/5' : '');
|
|
47
50
|
|
|
48
51
|
</script>
|
|
49
52
|
|
|
@@ -72,6 +75,7 @@
|
|
|
72
75
|
<PolymorphicInput
|
|
73
76
|
collectionField={polymorphicRelation.from.collection_field}
|
|
74
77
|
idField={polymorphicRelation.from.id_field}
|
|
78
|
+
virtualField={polymorphicRelation.from.virtual_field ?? ''}
|
|
75
79
|
targetCollections={polymorphicRelation.to}
|
|
76
80
|
bind:entry
|
|
77
81
|
{destructive}
|
|
@@ -87,7 +91,7 @@
|
|
|
87
91
|
{:else if field.label === "id"}
|
|
88
92
|
<Input
|
|
89
93
|
placeholder="AUTO GENERATED"
|
|
90
|
-
class="bg-muted text-xs"
|
|
94
|
+
class="bg-muted text-xs {changedClass}"
|
|
91
95
|
bind:value
|
|
92
96
|
/>
|
|
93
97
|
{:else if fieldRelationTarget && entry}
|
|
@@ -126,10 +130,7 @@
|
|
|
126
130
|
}
|
|
127
131
|
>
|
|
128
132
|
<Select.Trigger
|
|
129
|
-
class="
|
|
130
|
-
h-9 w-full bg-muted pr-8
|
|
131
|
-
{destructive ? 'border-destructive bg-destructive/10' : ''}
|
|
132
|
-
"
|
|
133
|
+
class="h-9 w-full bg-muted pr-8 {changedClass} {destructive ? 'border-destructive !bg-destructive/10' : ''}"
|
|
133
134
|
>
|
|
134
135
|
{#if value != null && enumOptions}
|
|
135
136
|
<EnumBadge value={String(value)} enum={enumOptions} />
|
|
@@ -159,7 +160,7 @@
|
|
|
159
160
|
placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
|
|
160
161
|
type="text"
|
|
161
162
|
class="
|
|
162
|
-
bg-muted text-xs
|
|
163
|
+
bg-muted text-xs {changedClass}
|
|
163
164
|
{destructive ? 'border-destructive bg-destructive/10' : ''}
|
|
164
165
|
"
|
|
165
166
|
bind:value
|
|
@@ -169,7 +170,7 @@
|
|
|
169
170
|
placeholder={ui?.placeholder ? ui.placeholder : value === "" ? "EMPTY STRING" : "NULL"}
|
|
170
171
|
rows={5}
|
|
171
172
|
class="
|
|
172
|
-
bg-muted text-xs
|
|
173
|
+
bg-muted text-xs {changedClass}
|
|
173
174
|
{destructive ? 'border-destructive bg-destructive/10' : ''}
|
|
174
175
|
"
|
|
175
176
|
bind:value
|
|
@@ -265,7 +266,7 @@
|
|
|
265
266
|
scale={isFloat ? 20 : 0}
|
|
266
267
|
groupDigits={ui?.groupDigits ?? false}
|
|
267
268
|
class="
|
|
268
|
-
bg-muted text-xs
|
|
269
|
+
bg-muted text-xs {changedClass}
|
|
269
270
|
{destructive ? 'border-destructive bg-destructive/10' : ''}
|
|
270
271
|
"
|
|
271
272
|
bind:value
|
|
@@ -275,7 +276,7 @@
|
|
|
275
276
|
placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
|
|
276
277
|
type="text"
|
|
277
278
|
class="
|
|
278
|
-
bg-muted text-xs
|
|
279
|
+
bg-muted text-xs {changedClass}
|
|
279
280
|
{destructive ? 'border-destructive bg-destructive/10' : ''}
|
|
280
281
|
"
|
|
281
282
|
bind:value
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
import { getStudioContext } from "../../../context";
|
|
28
28
|
import { toast } from "svelte-sonner";
|
|
29
29
|
import { untrack } from "svelte";
|
|
30
|
+
import { showDialog } from "../../../actions";
|
|
31
|
+
|
|
30
32
|
|
|
31
33
|
const { lobb, ctx } = getStudioContext();
|
|
32
34
|
import { getChangedProperties } from "../../../utils";
|
|
@@ -69,6 +71,27 @@
|
|
|
69
71
|
),
|
|
70
72
|
);
|
|
71
73
|
|
|
74
|
+
const totalChangeCount = $derived.by(() => {
|
|
75
|
+
let count = Object.keys(localChanges.data).length;
|
|
76
|
+
for (const ch of Object.values(localChanges.children) as ChildrenChanges[]) {
|
|
77
|
+
count += ch.created.length + ch.updated.length + ch.deleted.length + ch.linked.length + ch.unlinked.length;
|
|
78
|
+
}
|
|
79
|
+
return count;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const hasChildChanges = $derived(
|
|
83
|
+
Object.values(localChanges.children).some((ch: ChildrenChanges) =>
|
|
84
|
+
ch.created.length || ch.updated.length || ch.deleted.length || ch.linked.length || ch.unlinked.length
|
|
85
|
+
) ||
|
|
86
|
+
Object.values(localChanges.data).some((val: any) =>
|
|
87
|
+
val && typeof val === 'object' && !Array.isArray(val) && (val.create || val.update || val.delete)
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
import { buildChangeTree, renderTree } from "../changeTreeUtils";
|
|
92
|
+
|
|
93
|
+
const changeSummaryLines = $derived(renderTree(buildChangeTree(localChanges)));
|
|
94
|
+
|
|
72
95
|
$effect(() => {
|
|
73
96
|
const currentEntrySnap = $state.snapshot(values);
|
|
74
97
|
|
|
@@ -77,19 +100,18 @@
|
|
|
77
100
|
});
|
|
78
101
|
});
|
|
79
102
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (hasAny) untrack(() => onChanges?.(snap));
|
|
88
|
-
}
|
|
89
|
-
});
|
|
103
|
+
function cleanFkField(val: any): any {
|
|
104
|
+
if (!val || typeof val !== 'object' || Array.isArray(val)) return val;
|
|
105
|
+
// strip internal id from pending edit (server reads from DB)
|
|
106
|
+
if (val.id && val.update) return { ...(val.collection ? { collection: val.collection } : {}), update: val.update };
|
|
107
|
+
// { delete: true } — pass through as-is (no id needed)
|
|
108
|
+
return val;
|
|
109
|
+
}
|
|
90
110
|
|
|
91
111
|
function buildPayload(changes: Changes): { data: Record<string, any>; children?: Record<string, any> } {
|
|
92
|
-
const { id: _id, ...
|
|
112
|
+
const { id: _id, ...rawData } = changes.data;
|
|
113
|
+
const data: Record<string, any> = {};
|
|
114
|
+
for (const [key, val] of Object.entries(rawData)) data[key] = cleanFkField(val);
|
|
93
115
|
const children = buildChildren(changes.children);
|
|
94
116
|
return { data, ...(children ? { children } : {}) };
|
|
95
117
|
}
|
|
@@ -119,13 +141,21 @@
|
|
|
119
141
|
}
|
|
120
142
|
|
|
121
143
|
async function handleSave() {
|
|
144
|
+
if (!isRecordingMode && hasChildChanges && changeSummaryLines.length > 0) {
|
|
145
|
+
const confirmed = await showDialog(
|
|
146
|
+
"Confirm changes",
|
|
147
|
+
changeSummaryLines.join('\n')
|
|
148
|
+
);
|
|
149
|
+
if (!confirmed) return;
|
|
150
|
+
}
|
|
151
|
+
|
|
122
152
|
const snap = $state.snapshot(localChanges);
|
|
123
153
|
const response = await lobb.updateOne(collectionName, recordId, buildPayload(snap), isRecordingMode);
|
|
124
154
|
|
|
125
155
|
if (response.status === 204) {
|
|
126
156
|
onChanges?.(snap);
|
|
127
157
|
if (onSuccessfullSave) await onSuccessfullSave(snap);
|
|
128
|
-
toast.success(`The record was successfully updated`);
|
|
158
|
+
if (!isRecordingMode) toast.success(`The record was successfully updated`);
|
|
129
159
|
onCancel?.();
|
|
130
160
|
return;
|
|
131
161
|
}
|
|
@@ -145,7 +175,7 @@
|
|
|
145
175
|
|
|
146
176
|
onChanges?.(snap);
|
|
147
177
|
if (onSuccessfullSave) await onSuccessfullSave(snap);
|
|
148
|
-
toast.success(`The record was successfully updated`);
|
|
178
|
+
if (!isRecordingMode) toast.success(`The record was successfully updated`);
|
|
149
179
|
onCancel?.();
|
|
150
180
|
}
|
|
151
181
|
</script>
|
|
@@ -170,7 +200,7 @@
|
|
|
170
200
|
</div>
|
|
171
201
|
</div>
|
|
172
202
|
<div class="flex-1 overflow-y-auto">
|
|
173
|
-
<DetailView {collectionName} bind:entry={values} {fieldsErrors} />
|
|
203
|
+
<DetailView {collectionName} bind:entry={values} {fieldsErrors} changedFields={Object.keys(localChanges.data)} />
|
|
174
204
|
{#if showRelatedRecords}
|
|
175
205
|
<UpdateDetailViewChildren {collectionName} entry={values} changes={localChanges} onChanges={(children) => { localChanges.children = children; }} />
|
|
176
206
|
{/if}
|
|
@@ -189,10 +219,11 @@
|
|
|
189
219
|
variant="default"
|
|
190
220
|
size="sm"
|
|
191
221
|
Icon={submitButton?.icon ? submitButton.icon : Pencil}
|
|
222
|
+
aria-label={submitButton?.text ?? "Update"}
|
|
192
223
|
onclick={handleSave}
|
|
193
224
|
disabled={!hasChanges}
|
|
194
225
|
>
|
|
195
|
-
{submitButton?.text ?
|
|
226
|
+
{submitButton?.text ?? "Update"}{totalChangeCount > 0 ? ` (${totalChangeCount})` : ''}
|
|
196
227
|
</Button>
|
|
197
228
|
</div>
|
|
198
229
|
</div>
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
class?: ButtonProps["class"];
|
|
12
12
|
Icon?: ButtonProps["Icon"];
|
|
13
13
|
children?: ButtonProps["children"];
|
|
14
|
+
"aria-label"?: string;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
let props: LocalProp = $props();
|
|
@@ -20,6 +21,11 @@
|
|
|
20
21
|
const { lobb } = getStudioContext();
|
|
21
22
|
|
|
22
23
|
async function openView() {
|
|
24
|
+
if (props.values) {
|
|
25
|
+
values = props.values;
|
|
26
|
+
open = true;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
23
29
|
const params = {
|
|
24
30
|
fields: "*",
|
|
25
31
|
filter: { id: props.recordId },
|
|
@@ -37,6 +43,7 @@
|
|
|
37
43
|
size={props.size}
|
|
38
44
|
class={props.class}
|
|
39
45
|
Icon={props.Icon}
|
|
46
|
+
aria-label={props["aria-label"]}
|
|
40
47
|
onclick={openView}
|
|
41
48
|
>
|
|
42
49
|
{#if props.children}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { fade } from "svelte/transition";
|
|
5
5
|
import { cubicOut } from "svelte/easing";
|
|
6
6
|
import Portal from "svelte-portal";
|
|
7
|
+
import { getContext, setContext, onDestroy } from "svelte";
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
9
10
|
children?: Snippet<[]>;
|
|
@@ -13,6 +14,16 @@
|
|
|
13
14
|
|
|
14
15
|
let { onHide, children, position = "side" }: Props = $props();
|
|
15
16
|
|
|
17
|
+
// Track nesting depth for stacking effect
|
|
18
|
+
const DEPTH_KEY = 'drawer-depth';
|
|
19
|
+
const parentDepth: number = getContext(DEPTH_KEY) ?? 0;
|
|
20
|
+
const depth = parentDepth + 1;
|
|
21
|
+
setContext(DEPTH_KEY, depth);
|
|
22
|
+
|
|
23
|
+
// Side drawers get narrower, bottom drawers get shorter — both offset by 48px per level
|
|
24
|
+
const sideWidth = $derived(calculateDrawerWidth() - (depth - 1) * 48);
|
|
25
|
+
const bottomOffset = $derived((depth - 1) * 48);
|
|
26
|
+
|
|
16
27
|
function slide(_node: Element, { duration = 250, axis }: { duration?: number; axis: "x" | "y" }) {
|
|
17
28
|
return {
|
|
18
29
|
duration,
|
|
@@ -36,11 +47,14 @@
|
|
|
36
47
|
></button>
|
|
37
48
|
|
|
38
49
|
<div
|
|
50
|
+
role="dialog"
|
|
39
51
|
transition:slide={{ axis: position === "bottom" ? "y" : "x" }}
|
|
40
52
|
class={position === "bottom"
|
|
41
|
-
? "fixed bottom-0 left-0 z-40 flex
|
|
53
|
+
? "fixed bottom-0 left-0 z-40 flex w-full flex-col border-t bg-card"
|
|
42
54
|
: "fixed right-0 top-0 z-40 flex h-full w-full flex-col border-l bg-card"}
|
|
43
|
-
style={position === "side"
|
|
55
|
+
style={position === "side"
|
|
56
|
+
? `max-width: ${sideWidth}px;`
|
|
57
|
+
: `height: calc(60vh - ${bottomOffset}px);`}
|
|
44
58
|
>
|
|
45
59
|
{@render children?.()}
|
|
46
60
|
</div>
|