@lobb-js/studio 0.41.0 → 0.42.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 +182 -50
- package/dist/components/dataTable/header.svelte +5 -2
- package/dist/components/dataTable/header.svelte.d.ts +1 -0
- package/dist/components/dataTable/table.svelte +10 -21
- package/dist/components/dataTable/table.svelte.d.ts +1 -0
- package/dist/components/detailView/create/createDetailView.svelte +43 -1
- package/dist/components/detailView/create/createDetailView.svelte.d.ts +1 -0
- package/dist/components/detailView/update/updateDetailView.svelte +39 -12
- 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 +1 -0
- package/dist/components/foreingKeyInput.svelte +27 -1
- package/dist/components/polymorphicInput.svelte +27 -3
- package/package.json +2 -2
- package/src/lib/components/confirmationDialog/confirmationDialog.svelte +1 -1
- package/src/lib/components/dataTable/dataTable.svelte +182 -50
- package/src/lib/components/dataTable/header.svelte +5 -2
- package/src/lib/components/dataTable/table.svelte +10 -21
- package/src/lib/components/detailView/create/createDetailView.svelte +43 -1
- package/src/lib/components/detailView/update/updateDetailView.svelte +39 -12
- package/src/lib/components/detailView/update/updateDetailViewButton.svelte +7 -0
- package/src/lib/components/drawer.svelte +1 -0
- package/src/lib/components/foreingKeyInput.svelte +27 -1
- package/src/lib/components/polymorphicInput.svelte +27 -3
|
@@ -14,7 +14,7 @@
|
|
|
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 } from "lucide-svelte";
|
|
18
18
|
import ListViewChildren from "./listViewChildren.svelte";
|
|
19
19
|
import FieldCell from "./fieldCell.svelte";
|
|
20
20
|
import Skeleton from "../ui/skeleton/skeleton.svelte";
|
|
@@ -76,37 +76,80 @@
|
|
|
76
76
|
let localChanges = $state<ChildrenChanges>(
|
|
77
77
|
untrack(() => changes) ?? { created: [], updated: [], deleted: [], linked: [], unlinked: [] }
|
|
78
78
|
);
|
|
79
|
+
// Counter for temporary IDs assigned to locally-created pending records so they
|
|
80
|
+
// can be individually targeted by handleDelete before being committed to the DB.
|
|
81
|
+
let nextTempId = -1;
|
|
79
82
|
|
|
80
83
|
|
|
81
84
|
// Derives the displayed rows by applying localChanges on top of server data.
|
|
85
|
+
// Deleted/unlinked rows stay visible but carry _recordingState so the table
|
|
86
|
+
// can render them with visual staging indicators.
|
|
82
87
|
const data = $derived.by(() => {
|
|
83
88
|
if (!isRecordingMode) return serverData;
|
|
84
89
|
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
const deletedIds = new Set(localChanges.deleted.map((r: any) => String(r.id)));
|
|
91
|
+
const unlinkedIds = new Set(localChanges.unlinked.map((r: any) => String(r.id)));
|
|
92
|
+
|
|
93
|
+
let result = serverData.map((r: any) => {
|
|
94
|
+
const id = String(r.id);
|
|
95
|
+
const update = localChanges.updated.find((u) => String(u.id) === id);
|
|
96
|
+
const hasUpdate = update && (
|
|
97
|
+
Object.keys(update.changes.data).length > 0 ||
|
|
98
|
+
Object.values(update.changes.children).some((ch: any) =>
|
|
99
|
+
ch.created.length || ch.updated.length || ch.deleted.length || ch.linked.length || ch.unlinked.length
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
let state: string | undefined;
|
|
104
|
+
if (deletedIds.has(id)) state = 'deleted';
|
|
105
|
+
else if (unlinkedIds.has(id)) state = 'unlinked';
|
|
106
|
+
else if (hasUpdate) state = 'updated';
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
...(hasUpdate ? { ...r, ...update!.changes.data } : r),
|
|
110
|
+
...(state ? { _recordingState: state } : {}),
|
|
111
|
+
};
|
|
95
112
|
});
|
|
96
113
|
|
|
97
114
|
for (const record of localChanges.linked) {
|
|
98
115
|
if (!result.some((r: any) => String(r.id) === String(record.id))) {
|
|
99
|
-
result = [...result, record];
|
|
116
|
+
result = [...result, { ...record, _recordingState: 'linked' }];
|
|
100
117
|
}
|
|
101
118
|
}
|
|
102
119
|
|
|
103
120
|
for (const item of localChanges.created) {
|
|
104
|
-
result = [...result, { ...item.data,
|
|
121
|
+
result = [...result, { ...item.data, id: (item as any)._tempId, _recordingState: 'created' }];
|
|
105
122
|
}
|
|
106
123
|
|
|
107
124
|
return result;
|
|
108
125
|
});
|
|
109
126
|
|
|
127
|
+
// IDs to exclude from the Link picker — records already present in this table
|
|
128
|
+
const excludeIds = $derived([
|
|
129
|
+
...serverData.map((r: any) => r.id),
|
|
130
|
+
...localChanges.linked.map((r: any) => r.id),
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
function onCellClass(entry: any, cellIndex: number): string {
|
|
134
|
+
if (!isRecordingMode) return '';
|
|
135
|
+
const state = entry._recordingState;
|
|
136
|
+
const border = cellIndex === 0 ? {
|
|
137
|
+
deleted: 'border-l-2 border-l-red-500',
|
|
138
|
+
unlinked: 'border-l-2 border-l-orange-500',
|
|
139
|
+
created: 'border-l-2 border-l-green-500',
|
|
140
|
+
linked: 'border-l-2 border-l-blue-500',
|
|
141
|
+
updated: 'border-l-2 border-l-amber-500',
|
|
142
|
+
}[state as string] ?? '' : '';
|
|
143
|
+
const bg: Record<string, string> = {
|
|
144
|
+
deleted: '!bg-red-500/5 opacity-50',
|
|
145
|
+
unlinked: '!bg-orange-500/5 opacity-50',
|
|
146
|
+
created: '!bg-green-500/5',
|
|
147
|
+
linked: '!bg-blue-500/5',
|
|
148
|
+
updated: '!bg-amber-500/5',
|
|
149
|
+
};
|
|
150
|
+
return `${bg[state as string] ?? ''} ${border}`.trim();
|
|
151
|
+
}
|
|
152
|
+
|
|
110
153
|
const hasRowActions = $derived(
|
|
111
154
|
loadExtensionComponents(ctx, "listView.entry.actions", undefined, { collectionName }).length > 0
|
|
112
155
|
);
|
|
@@ -204,16 +247,24 @@
|
|
|
204
247
|
}
|
|
205
248
|
|
|
206
249
|
async function handleDelete(entryId: string) {
|
|
207
|
-
|
|
208
|
-
|
|
250
|
+
if (!isRecordingMode) {
|
|
251
|
+
const result = await showDialog("Are you sure?", "This will permanently delete the record.");
|
|
252
|
+
if (!result) return;
|
|
253
|
+
}
|
|
209
254
|
if (isRecordingMode) {
|
|
210
255
|
// If the record was locally linked (not yet in DB), just cancel the link instead of marking for deletion.
|
|
211
256
|
const linkedIdx = localChanges.linked.findIndex((r: any) => String(r.id) === String(entryId));
|
|
212
257
|
if (linkedIdx !== -1) {
|
|
213
258
|
localChanges.linked.splice(linkedIdx, 1);
|
|
214
259
|
} else {
|
|
215
|
-
|
|
216
|
-
|
|
260
|
+
// If it's a locally-created pending record (has a negative tempId), remove it from created.
|
|
261
|
+
const createdIdx = localChanges.created.findIndex((item: any) => String(item._tempId) === String(entryId));
|
|
262
|
+
if (createdIdx !== -1) {
|
|
263
|
+
localChanges.created.splice(createdIdx, 1);
|
|
264
|
+
} else {
|
|
265
|
+
const record = serverData.find((r: any) => String(r.id) === String(entryId));
|
|
266
|
+
if (record) localChanges.deleted.push($state.snapshot(record));
|
|
267
|
+
}
|
|
217
268
|
}
|
|
218
269
|
onChanges?.($state.snapshot(localChanges));
|
|
219
270
|
} else if (parentContext) {
|
|
@@ -228,8 +279,10 @@
|
|
|
228
279
|
}
|
|
229
280
|
|
|
230
281
|
async function handleUnlink(entryId: string) {
|
|
231
|
-
|
|
232
|
-
|
|
282
|
+
if (!isRecordingMode) {
|
|
283
|
+
const result = await showDialog("Are you sure?", "This will unlink the record without deleting it.");
|
|
284
|
+
if (!result) return;
|
|
285
|
+
}
|
|
233
286
|
if (isRecordingMode) {
|
|
234
287
|
// If the record was locally linked this session, just cancel the link — net effect is no change.
|
|
235
288
|
const linkedIdx = localChanges.linked.findIndex((r: any) => String(r.id) === String(entryId));
|
|
@@ -265,14 +318,63 @@
|
|
|
265
318
|
});
|
|
266
319
|
|
|
267
320
|
function handleCreate(changes: import("../detailView/utils").Changes) {
|
|
268
|
-
localChanges.created.push({ data: changes.data });
|
|
321
|
+
(localChanges.created as any[]).push({ data: changes.data, _tempId: nextTempId-- });
|
|
322
|
+
onChanges?.($state.snapshot(localChanges));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const hasAnyChanges = $derived(
|
|
326
|
+
localChanges.created.length > 0 ||
|
|
327
|
+
localChanges.updated.length > 0 ||
|
|
328
|
+
localChanges.deleted.length > 0 ||
|
|
329
|
+
localChanges.linked.length > 0 ||
|
|
330
|
+
localChanges.unlinked.length > 0
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
function handleRevertRow(entryId: string) {
|
|
334
|
+
const id = String(entryId);
|
|
335
|
+
const createdIdx = localChanges.created.findIndex((item: any) => String(item._tempId) === id);
|
|
336
|
+
if (createdIdx !== -1) localChanges.created.splice(createdIdx, 1);
|
|
337
|
+
const linkedIdx = localChanges.linked.findIndex((r: any) => String(r.id) === id);
|
|
338
|
+
if (linkedIdx !== -1) localChanges.linked.splice(linkedIdx, 1);
|
|
339
|
+
const deletedIdx = localChanges.deleted.findIndex((r: any) => String(r.id) === id);
|
|
340
|
+
if (deletedIdx !== -1) localChanges.deleted.splice(deletedIdx, 1);
|
|
341
|
+
const unlinkedIdx = localChanges.unlinked.findIndex((r: any) => String(r.id) === id);
|
|
342
|
+
if (unlinkedIdx !== -1) localChanges.unlinked.splice(unlinkedIdx, 1);
|
|
343
|
+
const updatedIdx = localChanges.updated.findIndex((u: any) => String(u.id) === id);
|
|
344
|
+
if (updatedIdx !== -1) localChanges.updated.splice(updatedIdx, 1);
|
|
345
|
+
onChanges?.($state.snapshot(localChanges));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function handleRevertAll() {
|
|
349
|
+
localChanges.created = [];
|
|
350
|
+
localChanges.updated = [];
|
|
351
|
+
localChanges.deleted = [];
|
|
352
|
+
localChanges.linked = [];
|
|
353
|
+
localChanges.unlinked = [];
|
|
354
|
+
onChanges?.($state.snapshot(localChanges));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function handleEditPending(tempId: string, editChanges: import("../detailView/utils").Changes) {
|
|
358
|
+
const createdIdx = localChanges.created.findIndex((item: any) => String(item._tempId) === tempId);
|
|
359
|
+
if (createdIdx !== -1) {
|
|
360
|
+
localChanges.created[createdIdx] = {
|
|
361
|
+
...localChanges.created[createdIdx],
|
|
362
|
+
data: { ...localChanges.created[createdIdx].data, ...editChanges.data },
|
|
363
|
+
};
|
|
364
|
+
}
|
|
269
365
|
onChanges?.($state.snapshot(localChanges));
|
|
270
366
|
}
|
|
271
367
|
|
|
272
368
|
function handleUpdate(id: string, editChanges: import("../detailView/utils").Changes) {
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
369
|
+
const isEmpty = Object.keys(editChanges.data).length === 0 &&
|
|
370
|
+
Object.values(editChanges.children).every((ch: any) =>
|
|
371
|
+
!ch.created.length && !ch.updated.length && !ch.deleted.length && !ch.linked.length && !ch.unlinked.length
|
|
372
|
+
);
|
|
373
|
+
const existingIdx = localChanges.updated.findIndex((u) => String(u.id) === id);
|
|
374
|
+
if (isEmpty) {
|
|
375
|
+
if (existingIdx !== -1) localChanges.updated.splice(existingIdx, 1);
|
|
376
|
+
} else if (existingIdx !== -1) {
|
|
377
|
+
localChanges.updated[existingIdx].changes = editChanges;
|
|
276
378
|
} else {
|
|
277
379
|
localChanges.updated.push({ id, changes: editChanges });
|
|
278
380
|
}
|
|
@@ -306,9 +408,22 @@
|
|
|
306
408
|
{parentContext}
|
|
307
409
|
onLink={isRecordingMode ? handleLink : undefined}
|
|
308
410
|
onCreate={isRecordingMode ? handleCreate : undefined}
|
|
411
|
+
{excludeIds}
|
|
309
412
|
>
|
|
310
413
|
{#snippet left()}
|
|
311
414
|
{@render headerLeft?.()}
|
|
415
|
+
{#if isRecordingMode && hasAnyChanges}
|
|
416
|
+
<Button
|
|
417
|
+
class="h-6 px-2 font-normal text-xs text-muted-foreground"
|
|
418
|
+
variant="ghost"
|
|
419
|
+
size="sm"
|
|
420
|
+
Icon={RotateCcw}
|
|
421
|
+
onclick={handleRevertAll}
|
|
422
|
+
title="Revert all changes"
|
|
423
|
+
>
|
|
424
|
+
Revert all
|
|
425
|
+
</Button>
|
|
426
|
+
{/if}
|
|
312
427
|
{/snippet}
|
|
313
428
|
</Header>
|
|
314
429
|
{/if}
|
|
@@ -346,40 +461,57 @@
|
|
|
346
461
|
bind:selectedRecords
|
|
347
462
|
bind:tableWidth={dataTableWidth}
|
|
348
463
|
{...tableProps}
|
|
349
|
-
rowActions={hasRowActions ? rowActionsSnippet : undefined}
|
|
464
|
+
rowActions={hasRowActions ? rowActionsSnippet : undefined}
|
|
465
|
+
onCellClass={isRecordingMode ? onCellClass : undefined}>
|
|
350
466
|
{#snippet tools(entry)}
|
|
351
|
-
{#if
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
467
|
+
{#if entry._recordingState !== 'deleted' && entry._recordingState !== 'unlinked'}
|
|
468
|
+
{#if showEdit}
|
|
469
|
+
{@const isPending = entry._recordingState === 'created'}
|
|
470
|
+
<CanAccess collection={collectionName} action="update">
|
|
471
|
+
<UpdateDetailViewButton
|
|
472
|
+
{collectionName}
|
|
473
|
+
recordId={entry.id}
|
|
474
|
+
variant="ghost"
|
|
475
|
+
class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
|
|
476
|
+
Icon={Pencil}
|
|
477
|
+
aria-label={`Edit record ${entry.id}`}
|
|
478
|
+
values={isPending ? entry : undefined}
|
|
479
|
+
changes={isRecordingMode && !isPending ? localChanges.updated.find((u) => String(u.id) === String(entry.id))?.changes : undefined}
|
|
480
|
+
onChanges={isPending ? (c) => handleEditPending(String(entry.id), c) : isRecordingMode ? (c) => handleUpdate(String(entry.id), c) : undefined}
|
|
481
|
+
onSuccessfullSave={!isRecordingMode ? async () => { params = { ...params }; } : undefined}
|
|
482
|
+
></UpdateDetailViewButton>
|
|
483
|
+
</CanAccess>
|
|
484
|
+
{/if}
|
|
485
|
+
{#if parentContext}
|
|
486
|
+
<Button
|
|
487
|
+
class="h-6 w-6 text-muted-foreground hover:bg-transparent"
|
|
356
488
|
variant="ghost"
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
></Button>
|
|
489
|
+
size="icon"
|
|
490
|
+
onclick={() => handleUnlink(entry.id)}
|
|
491
|
+
Icon={Unlink}
|
|
492
|
+
title="Remove from this entry"
|
|
493
|
+
></Button>
|
|
494
|
+
{/if}
|
|
495
|
+
{#if showDelete}
|
|
496
|
+
<Button
|
|
497
|
+
class="h-6 w-6 text-muted-foreground hover:bg-transparent"
|
|
498
|
+
variant="ghost"
|
|
499
|
+
size="icon"
|
|
500
|
+
onclick={() => handleDelete(entry.id)}
|
|
501
|
+
Icon={Trash}
|
|
502
|
+
title="Delete permanently"
|
|
503
|
+
></Button>
|
|
504
|
+
{/if}
|
|
374
505
|
{/if}
|
|
375
|
-
{#if
|
|
506
|
+
{#if isRecordingMode && entry._recordingState}
|
|
376
507
|
<Button
|
|
377
508
|
class="h-6 w-6 text-muted-foreground hover:bg-transparent"
|
|
378
509
|
variant="ghost"
|
|
379
510
|
size="icon"
|
|
380
|
-
onclick={() =>
|
|
381
|
-
Icon={
|
|
382
|
-
title="
|
|
511
|
+
onclick={() => handleRevertRow(entry.id)}
|
|
512
|
+
Icon={RotateCcw}
|
|
513
|
+
title="Revert changes"
|
|
514
|
+
aria-label={`Revert changes for record ${entry.id}`}
|
|
383
515
|
></Button>
|
|
384
516
|
{/if}
|
|
385
517
|
{/snippet}
|
|
@@ -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>
|
|
@@ -12,6 +12,7 @@ interface Props {
|
|
|
12
12
|
showFilter?: boolean;
|
|
13
13
|
loading?: boolean;
|
|
14
14
|
left?: Snippet<[]>;
|
|
15
|
+
excludeIds?: (string | number)[];
|
|
15
16
|
}
|
|
16
17
|
declare const Header: import("svelte").Component<Props, {}, "selectedRecords" | "params">;
|
|
17
18
|
type Header = ReturnType<typeof Header>;
|
|
@@ -41,6 +41,9 @@
|
|
|
41
41
|
parentWidth?: number;
|
|
42
42
|
select?: Select;
|
|
43
43
|
tableWidth?: number;
|
|
44
|
+
|
|
45
|
+
// recording mode row visuals — cellIndex 0 = tools cell, 1+ = data/action cells
|
|
46
|
+
onCellClass?: (entry: Entry, cellIndex: number) => string;
|
|
44
47
|
}
|
|
45
48
|
</script>
|
|
46
49
|
|
|
@@ -79,6 +82,7 @@
|
|
|
79
82
|
collapsible,
|
|
80
83
|
select,
|
|
81
84
|
tableWidth = $bindable(),
|
|
85
|
+
onCellClass,
|
|
82
86
|
}: TableProps = $props();
|
|
83
87
|
|
|
84
88
|
let expandedRows: boolean[] = $state(new Array(data.length).fill(false));
|
|
@@ -183,7 +187,7 @@
|
|
|
183
187
|
flex items-center p-2.5 text-xs h-10
|
|
184
188
|
border-r border-b gap-2
|
|
185
189
|
{headerBorderTop ? 'border-t' : ''}
|
|
186
|
-
bg-muted
|
|
190
|
+
bg-muted/50
|
|
187
191
|
"
|
|
188
192
|
>
|
|
189
193
|
<!-- collapsable toggle -->
|
|
@@ -208,7 +212,7 @@
|
|
|
208
212
|
class="
|
|
209
213
|
sticky top-0 z-10
|
|
210
214
|
flex items-center p-2.5 text-xs h-10
|
|
211
|
-
bg-muted
|
|
215
|
+
bg-muted/50
|
|
212
216
|
{lastColumn && !showLastColumnBorder ? '' : 'border-r'}
|
|
213
217
|
border-b gap-2
|
|
214
218
|
{headerBorderTop ? 'border-t' : ''}
|
|
@@ -235,7 +239,7 @@
|
|
|
235
239
|
class="
|
|
236
240
|
sticky top-0 right-0 z-20
|
|
237
241
|
flex items-center p-2.5 h-10
|
|
238
|
-
bg-muted
|
|
242
|
+
bg-muted/50
|
|
239
243
|
border-l border-b
|
|
240
244
|
{headerBorderTop ? 'border-t' : ''}
|
|
241
245
|
"
|
|
@@ -247,12 +251,7 @@
|
|
|
247
251
|
{@const lastRow = data.length - 1 === index}
|
|
248
252
|
{#if selectedRecords || tools}
|
|
249
253
|
<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
|
-
"
|
|
254
|
+
class="sticky left-0 flex items-center p-2.5 text-xs h-10 bg-card border-r gap-2 {onCellClass?.(entry, 0) ?? ''}"
|
|
256
255
|
>
|
|
257
256
|
<!-- collapsable toggle -->
|
|
258
257
|
{#if showCollapsible}
|
|
@@ -294,12 +293,7 @@
|
|
|
294
293
|
onclick={() => {
|
|
295
294
|
select?.onSelect(entry);
|
|
296
295
|
}}
|
|
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
|
-
"
|
|
296
|
+
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
297
|
>
|
|
304
298
|
{#if overrideCell}
|
|
305
299
|
{@render overrideCell(fieldValue, column, entry)}
|
|
@@ -310,12 +304,7 @@
|
|
|
310
304
|
{/each}
|
|
311
305
|
{#if rowActions}
|
|
312
306
|
<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
|
-
"
|
|
307
|
+
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
308
|
>
|
|
320
309
|
{@render rowActions?.(entry, index)}
|
|
321
310
|
</div>
|
|
@@ -27,6 +27,7 @@ export interface TableProps {
|
|
|
27
27
|
parentWidth?: number;
|
|
28
28
|
select?: Select;
|
|
29
29
|
tableWidth?: number;
|
|
30
|
+
onCellClass?: (entry: Entry, cellIndex: number) => string;
|
|
30
31
|
}
|
|
31
32
|
import type { Snippet } from "svelte";
|
|
32
33
|
declare const Table: import("svelte").Component<TableProps, {}, "sort" | "selectedRecords" | "tableWidth">;
|
|
@@ -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,31 @@
|
|
|
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
|
+
);
|
|
66
|
+
|
|
67
|
+
const changeSummaryLines = $derived.by(() => {
|
|
68
|
+
const lines: string[] = [];
|
|
69
|
+
const fieldCount = Object.keys(changes.data).length;
|
|
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
|
+
});
|
|
77
|
+
|
|
49
78
|
const fieldNames = Object.keys(ctx.meta.collections[collectionName].fields);
|
|
50
79
|
let values = $state(getDefaultEntry(ctx, fieldNames, collectionName, passedValues));
|
|
51
80
|
let fieldsErrors: Record<string, any> = $state({});
|
|
@@ -82,17 +111,27 @@
|
|
|
82
111
|
}
|
|
83
112
|
|
|
84
113
|
async function handleSave() {
|
|
114
|
+
if (!isRecordingMode && hasChildChanges && changeSummaryLines.length > 0) {
|
|
115
|
+
const confirmed = await showDialog(
|
|
116
|
+
"Confirm changes",
|
|
117
|
+
changeSummaryLines.map(l => `• ${l}`).join('\n')
|
|
118
|
+
);
|
|
119
|
+
if (!confirmed) return;
|
|
120
|
+
}
|
|
121
|
+
|
|
85
122
|
const snap = $state.snapshot(changes);
|
|
86
123
|
const response = await lobb.createOne(collectionName, buildPayload(snap), undefined, isRecordingMode);
|
|
87
124
|
|
|
88
125
|
if (response.status === 204) {
|
|
89
126
|
onChanges?.(snap);
|
|
90
127
|
if (onSuccessfullSave) await onSuccessfullSave(snap);
|
|
128
|
+
await onCreated?.(snap.data);
|
|
91
129
|
toast.success(`The record was successfully created`);
|
|
92
130
|
handleCancel();
|
|
93
131
|
return;
|
|
94
132
|
}
|
|
95
133
|
|
|
134
|
+
let createdRecord: any = null;
|
|
96
135
|
if (!response.bodyUsed) {
|
|
97
136
|
const result = await response.json();
|
|
98
137
|
if (response.status >= 400) {
|
|
@@ -104,10 +143,12 @@
|
|
|
104
143
|
return;
|
|
105
144
|
}
|
|
106
145
|
}
|
|
146
|
+
createdRecord = result.data ?? result;
|
|
107
147
|
}
|
|
108
148
|
|
|
109
149
|
onChanges?.(snap);
|
|
110
150
|
if (onSuccessfullSave) await onSuccessfullSave(snap);
|
|
151
|
+
await onCreated?.(createdRecord ?? snap.data);
|
|
111
152
|
toast.success(`The record was successfully created`);
|
|
112
153
|
onCancel?.();
|
|
113
154
|
}
|
|
@@ -152,9 +193,10 @@
|
|
|
152
193
|
variant="default"
|
|
153
194
|
size="sm"
|
|
154
195
|
Icon={submitButton?.icon ? submitButton.icon : Plus}
|
|
196
|
+
aria-label={submitButton?.text ?? "Create record"}
|
|
155
197
|
onclick={handleSave}
|
|
156
198
|
>
|
|
157
|
-
{submitButton?.text ?
|
|
199
|
+
{submitButton?.text ?? "Create"}{totalChangeCount > 0 ? ` (${totalChangeCount})` : ''}
|
|
158
200
|
</Button>
|
|
159
201
|
</div>
|
|
160
202
|
</div>
|
|
@@ -11,6 +11,7 @@ export interface CreateDetailViewProp {
|
|
|
11
11
|
submitButton?: SubmitButton;
|
|
12
12
|
title?: Snippet<[string]>;
|
|
13
13
|
onSuccessfullSave?: (entry: any) => Promise<void>;
|
|
14
|
+
onCreated?: (record: any) => Promise<void>;
|
|
14
15
|
onCancel?: () => Promise<void>;
|
|
15
16
|
onChanges?: (changes: Changes) => void;
|
|
16
17
|
}
|