@lobb-js/studio 0.30.0 → 0.32.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/actions.d.ts +2 -0
- package/dist/components/Studio.svelte +1 -10
- package/dist/components/dataTable/dataTable.svelte +104 -39
- package/dist/components/dataTable/dataTable.svelte.d.ts +4 -1
- package/dist/components/dataTable/fieldCell.svelte +7 -4
- package/dist/components/dataTable/fieldCell.svelte.d.ts +2 -2
- package/dist/components/dataTable/filter.svelte +0 -15
- package/dist/components/dataTable/header.svelte +13 -14
- package/dist/components/dataTable/header.svelte.d.ts +3 -2
- package/dist/components/dataTable/numberCell.svelte +28 -0
- package/dist/components/dataTable/numberCell.svelte.d.ts +7 -0
- package/dist/components/dataTable/polymorphicFieldCell.svelte +3 -3
- package/dist/components/dataTable/polymorphicFieldCell.svelte.d.ts +2 -2
- package/dist/components/dataTablePopup/dataTablePopup.svelte +17 -0
- package/dist/components/dataTablePopup/dataTablePopup.svelte.d.ts +2 -0
- package/dist/components/detailView/create/createDetailView.svelte +28 -54
- package/dist/components/detailView/create/createDetailView.svelte.d.ts +4 -3
- package/dist/components/detailView/create/createDetailViewChildren.svelte +113 -0
- package/dist/components/detailView/create/createDetailViewChildren.svelte.d.ts +9 -0
- package/dist/components/detailView/create/createManyView.svelte +2 -2
- package/dist/components/detailView/detailView.svelte +6 -1
- package/dist/components/detailView/fieldInput.svelte +7 -5
- package/dist/components/detailView/update/updateDetailView.svelte +46 -40
- package/dist/components/detailView/update/updateDetailView.svelte.d.ts +5 -3
- package/dist/components/detailView/update/updateDetailViewButton.svelte +0 -1
- package/dist/components/detailView/update/updateDetailViewChildren.svelte +122 -0
- package/dist/components/detailView/update/updateDetailViewChildren.svelte.d.ts +10 -0
- package/dist/components/detailView/utils.d.ts +1 -2
- package/dist/components/importButton.svelte +1 -1
- package/dist/components/miniSidebar.svelte +6 -3
- package/dist/components/richTextEditor.svelte +2 -0
- package/dist/components/routes/extensions/extension.svelte +1 -1
- package/dist/components/routes/home.svelte +35 -21
- package/dist/components/ui/input/numberInput.svelte +104 -0
- package/dist/components/ui/input/numberInput.svelte.d.ts +9 -0
- package/dist/components/workflowEditor.svelte +6 -4
- package/package.json +4 -3
- package/src/lib/actions.ts +2 -0
- package/src/lib/components/Studio.svelte +1 -10
- package/src/lib/components/dataTable/dataTable.svelte +104 -39
- package/src/lib/components/dataTable/fieldCell.svelte +7 -4
- package/src/lib/components/dataTable/filter.svelte +0 -15
- package/src/lib/components/dataTable/header.svelte +13 -14
- package/src/lib/components/dataTable/numberCell.svelte +28 -0
- package/src/lib/components/dataTable/polymorphicFieldCell.svelte +3 -3
- package/src/lib/components/dataTablePopup/dataTablePopup.svelte +17 -0
- package/src/lib/components/detailView/create/createDetailView.svelte +28 -54
- package/src/lib/components/detailView/create/createDetailViewChildren.svelte +113 -0
- package/src/lib/components/detailView/create/createManyView.svelte +2 -2
- package/src/lib/components/detailView/detailView.svelte +6 -1
- package/src/lib/components/detailView/fieldInput.svelte +7 -5
- package/src/lib/components/detailView/update/updateDetailView.svelte +46 -40
- package/src/lib/components/detailView/update/updateDetailViewButton.svelte +0 -1
- package/src/lib/components/detailView/update/updateDetailViewChildren.svelte +122 -0
- package/src/lib/components/detailView/utils.ts +1 -1
- package/src/lib/components/importButton.svelte +1 -1
- package/src/lib/components/miniSidebar.svelte +6 -3
- package/src/lib/components/richTextEditor.svelte +2 -0
- package/src/lib/components/routes/extensions/extension.svelte +1 -1
- package/src/lib/components/routes/home.svelte +35 -21
- package/src/lib/components/ui/input/numberInput.svelte +104 -0
- package/src/lib/components/workflowEditor.svelte +6 -4
- package/dist/components/breadCrumbs.svelte +0 -61
- package/dist/components/breadCrumbs.svelte.d.ts +0 -3
- package/dist/components/detailView/update/detailViewChildren.svelte +0 -61
- package/dist/components/detailView/update/detailViewChildren.svelte.d.ts +0 -9
- package/dist/components/header.svelte +0 -45
- package/dist/components/header.svelte.d.ts +0 -6
- package/src/lib/components/breadCrumbs.svelte +0 -61
- package/src/lib/components/detailView/update/detailViewChildren.svelte +0 -61
- package/src/lib/components/header.svelte +0 -45
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
import { showDialog } from "../../actions";
|
|
22
22
|
import UpdateDetailViewButton from "../detailView/update/updateDetailViewButton.svelte";
|
|
23
23
|
import type { Snippet } from "svelte";
|
|
24
|
-
import type {
|
|
24
|
+
import type { ChildrenChanges } from "../detailView/utils";
|
|
25
25
|
import ExtensionsComponents from "../extensionsComponents.svelte";
|
|
26
26
|
import { getExtensionUtils, loadExtensionComponents } from "../../extensions/extensionUtils";
|
|
27
27
|
import { emitEvent } from "../../eventSystem";
|
|
28
|
-
import { onMount } from "svelte";
|
|
28
|
+
import { onMount, untrack } from "svelte";
|
|
29
29
|
import Tabs from "./dataTableTabs.svelte";
|
|
30
30
|
import { fade } from "svelte/transition";
|
|
31
31
|
import type { CollectionTab } from "../../store.types";
|
|
@@ -37,11 +37,14 @@
|
|
|
37
37
|
filter?: any;
|
|
38
38
|
searchParams?: Record<string, any>;
|
|
39
39
|
parentContext?: ParentContext;
|
|
40
|
+
onChanges?: (changes: ChildrenChanges) => void;
|
|
40
41
|
changes?: ChildrenChanges;
|
|
41
42
|
showHeader?: boolean;
|
|
42
43
|
showFooter?: boolean;
|
|
43
44
|
showImport?: boolean;
|
|
44
45
|
showDelete?: boolean;
|
|
46
|
+
showEdit?: boolean;
|
|
47
|
+
onDataLoad?: (total: number) => void;
|
|
45
48
|
tableProps?: Partial<TableProps>;
|
|
46
49
|
tabs?: CollectionTab[];
|
|
47
50
|
headerLeft?: Snippet<[]>;
|
|
@@ -52,16 +55,24 @@
|
|
|
52
55
|
filter,
|
|
53
56
|
searchParams,
|
|
54
57
|
parentContext,
|
|
55
|
-
|
|
58
|
+
onChanges,
|
|
59
|
+
changes,
|
|
56
60
|
showHeader = true,
|
|
57
61
|
showFooter = true,
|
|
58
62
|
showImport = true,
|
|
59
63
|
showDelete = false,
|
|
64
|
+
showEdit = true,
|
|
65
|
+
onDataLoad,
|
|
60
66
|
tableProps,
|
|
61
67
|
tabs,
|
|
62
68
|
headerLeft,
|
|
63
69
|
}: Props = $props();
|
|
64
70
|
|
|
71
|
+
const isRecordingMode = onChanges !== undefined;
|
|
72
|
+
let localChanges = $state<ChildrenChanges>(
|
|
73
|
+
untrack(() => changes) ?? { created: [], updated: [], deleted: [], linked: [], unlinked: [] }
|
|
74
|
+
);
|
|
75
|
+
|
|
65
76
|
// Gate row/header buttons by the current user's permissions:
|
|
66
77
|
// - showUpdate → per-row edit button
|
|
67
78
|
// - showCreate → header's Create + Import buttons (passed to Header)
|
|
@@ -76,40 +87,29 @@
|
|
|
76
87
|
showCreate = create === true;
|
|
77
88
|
});
|
|
78
89
|
|
|
79
|
-
|
|
80
|
-
if (!changes) return undefined;
|
|
81
|
-
let slot = changes.updated.find((u) => String(u.id) === String(recordId));
|
|
82
|
-
if (!slot) {
|
|
83
|
-
slot = { id: recordId, data: {}, children: {} };
|
|
84
|
-
changes.updated.push(slot);
|
|
85
|
-
}
|
|
86
|
-
return slot;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Derives the displayed rows by applying changes on top of server data.
|
|
90
|
-
// This is the single place responsible for optimistic UI — no handler touches data directly.
|
|
90
|
+
// Derives the displayed rows by applying localChanges on top of server data.
|
|
91
91
|
const data = $derived.by(() => {
|
|
92
|
-
if (!
|
|
92
|
+
if (!isRecordingMode) return serverData;
|
|
93
93
|
|
|
94
94
|
const removedIds = new Set([
|
|
95
|
-
...
|
|
96
|
-
...
|
|
95
|
+
...localChanges.deleted.map((r: any) => String(r.id)),
|
|
96
|
+
...localChanges.unlinked.map((r: any) => String(r.id)),
|
|
97
97
|
]);
|
|
98
98
|
|
|
99
99
|
let result = serverData.filter((r: any) => !removedIds.has(String(r.id)));
|
|
100
100
|
|
|
101
101
|
result = result.map((r: any) => {
|
|
102
|
-
const update =
|
|
103
|
-
return update && Object.keys(update.data).length ? { ...r, ...update.data } : r;
|
|
102
|
+
const update = localChanges.updated.find((u) => String(u.id) === String(r.id));
|
|
103
|
+
return update && Object.keys(update.changes.data).length ? { ...r, ...update.changes.data } : r;
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
for (const record of
|
|
106
|
+
for (const record of localChanges.linked) {
|
|
107
107
|
if (!result.some((r: any) => String(r.id) === String(record.id))) {
|
|
108
108
|
result = [...result, record];
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
for (const item of
|
|
112
|
+
for (const item of localChanges.created) {
|
|
113
113
|
result = [...result, { ...item.data, _pending: true }];
|
|
114
114
|
}
|
|
115
115
|
|
|
@@ -122,10 +122,28 @@
|
|
|
122
122
|
|
|
123
123
|
let activeTabFilter = $state<any>(undefined);
|
|
124
124
|
|
|
125
|
+
// Canonicalize the incoming filter so values like `{ status: "Open" }`
|
|
126
|
+
// become `{ status: { $eq: "Open" } }`. The Filter UI and the server
|
|
127
|
+
// both expect operator objects, so doing this once at the boundary
|
|
128
|
+
// keeps the rest of the data flow uniform.
|
|
129
|
+
function normalizeFilter(f: Record<string, any> | undefined) {
|
|
130
|
+
const out: Record<string, any> = {};
|
|
131
|
+
for (const [key, value] of Object.entries(f ?? {})) {
|
|
132
|
+
if (key === "$and" || key === "$or") {
|
|
133
|
+
out[key] = value;
|
|
134
|
+
} else if (!_.isPlainObject(value)) {
|
|
135
|
+
out[key] = { $eq: value };
|
|
136
|
+
} else {
|
|
137
|
+
out[key] = value;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
125
143
|
const fields = getCollectionParamsFields(ctx, collectionName);
|
|
126
144
|
let params = $state({
|
|
127
145
|
fields: fields,
|
|
128
|
-
filter: { ...filter, ...activeTabFilter },
|
|
146
|
+
filter: { ...normalizeFilter(filter), ...activeTabFilter },
|
|
129
147
|
sort: {},
|
|
130
148
|
limit: "100",
|
|
131
149
|
page: 1,
|
|
@@ -134,7 +152,7 @@
|
|
|
134
152
|
|
|
135
153
|
$effect(() => {
|
|
136
154
|
const tabFilter = activeTabFilter;
|
|
137
|
-
params.filter = { ...filter, ...tabFilter };
|
|
155
|
+
params.filter = { ...normalizeFilter(filter), ...tabFilter };
|
|
138
156
|
});
|
|
139
157
|
|
|
140
158
|
let selectedRecords = $state([]);
|
|
@@ -171,18 +189,26 @@
|
|
|
171
189
|
const res = await response.json();
|
|
172
190
|
serverData = res.data;
|
|
173
191
|
totalCount = res.meta.totalCount;
|
|
192
|
+
onDataLoad?.(totalCount);
|
|
174
193
|
loading = false;
|
|
175
194
|
}
|
|
176
195
|
|
|
177
196
|
async function handleDelete(entryId: string) {
|
|
178
197
|
const result = await showDialog("Are you sure?", "This will permanently delete the record.");
|
|
179
198
|
if (!result) return;
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
199
|
+
if (isRecordingMode) {
|
|
200
|
+
// If the record was locally linked (not yet in DB), just cancel the link instead of marking for deletion.
|
|
201
|
+
const linkedIdx = localChanges.linked.findIndex((r: any) => String(r.id) === String(entryId));
|
|
202
|
+
if (linkedIdx !== -1) {
|
|
203
|
+
localChanges.linked.splice(linkedIdx, 1);
|
|
204
|
+
} else {
|
|
205
|
+
const record = serverData.find((r: any) => String(r.id) === String(entryId));
|
|
206
|
+
if (record) localChanges.deleted.push($state.snapshot(record));
|
|
207
|
+
}
|
|
208
|
+
onChanges?.($state.snapshot(localChanges));
|
|
183
209
|
} else if (parentContext) {
|
|
184
210
|
serverData = serverData.filter((r: any) => String(r.id) !== String(entryId));
|
|
185
|
-
await lobb.updateOne(parentContext.collectionName, String(parentContext.recordId), {}, { [collectionName]: { delete: [entryId] } });
|
|
211
|
+
await lobb.updateOne(parentContext.collectionName, String(parentContext.recordId), { data: {}, children: { [collectionName]: { delete: [entryId] } } });
|
|
186
212
|
params = { ...params };
|
|
187
213
|
} else {
|
|
188
214
|
serverData = serverData.filter((r: any) => String(r.id) !== String(entryId));
|
|
@@ -194,16 +220,55 @@
|
|
|
194
220
|
async function handleUnlink(entryId: string) {
|
|
195
221
|
const result = await showDialog("Are you sure?", "This will unlink the record without deleting it.");
|
|
196
222
|
if (!result) return;
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
|
|
223
|
+
if (isRecordingMode) {
|
|
224
|
+
// If the record was locally linked this session, just cancel the link — net effect is no change.
|
|
225
|
+
const linkedIdx = localChanges.linked.findIndex((r: any) => String(r.id) === String(entryId));
|
|
226
|
+
if (linkedIdx !== -1) {
|
|
227
|
+
localChanges.linked.splice(linkedIdx, 1);
|
|
228
|
+
} else {
|
|
229
|
+
const record = serverData.find((r: any) => String(r.id) === String(entryId));
|
|
230
|
+
if (record) localChanges.unlinked.push($state.snapshot(record));
|
|
231
|
+
}
|
|
232
|
+
onChanges?.($state.snapshot(localChanges));
|
|
200
233
|
} else {
|
|
201
234
|
serverData = serverData.filter((r: any) => String(r.id) !== String(entryId));
|
|
202
|
-
await lobb.updateOne(parentContext!.collectionName, String(parentContext!.recordId), {}, { [collectionName]: { unlink: [entryId] } });
|
|
235
|
+
await lobb.updateOne(parentContext!.collectionName, String(parentContext!.recordId), { data: {}, children: { [collectionName]: { unlink: [entryId] } } });
|
|
203
236
|
params = { ...params };
|
|
204
237
|
}
|
|
205
238
|
}
|
|
206
239
|
|
|
240
|
+
function handleLink(record: any) {
|
|
241
|
+
// If the record was locally unlinked this session, just cancel the unlink — net effect is no change.
|
|
242
|
+
const unlinkedIdx = localChanges.unlinked.findIndex((r: any) => String(r.id) === String(record.id));
|
|
243
|
+
if (unlinkedIdx !== -1) {
|
|
244
|
+
localChanges.unlinked.splice(unlinkedIdx, 1);
|
|
245
|
+
} else if (!localChanges.linked.some((r: any) => String(r.id) === String(record.id))) {
|
|
246
|
+
localChanges.linked.push(record);
|
|
247
|
+
}
|
|
248
|
+
onChanges?.($state.snapshot(localChanges));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
$effect(() => {
|
|
252
|
+
if (isRecordingMode) {
|
|
253
|
+
console.log(`[DataTable:${collectionName}] localChanges:`, $state.snapshot(localChanges));
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
function handleCreate(changes: import("../detailView/utils").Changes) {
|
|
258
|
+
localChanges.created.push({ data: changes.data });
|
|
259
|
+
onChanges?.($state.snapshot(localChanges));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function handleUpdate(id: string, editChanges: import("../detailView/utils").Changes) {
|
|
263
|
+
const existing = localChanges.updated.find((u) => String(u.id) === id);
|
|
264
|
+
if (existing) {
|
|
265
|
+
existing.changes = editChanges;
|
|
266
|
+
} else {
|
|
267
|
+
localChanges.updated.push({ id, changes: editChanges });
|
|
268
|
+
}
|
|
269
|
+
onChanges?.($state.snapshot(localChanges));
|
|
270
|
+
}
|
|
271
|
+
|
|
207
272
|
</script>
|
|
208
273
|
|
|
209
274
|
<div
|
|
@@ -228,7 +293,8 @@
|
|
|
228
293
|
{showImport}
|
|
229
294
|
{showCreate}
|
|
230
295
|
{parentContext}
|
|
231
|
-
{
|
|
296
|
+
onLink={isRecordingMode ? handleLink : undefined}
|
|
297
|
+
onCreate={isRecordingMode ? handleCreate : undefined}
|
|
232
298
|
>
|
|
233
299
|
{#snippet left()}
|
|
234
300
|
{@render headerLeft?.()}
|
|
@@ -259,17 +325,16 @@
|
|
|
259
325
|
{...tableProps}
|
|
260
326
|
rowActions={hasRowActions ? rowActionsSnippet : undefined}>
|
|
261
327
|
{#snippet tools(entry)}
|
|
262
|
-
{#if showUpdate}
|
|
328
|
+
{#if showUpdate && showEdit}
|
|
263
329
|
<UpdateDetailViewButton
|
|
264
330
|
{collectionName}
|
|
265
331
|
recordId={entry.id}
|
|
266
332
|
variant="ghost"
|
|
267
333
|
class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
|
|
268
334
|
Icon={Pencil}
|
|
269
|
-
changes={
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}}
|
|
335
|
+
changes={isRecordingMode ? localChanges.updated.find((u) => String(u.id) === String(entry.id))?.changes : undefined}
|
|
336
|
+
onChanges={isRecordingMode ? (c) => handleUpdate(String(entry.id), c) : undefined}
|
|
337
|
+
onSuccessfullSave={!isRecordingMode ? async () => { params = { ...params }; } : undefined}
|
|
273
338
|
></UpdateDetailViewButton>
|
|
274
339
|
{/if}
|
|
275
340
|
{#if parentContext}
|
|
@@ -299,7 +364,7 @@
|
|
|
299
364
|
fieldName={column.id}
|
|
300
365
|
{value}
|
|
301
366
|
{entry}
|
|
302
|
-
|
|
367
|
+
refresh={() => { params = { ...params }; }}
|
|
303
368
|
/>
|
|
304
369
|
{/snippet}
|
|
305
370
|
{#snippet collapsible(entry)}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { getStudioContext } from "../../context";
|
|
9
9
|
import ExtensionsComponents from "../extensionsComponents.svelte";
|
|
10
10
|
import { getExtensionUtils } from "../../extensions/extensionUtils";
|
|
11
|
+
import NumberCell from "./numberCell.svelte";
|
|
11
12
|
|
|
12
13
|
const { ctx, lobb } = getStudioContext();
|
|
13
14
|
|
|
@@ -16,7 +17,7 @@
|
|
|
16
17
|
fieldName: string;
|
|
17
18
|
value: any;
|
|
18
19
|
entry: Record<string, any>;
|
|
19
|
-
|
|
20
|
+
refresh?: () => void;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
let {
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
fieldName,
|
|
25
26
|
value,
|
|
26
27
|
entry,
|
|
27
|
-
|
|
28
|
+
refresh,
|
|
28
29
|
}: Props = $props();
|
|
29
30
|
|
|
30
31
|
const field = getField(ctx, fieldName, collectionName);
|
|
@@ -48,7 +49,7 @@
|
|
|
48
49
|
collectionField={polymorphicRelation.from.collection_field}
|
|
49
50
|
idField={polymorphicRelation.from.id_field}
|
|
50
51
|
{entry}
|
|
51
|
-
|
|
52
|
+
{refresh}
|
|
52
53
|
/>
|
|
53
54
|
{:else if isRefrenceField}
|
|
54
55
|
{#if value?.id && value.id !== 0}
|
|
@@ -66,7 +67,7 @@
|
|
|
66
67
|
class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
|
|
67
68
|
Icon={ExternalLink}
|
|
68
69
|
onSuccessfullSave={async () => {
|
|
69
|
-
|
|
70
|
+
refresh?.();
|
|
70
71
|
}}
|
|
71
72
|
/>
|
|
72
73
|
</div>
|
|
@@ -92,6 +93,8 @@
|
|
|
92
93
|
<div>{date}</div>
|
|
93
94
|
{:else if field.type === "time"}
|
|
94
95
|
<div>{value}</div>
|
|
96
|
+
{:else if field.type === "integer" || field.type === "long" || field.type === "decimal" || field.type === "float"}
|
|
97
|
+
<NumberCell {value} groupDigits={field.ui?.groupDigits ?? false} />
|
|
95
98
|
{:else}
|
|
96
99
|
{value}
|
|
97
100
|
{/if}
|
|
@@ -33,21 +33,6 @@
|
|
|
33
33
|
let firstPopover = $state(false);
|
|
34
34
|
let secondPopover = $state(false);
|
|
35
35
|
|
|
36
|
-
$effect.pre(() => {
|
|
37
|
-
// convert direct values to { $eq: (value) }
|
|
38
|
-
for (let index = 0; index < Object.keys(filter).length; index++) {
|
|
39
|
-
const key = Object.keys(filter)[index];
|
|
40
|
-
const value = filter[key];
|
|
41
|
-
if (key !== "$and" && key !== "$or") {
|
|
42
|
-
if (!_.isPlainObject(value)) {
|
|
43
|
-
filter[key] = {
|
|
44
|
-
$eq: value,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
36
|
function groupAddingHandler(filter: any, key: string) {
|
|
52
37
|
if (key === "$and" || key === "$or") {
|
|
53
38
|
filter[key] = [];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getStudioContext } from "../../context";
|
|
3
|
-
import type { Changes
|
|
3
|
+
import type { Changes } from "../detailView/utils";
|
|
4
4
|
import type { ParentContext } from "./dataTable.svelte";
|
|
5
5
|
import { Download, ListRestart, Plus, Trash, Link } from "lucide-svelte";
|
|
6
6
|
import * as Tooltip from "../ui/tooltip";
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
params: any;
|
|
24
24
|
selectedRecords: string[];
|
|
25
25
|
parentContext?: ParentContext;
|
|
26
|
-
|
|
26
|
+
onLink?: (record: any) => void;
|
|
27
|
+
onCreate?: (changes: Changes) => void;
|
|
27
28
|
showImport?: boolean;
|
|
28
29
|
showCreate?: boolean;
|
|
29
30
|
left?: Snippet<[]>;
|
|
@@ -34,27 +35,25 @@
|
|
|
34
35
|
params = $bindable(),
|
|
35
36
|
selectedRecords = $bindable(),
|
|
36
37
|
parentContext,
|
|
37
|
-
|
|
38
|
+
onLink,
|
|
39
|
+
onCreate,
|
|
38
40
|
showImport = true,
|
|
39
41
|
showCreate = false,
|
|
40
42
|
left
|
|
41
43
|
}: Props = $props();
|
|
42
44
|
|
|
43
|
-
// Local changes object for the create form (recording mode only)
|
|
44
|
-
let createChanges = $state<Changes>({ data: {}, children: {} });
|
|
45
|
-
|
|
46
45
|
function handleLink(record: any) {
|
|
47
|
-
if (
|
|
48
|
-
|
|
46
|
+
if (onLink) {
|
|
47
|
+
onLink(record);
|
|
49
48
|
} else if (parentContext) {
|
|
50
|
-
lobb.updateOne(parentContext.collectionName, String(parentContext.recordId), {}, { [collectionName]: { link: [record.id] } });
|
|
49
|
+
lobb.updateOne(parentContext.collectionName, String(parentContext.recordId), { data: {}, children: { [collectionName]: { link: [record.id] } } });
|
|
51
50
|
resetTable();
|
|
52
51
|
}
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
async function
|
|
56
|
-
if (
|
|
57
|
-
|
|
54
|
+
async function handleCreate(snap: any) {
|
|
55
|
+
if (onCreate) {
|
|
56
|
+
onCreate(snap);
|
|
58
57
|
} else {
|
|
59
58
|
resetTable();
|
|
60
59
|
}
|
|
@@ -208,8 +207,8 @@
|
|
|
208
207
|
variant="default"
|
|
209
208
|
class="h-7 px-3 text-xs font-normal"
|
|
210
209
|
Icon={Plus}
|
|
211
|
-
|
|
212
|
-
onSuccessfullSave={
|
|
210
|
+
onChanges={onCreate ? handleCreate : undefined}
|
|
211
|
+
onSuccessfullSave={onCreate ? undefined : handleCreate}
|
|
213
212
|
>
|
|
214
213
|
{headerIsSmall ? "" : "Create"}
|
|
215
214
|
</CreateDetailViewButton>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Matches the input mask in numberInput.svelte: space-grouped thousands,
|
|
3
|
+
// dot decimal (ISO 31-0). Up to 20 fractional digits so we don't silently
|
|
4
|
+
// round decimal column values on display.
|
|
5
|
+
const formatter = new Intl.NumberFormat("en-US", {
|
|
6
|
+
useGrouping: true,
|
|
7
|
+
maximumFractionDigits: 20,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
value: any;
|
|
12
|
+
// When false, render the value plainly with no grouping. Default false
|
|
13
|
+
// so dropping this in for any number type is safe; opt into grouping
|
|
14
|
+
// only where it makes sense (quantities/amounts, not identifiers).
|
|
15
|
+
groupDigits?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { value, groupDigits = false }: Props = $props();
|
|
19
|
+
|
|
20
|
+
const formatted = $derived.by(() => {
|
|
21
|
+
if (!groupDigits) return String(value);
|
|
22
|
+
const n = Number(value);
|
|
23
|
+
if (!Number.isFinite(n)) return String(value);
|
|
24
|
+
return formatter.format(n).replaceAll(",", " ");
|
|
25
|
+
});
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<div class="tabular-nums">{formatted}</div>
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
collectionField: string;
|
|
7
7
|
idField: string;
|
|
8
8
|
entry: Record<string, any>;
|
|
9
|
-
|
|
9
|
+
refresh?: () => void;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
let {
|
|
13
13
|
collectionField,
|
|
14
14
|
idField,
|
|
15
15
|
entry,
|
|
16
|
-
|
|
16
|
+
refresh,
|
|
17
17
|
}: Props = $props();
|
|
18
18
|
|
|
19
19
|
const targetCollection = $derived(entry[collectionField] ?? null);
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
|
|
35
35
|
Icon={ExternalLink}
|
|
36
36
|
onSuccessfullSave={async () => {
|
|
37
|
-
|
|
37
|
+
refresh?.();
|
|
38
38
|
}}
|
|
39
39
|
/>
|
|
40
40
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { X } from "lucide-svelte";
|
|
3
|
+
import { untrack } from "svelte";
|
|
3
4
|
import { fade, scale } from "svelte/transition";
|
|
4
5
|
import { cubicOut } from "svelte/easing";
|
|
5
6
|
import Portal from "svelte-portal";
|
|
@@ -11,6 +12,8 @@
|
|
|
11
12
|
interface Props {
|
|
12
13
|
collectionName: string;
|
|
13
14
|
filter?: Record<string, any>;
|
|
15
|
+
sort?: Record<string, "asc" | "desc">;
|
|
16
|
+
limit?: number;
|
|
14
17
|
title?: string;
|
|
15
18
|
showHeader?: boolean;
|
|
16
19
|
showFooter?: boolean;
|
|
@@ -22,6 +25,8 @@
|
|
|
22
25
|
let {
|
|
23
26
|
collectionName,
|
|
24
27
|
filter,
|
|
28
|
+
sort,
|
|
29
|
+
limit,
|
|
25
30
|
title,
|
|
26
31
|
showHeader = true,
|
|
27
32
|
showFooter = true,
|
|
@@ -29,6 +34,17 @@
|
|
|
29
34
|
tabs,
|
|
30
35
|
onClose,
|
|
31
36
|
}: Props = $props();
|
|
37
|
+
|
|
38
|
+
// Read once on mount — sort/limit are fixed for the popup's lifetime,
|
|
39
|
+
// and DataTable only reads searchParams during its initial $state setup
|
|
40
|
+
// so even live updates wouldn't propagate. untrack makes that intent
|
|
41
|
+
// explicit and silences Svelte's "captures initial value" warning.
|
|
42
|
+
const searchParams = untrack(() => {
|
|
43
|
+
const p: Record<string, any> = {};
|
|
44
|
+
if (sort) p.sort = sort;
|
|
45
|
+
if (limit != null) p.limit = String(limit);
|
|
46
|
+
return p;
|
|
47
|
+
});
|
|
32
48
|
</script>
|
|
33
49
|
|
|
34
50
|
<Portal target="body">
|
|
@@ -57,6 +73,7 @@
|
|
|
57
73
|
<DataTable
|
|
58
74
|
{collectionName}
|
|
59
75
|
{filter}
|
|
76
|
+
{searchParams}
|
|
60
77
|
{showHeader}
|
|
61
78
|
{showFooter}
|
|
62
79
|
{tableProps}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
|
+
import type { Changes } from "../utils";
|
|
3
|
+
import type { Snippet } from "svelte";
|
|
4
|
+
|
|
2
5
|
interface SubmitButton {
|
|
3
6
|
text: string;
|
|
4
7
|
icon: any;
|
|
@@ -12,7 +15,7 @@
|
|
|
12
15
|
title?: Snippet<[string]>;
|
|
13
16
|
onSuccessfullSave?: (entry: any) => Promise<void>;
|
|
14
17
|
onCancel?: () => Promise<void>;
|
|
15
|
-
|
|
18
|
+
onChanges?: (changes: Changes) => void;
|
|
16
19
|
}
|
|
17
20
|
</script>
|
|
18
21
|
|
|
@@ -24,11 +27,8 @@
|
|
|
24
27
|
import { untrack } from "svelte";
|
|
25
28
|
|
|
26
29
|
const { lobb, ctx } = getStudioContext();
|
|
27
|
-
import
|
|
28
|
-
import {
|
|
29
|
-
import type { Changes } from "../utils";
|
|
30
|
-
import { getChangedProperties } from "../../../utils";
|
|
31
|
-
import type { Snippet } from "svelte";
|
|
30
|
+
import CreateDetailViewChildren from "./createDetailViewChildren.svelte";
|
|
31
|
+
import { getDefaultEntry } from "../utils";
|
|
32
32
|
import DetailView from "../detailView.svelte";
|
|
33
33
|
import Drawer from "../../drawer.svelte";
|
|
34
34
|
|
|
@@ -40,80 +40,53 @@
|
|
|
40
40
|
onSuccessfullSave,
|
|
41
41
|
title,
|
|
42
42
|
submitButton,
|
|
43
|
-
|
|
43
|
+
onChanges,
|
|
44
44
|
}: CreateDetailViewProp = $props();
|
|
45
45
|
|
|
46
|
-
const isRecordingMode =
|
|
47
|
-
|
|
48
|
-
const changes = passedChanges as Changes;
|
|
46
|
+
const isRecordingMode = onChanges !== undefined;
|
|
47
|
+
let changes = $state<Changes>({ data: {}, children: {} });
|
|
49
48
|
|
|
50
49
|
const fieldNames = Object.keys(ctx.meta.collections[collectionName].fields);
|
|
51
50
|
let values = $state(getDefaultEntry(ctx, fieldNames, collectionName, passedValues));
|
|
52
51
|
let fieldsErrors: Record<string, any> = $state({});
|
|
53
52
|
|
|
54
|
-
const childCollections = ctx.meta.relations
|
|
55
|
-
.filter((r) => r.to.collection === collectionName)
|
|
56
|
-
.map((r) => (r as any).from.collection);
|
|
57
|
-
|
|
58
|
-
const subCollectionsValues: Record<string, any> = {};
|
|
59
|
-
for (const col of childCollections) {
|
|
60
|
-
if (passedValues[col]) subCollectionsValues[col] = passedValues[col];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
53
|
$effect(() => {
|
|
64
54
|
const snap = $state.snapshot(values);
|
|
65
|
-
|
|
66
55
|
untrack(() => {
|
|
67
56
|
const data: Record<string, any> = {};
|
|
68
|
-
const children: Record<string, any> = {};
|
|
69
|
-
|
|
70
57
|
for (const [key, value] of Object.entries(snap)) {
|
|
71
|
-
if (
|
|
72
|
-
children[key] = {
|
|
73
|
-
created: (value as any[]).filter((r) => !r.id).map((r) => ({ data: r })),
|
|
74
|
-
updated: [],
|
|
75
|
-
deleted: [],
|
|
76
|
-
linked: (value as any[]).filter((r) => r.id).map((r) => r.id),
|
|
77
|
-
unlinked: [],
|
|
78
|
-
};
|
|
79
|
-
} else if (value !== null && value !== undefined && value !== '') {
|
|
58
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
80
59
|
data[key] = value;
|
|
81
60
|
}
|
|
82
61
|
}
|
|
83
|
-
|
|
84
62
|
changes.data = data;
|
|
85
|
-
changes.children = children;
|
|
86
|
-
|
|
87
|
-
if (!isRecordingMode) {
|
|
88
|
-
console.log(`[${collectionName}] changes:`, $state.snapshot(changes));
|
|
89
|
-
}
|
|
90
63
|
});
|
|
91
64
|
});
|
|
92
65
|
|
|
93
|
-
function
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
66
|
+
function buildPayload(changes: Changes): { data: Record<string, any>; children?: Record<string, any> } {
|
|
67
|
+
const result: Record<string, any> = {};
|
|
68
|
+
for (const [collection, ops] of Object.entries(changes.children)) {
|
|
69
|
+
const hasOps = ops.created.length || ops.linked.length;
|
|
70
|
+
if (!hasOps) continue;
|
|
71
|
+
result[collection] = {
|
|
72
|
+
...(ops.created.length ? { create: ops.created.map((op) => op.data) } : {}),
|
|
73
|
+
...(ops.linked.length ? { link: ops.linked.map((r) => r.id) } : {}),
|
|
74
|
+
};
|
|
97
75
|
}
|
|
76
|
+
const children = Object.keys(result).length ? result : undefined;
|
|
77
|
+
return { data: changes.data, ...(children ? { children } : {}) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function handleCancel() {
|
|
98
81
|
onCancel?.();
|
|
99
82
|
}
|
|
100
83
|
|
|
101
84
|
async function handleSave() {
|
|
102
85
|
const snap = $state.snapshot(changes);
|
|
103
|
-
|
|
104
|
-
const children = buildChildren(ctx, collectionName, { ...snap.data, ...Object.fromEntries(
|
|
105
|
-
Object.entries(snap.children).map(([col, ops]) => [
|
|
106
|
-
col,
|
|
107
|
-
[
|
|
108
|
-
...(ops.created.map((op) => op.data)),
|
|
109
|
-
...(ops.linked.map((id) => ({ id }))),
|
|
110
|
-
]
|
|
111
|
-
])
|
|
112
|
-
)});
|
|
113
|
-
|
|
114
|
-
const response = await lobb.createOne(collectionName, snap.data, children, undefined, isRecordingMode);
|
|
86
|
+
const response = await lobb.createOne(collectionName, buildPayload(snap), undefined, isRecordingMode);
|
|
115
87
|
|
|
116
88
|
if (response.status === 204) {
|
|
89
|
+
onChanges?.(snap);
|
|
117
90
|
if (onSuccessfullSave) await onSuccessfullSave(snap);
|
|
118
91
|
toast.success(`The record was successfully created`);
|
|
119
92
|
handleCancel();
|
|
@@ -133,6 +106,7 @@
|
|
|
133
106
|
}
|
|
134
107
|
}
|
|
135
108
|
|
|
109
|
+
onChanges?.(snap);
|
|
136
110
|
if (onSuccessfullSave) await onSuccessfullSave(snap);
|
|
137
111
|
toast.success(`The record was successfully created`);
|
|
138
112
|
onCancel?.();
|
|
@@ -161,7 +135,7 @@
|
|
|
161
135
|
<div class="flex-1 overflow-y-auto">
|
|
162
136
|
<DetailView {collectionName} bind:entry={values} {fieldsErrors} />
|
|
163
137
|
{#if showRelatedRecords}
|
|
164
|
-
<
|
|
138
|
+
<CreateDetailViewChildren {collectionName} {changes} onChanges={(children) => { changes.children = children; }} />
|
|
165
139
|
{/if}
|
|
166
140
|
</div>
|
|
167
141
|
<div class="flex h-12 items-center justify-end gap-2 border-t px-4">
|