@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.
Files changed (71) hide show
  1. package/dist/actions.d.ts +2 -0
  2. package/dist/components/Studio.svelte +1 -10
  3. package/dist/components/dataTable/dataTable.svelte +104 -39
  4. package/dist/components/dataTable/dataTable.svelte.d.ts +4 -1
  5. package/dist/components/dataTable/fieldCell.svelte +7 -4
  6. package/dist/components/dataTable/fieldCell.svelte.d.ts +2 -2
  7. package/dist/components/dataTable/filter.svelte +0 -15
  8. package/dist/components/dataTable/header.svelte +13 -14
  9. package/dist/components/dataTable/header.svelte.d.ts +3 -2
  10. package/dist/components/dataTable/numberCell.svelte +28 -0
  11. package/dist/components/dataTable/numberCell.svelte.d.ts +7 -0
  12. package/dist/components/dataTable/polymorphicFieldCell.svelte +3 -3
  13. package/dist/components/dataTable/polymorphicFieldCell.svelte.d.ts +2 -2
  14. package/dist/components/dataTablePopup/dataTablePopup.svelte +17 -0
  15. package/dist/components/dataTablePopup/dataTablePopup.svelte.d.ts +2 -0
  16. package/dist/components/detailView/create/createDetailView.svelte +28 -54
  17. package/dist/components/detailView/create/createDetailView.svelte.d.ts +4 -3
  18. package/dist/components/detailView/create/createDetailViewChildren.svelte +113 -0
  19. package/dist/components/detailView/create/createDetailViewChildren.svelte.d.ts +9 -0
  20. package/dist/components/detailView/create/createManyView.svelte +2 -2
  21. package/dist/components/detailView/detailView.svelte +6 -1
  22. package/dist/components/detailView/fieldInput.svelte +7 -5
  23. package/dist/components/detailView/update/updateDetailView.svelte +46 -40
  24. package/dist/components/detailView/update/updateDetailView.svelte.d.ts +5 -3
  25. package/dist/components/detailView/update/updateDetailViewButton.svelte +0 -1
  26. package/dist/components/detailView/update/updateDetailViewChildren.svelte +122 -0
  27. package/dist/components/detailView/update/updateDetailViewChildren.svelte.d.ts +10 -0
  28. package/dist/components/detailView/utils.d.ts +1 -2
  29. package/dist/components/importButton.svelte +1 -1
  30. package/dist/components/miniSidebar.svelte +6 -3
  31. package/dist/components/richTextEditor.svelte +2 -0
  32. package/dist/components/routes/extensions/extension.svelte +1 -1
  33. package/dist/components/routes/home.svelte +35 -21
  34. package/dist/components/ui/input/numberInput.svelte +104 -0
  35. package/dist/components/ui/input/numberInput.svelte.d.ts +9 -0
  36. package/dist/components/workflowEditor.svelte +6 -4
  37. package/package.json +4 -3
  38. package/src/lib/actions.ts +2 -0
  39. package/src/lib/components/Studio.svelte +1 -10
  40. package/src/lib/components/dataTable/dataTable.svelte +104 -39
  41. package/src/lib/components/dataTable/fieldCell.svelte +7 -4
  42. package/src/lib/components/dataTable/filter.svelte +0 -15
  43. package/src/lib/components/dataTable/header.svelte +13 -14
  44. package/src/lib/components/dataTable/numberCell.svelte +28 -0
  45. package/src/lib/components/dataTable/polymorphicFieldCell.svelte +3 -3
  46. package/src/lib/components/dataTablePopup/dataTablePopup.svelte +17 -0
  47. package/src/lib/components/detailView/create/createDetailView.svelte +28 -54
  48. package/src/lib/components/detailView/create/createDetailViewChildren.svelte +113 -0
  49. package/src/lib/components/detailView/create/createManyView.svelte +2 -2
  50. package/src/lib/components/detailView/detailView.svelte +6 -1
  51. package/src/lib/components/detailView/fieldInput.svelte +7 -5
  52. package/src/lib/components/detailView/update/updateDetailView.svelte +46 -40
  53. package/src/lib/components/detailView/update/updateDetailViewButton.svelte +0 -1
  54. package/src/lib/components/detailView/update/updateDetailViewChildren.svelte +122 -0
  55. package/src/lib/components/detailView/utils.ts +1 -1
  56. package/src/lib/components/importButton.svelte +1 -1
  57. package/src/lib/components/miniSidebar.svelte +6 -3
  58. package/src/lib/components/richTextEditor.svelte +2 -0
  59. package/src/lib/components/routes/extensions/extension.svelte +1 -1
  60. package/src/lib/components/routes/home.svelte +35 -21
  61. package/src/lib/components/ui/input/numberInput.svelte +104 -0
  62. package/src/lib/components/workflowEditor.svelte +6 -4
  63. package/dist/components/breadCrumbs.svelte +0 -61
  64. package/dist/components/breadCrumbs.svelte.d.ts +0 -3
  65. package/dist/components/detailView/update/detailViewChildren.svelte +0 -61
  66. package/dist/components/detailView/update/detailViewChildren.svelte.d.ts +0 -9
  67. package/dist/components/header.svelte +0 -45
  68. package/dist/components/header.svelte.d.ts +0 -6
  69. package/src/lib/components/breadCrumbs.svelte +0 -61
  70. package/src/lib/components/detailView/update/detailViewChildren.svelte +0 -61
  71. package/src/lib/components/header.svelte +0 -45
package/dist/actions.d.ts CHANGED
@@ -14,6 +14,8 @@ export interface OpenDataTableDrawerProps {
14
14
  export interface OpenDataTablePopupProps {
15
15
  collectionName: string;
16
16
  filter?: Record<string, any>;
17
+ sort?: Record<string, "asc" | "desc">;
18
+ limit?: number;
17
19
  title?: string;
18
20
  showHeader?: boolean;
19
21
  showFooter?: boolean;
@@ -4,7 +4,6 @@
4
4
  import { ModeWatcher } from "mode-watcher";
5
5
  import { createLobb } from "../store.svelte";
6
6
  import { setStudioContext } from "../context";
7
- import Header from "./header.svelte";
8
7
  import { LoaderCircle, ServerOff } from "lucide-svelte";
9
8
  import MiniSidebar from "./miniSidebar.svelte";
10
9
  import * as Tooltip from "./ui/tooltip";
@@ -107,8 +106,7 @@
107
106
  style="display: grid; grid-template-columns: {isSmallScreen ? '1fr' : '3.5rem 1fr'};"
108
107
  >
109
108
  <MiniSidebar />
110
- <div class="second_grid">
111
- <Header />
109
+ <div class="min-h-0 h-screen overflow-hidden">
112
110
  {#if page.url.pathname.replace(/\/$/, "") === "/studio"}
113
111
  <Home />
114
112
  {:else if page.url.pathname.startsWith("/studio/collections")}
@@ -131,10 +129,3 @@
131
129
  </main>
132
130
  </Tooltip.Provider>
133
131
  {/if}
134
-
135
- <style>
136
- .second_grid {
137
- display: grid;
138
- grid-template-rows: 2.5rem 1fr;
139
- }
140
- </style>
@@ -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 { Changes, ChildrenChanges } from "../detailView/utils";
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
- changes = $bindable<ChildrenChanges | undefined>(undefined),
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
- function getOrCreateUpdatedSlot(recordId: string): Changes | undefined {
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 (!changes) return serverData;
92
+ if (!isRecordingMode) return serverData;
93
93
 
94
94
  const removedIds = new Set([
95
- ...changes.deleted.map((r) => String(r.id)),
96
- ...changes.unlinked.map((r) => String(r.id)),
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 = changes.updated.find((u) => String(u.id) === String(r.id));
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 changes.linked) {
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 changes.created) {
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 (changes) {
181
- const record = data.find((r: any) => String(r.id) === String(entryId));
182
- if (record) changes.deleted.push($state.snapshot(record));
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 (changes) {
198
- const record = data.find((r: any) => String(r.id) === String(entryId));
199
- if (record) changes.unlinked.push($state.snapshot(record));
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
- {changes}
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={getOrCreateUpdatedSlot(String(entry.id))}
270
- onSuccessfullSave={async () => {
271
- params = { ...params };
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
- tableParams={params}
367
+ refresh={() => { params = { ...params }; }}
303
368
  />
304
369
  {/snippet}
305
370
  {#snippet collapsible(entry)}
@@ -11,15 +11,18 @@ interface Props {
11
11
  filter?: any;
12
12
  searchParams?: Record<string, any>;
13
13
  parentContext?: ParentContext;
14
+ onChanges?: (changes: ChildrenChanges) => void;
14
15
  changes?: ChildrenChanges;
15
16
  showHeader?: boolean;
16
17
  showFooter?: boolean;
17
18
  showImport?: boolean;
18
19
  showDelete?: boolean;
20
+ showEdit?: boolean;
21
+ onDataLoad?: (total: number) => void;
19
22
  tableProps?: Partial<TableProps>;
20
23
  tabs?: CollectionTab[];
21
24
  headerLeft?: Snippet<[]>;
22
25
  }
23
- declare const DataTable: import("svelte").Component<Props, {}, "changes">;
26
+ declare const DataTable: import("svelte").Component<Props, {}, "">;
24
27
  type DataTable = ReturnType<typeof DataTable>;
25
28
  export default DataTable;
@@ -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
- tableParams?: any;
20
+ refresh?: () => void;
20
21
  }
21
22
 
22
23
  let {
@@ -24,7 +25,7 @@
24
25
  fieldName,
25
26
  value,
26
27
  entry,
27
- tableParams = $bindable(),
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
- bind:tableParams
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
- tableParams = { ...tableParams };
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}
@@ -3,8 +3,8 @@ interface Props {
3
3
  fieldName: string;
4
4
  value: any;
5
5
  entry: Record<string, any>;
6
- tableParams?: any;
6
+ refresh?: () => void;
7
7
  }
8
- declare const FieldCell: import("svelte").Component<Props, {}, "tableParams">;
8
+ declare const FieldCell: import("svelte").Component<Props, {}, "">;
9
9
  type FieldCell = ReturnType<typeof FieldCell>;
10
10
  export default FieldCell;
@@ -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, ChildrenChanges } from "../detailView/utils";
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
- changes?: ChildrenChanges;
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
- changes,
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 (changes) {
48
- changes.linked.push(record);
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 handleCreateSuccess(snap: any) {
56
- if (changes) {
57
- changes.created.push({ data: snap.data });
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
- changes={changes ? createChanges : undefined}
212
- onSuccessfullSave={handleCreateSuccess}
210
+ onChanges={onCreate ? handleCreate : undefined}
211
+ onSuccessfullSave={onCreate ? undefined : handleCreate}
213
212
  >
214
213
  {headerIsSmall ? "" : "Create"}
215
214
  </CreateDetailViewButton>
@@ -1,4 +1,4 @@
1
- import type { ChildrenChanges } from "../detailView/utils";
1
+ import type { Changes } from "../detailView/utils";
2
2
  import type { ParentContext } from "./dataTable.svelte";
3
3
  import type { Snippet } from "svelte";
4
4
  interface Props {
@@ -6,7 +6,8 @@ interface Props {
6
6
  params: any;
7
7
  selectedRecords: string[];
8
8
  parentContext?: ParentContext;
9
- changes?: ChildrenChanges;
9
+ onLink?: (record: any) => void;
10
+ onCreate?: (changes: Changes) => void;
10
11
  showImport?: boolean;
11
12
  showCreate?: boolean;
12
13
  left?: Snippet<[]>;
@@ -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>
@@ -0,0 +1,7 @@
1
+ interface Props {
2
+ value: any;
3
+ groupDigits?: boolean;
4
+ }
5
+ declare const NumberCell: import("svelte").Component<Props, {}, "">;
6
+ type NumberCell = ReturnType<typeof NumberCell>;
7
+ export default NumberCell;
@@ -6,14 +6,14 @@
6
6
  collectionField: string;
7
7
  idField: string;
8
8
  entry: Record<string, any>;
9
- tableParams?: any;
9
+ refresh?: () => void;
10
10
  }
11
11
 
12
12
  let {
13
13
  collectionField,
14
14
  idField,
15
15
  entry,
16
- tableParams = $bindable(),
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
- tableParams = { ...tableParams };
37
+ refresh?.();
38
38
  }}
39
39
  />
40
40
  </div>
@@ -2,8 +2,8 @@ interface Props {
2
2
  collectionField: string;
3
3
  idField: string;
4
4
  entry: Record<string, any>;
5
- tableParams?: any;
5
+ refresh?: () => void;
6
6
  }
7
- declare const PolymorphicFieldCell: import("svelte").Component<Props, {}, "tableParams">;
7
+ declare const PolymorphicFieldCell: import("svelte").Component<Props, {}, "">;
8
8
  type PolymorphicFieldCell = ReturnType<typeof PolymorphicFieldCell>;
9
9
  export default PolymorphicFieldCell;
@@ -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}
@@ -3,6 +3,8 @@ import type { CollectionTab } from "../../store.types";
3
3
  interface Props {
4
4
  collectionName: string;
5
5
  filter?: Record<string, any>;
6
+ sort?: Record<string, "asc" | "desc">;
7
+ limit?: number;
6
8
  title?: string;
7
9
  showHeader?: boolean;
8
10
  showFooter?: boolean;