@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
@@ -0,0 +1,113 @@
1
+ <script lang="ts">
2
+ import DataTable from "../../dataTable/dataTable.svelte";
3
+ import { getStudioContext } from "../../../context";
4
+ import { Table, Link, Plus } from "lucide-svelte";
5
+ import { untrack } from "svelte";
6
+ import CreateDetailViewButton from "./createDetailViewButton.svelte";
7
+ import SelectRecord from "../../selectRecord.svelte";
8
+
9
+ const { ctx } = getStudioContext();
10
+
11
+ import type { Changes, ChildrenChanges } from "../utils";
12
+
13
+ interface LocalProp {
14
+ collectionName: string;
15
+ changes?: Changes;
16
+ onChanges?: (children: Changes["children"]) => void;
17
+ }
18
+
19
+ let { collectionName, changes, onChanges }: LocalProp = $props();
20
+
21
+ const children = (ctx.meta.collections[collectionName]?.children ?? [])
22
+ .filter((c: any) => c.type === "fk" || c.type === "m2m" || c.type === "polymorphic");
23
+
24
+ let localChildren = $state<Changes["children"]>(untrack(() => changes?.children ?? {}));
25
+
26
+ function handleChildChanges(collection: string, updated: ChildrenChanges) {
27
+ localChildren[collection] = updated;
28
+ onChanges?.($state.snapshot(localChildren));
29
+ }
30
+
31
+ function handleEmptyCreate(collection: string, c: Changes) {
32
+ if (!localChildren[collection]) {
33
+ localChildren[collection] = { created: [], updated: [], deleted: [], linked: [], unlinked: [] };
34
+ }
35
+ localChildren[collection].created.push({ data: c.data });
36
+ onChanges?.($state.snapshot(localChildren));
37
+ }
38
+
39
+ function handleEmptyLink(collection: string, record: any) {
40
+ if (!localChildren[collection]) {
41
+ localChildren[collection] = { created: [], updated: [], deleted: [], linked: [], unlinked: [] };
42
+ }
43
+ localChildren[collection].linked.push(record);
44
+ onChanges?.($state.snapshot(localChildren));
45
+ }
46
+ </script>
47
+
48
+ {#if children.length}
49
+ <div class="flex flex-col gap-3 border-t p-4">
50
+ <div class="flex items-center gap-2">
51
+ <Link size="14" class="text-muted-foreground" />
52
+ <span class="text-sm font-medium">Sub Records</span>
53
+ </div>
54
+ {#each children as child}
55
+ {@const localAdditions = (localChildren[child.collection]?.created.length ?? 0) + (localChildren[child.collection]?.linked.length ?? 0)}
56
+ {#if localAdditions === 0}
57
+ <div class="rounded-lg border bg-muted-soft overflow-hidden flex flex-col">
58
+ <div class="flex flex-col items-center justify-center gap-3 py-6 px-4">
59
+ <div class="flex flex-col items-center gap-2 text-center">
60
+ <div class="flex items-center gap-1.5 text-sm font-medium text-muted-foreground">
61
+ <span>No records in</span>
62
+ <span class="rounded-md border bg-muted px-2 py-0.5 text-xs font-normal">{child.collection}</span>
63
+ </div>
64
+ <span class="text-xs text-muted-foreground/70">Create a new record or link an existing one.</span>
65
+ </div>
66
+ <div class="flex gap-2">
67
+ <SelectRecord
68
+ collectionName={child.collection}
69
+ variant="outline"
70
+ class="h-7 px-3 text-xs font-normal"
71
+ Icon={Link}
72
+ onSelect={(r) => handleEmptyLink(child.collection, r)}
73
+ >
74
+ Link
75
+ </SelectRecord>
76
+ <CreateDetailViewButton
77
+ collectionName={child.collection}
78
+ variant="default"
79
+ class="h-7 px-3 text-xs font-normal"
80
+ Icon={Plus}
81
+ onChanges={(c) => handleEmptyCreate(child.collection, c)}
82
+ >
83
+ Create
84
+ </CreateDetailViewButton>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ {:else}
89
+ <div class="rounded-lg border bg-background overflow-hidden flex flex-col max-h-96">
90
+ <DataTable
91
+ collectionName={child.collection}
92
+ searchParams={{ children_of: collectionName, parent_id: -1 }}
93
+ onChanges={(updated) => handleChildChanges(child.collection, updated)}
94
+ changes={localChildren[child.collection]}
95
+ showImport={false}
96
+ showHeader={true}
97
+ showFooter={false}
98
+ showDelete={false}
99
+ showEdit={false}
100
+ tableProps={{ showLastColumnBorder: false, showLastRowBorder: true }}
101
+ >
102
+ {#snippet headerLeft()}
103
+ <div class="flex items-center gap-2 px-1">
104
+ <Table size="14" class="text-muted-foreground" />
105
+ <span class="text-sm font-medium">{child.collection}</span>
106
+ </div>
107
+ {/snippet}
108
+ </DataTable>
109
+ </div>
110
+ {/if}
111
+ {/each}
112
+ </div>
113
+ {/if}
@@ -145,7 +145,7 @@
145
145
  class="h-7 px-2 font-normal text-xs"
146
146
  Icon={Plus}
147
147
  {collectionName}
148
- changes={addChanges}
148
+ onChanges={(updated) => { addChanges = updated; }}
149
149
  showRelatedRecords={true}
150
150
  onSuccessfullSave={onRecordAdd}
151
151
  values={createValues}
@@ -200,7 +200,7 @@
200
200
  class="h-6 w-6 text-muted-foreground hover:bg-transparent p-0"
201
201
  Icon={Pencil}
202
202
  {collectionName}
203
- changes={editChanges}
203
+ onChanges={(updated) => { editChanges = updated; }}
204
204
  showRelatedRecords={true}
205
205
  onSuccessfullSave={(entry) =>
206
206
  onRecordOverride(entry, index)}
@@ -20,8 +20,13 @@
20
20
  }: Props = $props();
21
21
 
22
22
  const { lobb, ctx } = getStudioContext();
23
+ // Singleton collections only ever have one row, and `id` is auto-
24
+ // generated for that row — there's no value to showing it in the form.
23
25
  const fieldNames = $derived(
24
- Object.keys(ctx.meta.collections[collectionName].fields),
26
+ Object.keys(ctx.meta.collections[collectionName].fields).filter(
27
+ (fieldName) =>
28
+ !(ctx.meta.collections[collectionName].singleton && fieldName === "id"),
29
+ ),
25
30
  );
26
31
  </script>
27
32
 
@@ -6,6 +6,7 @@
6
6
  import Button from "../ui/button/button.svelte";
7
7
  import FieldCustomInput from "./fieldCustomInput.svelte";
8
8
  import Input from "../ui/input/input.svelte";
9
+ import NumberInput from "../ui/input/numberInput.svelte";
9
10
  import * as Select from "../ui/select/index";
10
11
  import EnumBadge from "../dataTable/enumBadge.svelte";
11
12
  import type { EnumOption } from "@lobb-js/core";
@@ -257,11 +258,12 @@
257
258
  </Select.Item>
258
259
  </Select.Content>
259
260
  </Select.Root>
260
- {:else if field.type === "decimal"}
261
- <Input
261
+ {:else if field.type === "decimal" || field.type === "float" || field.type === "integer" || field.type === "long"}
262
+ {@const isFloat = field.type === "decimal" || field.type === "float"}
263
+ <NumberInput
262
264
  placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
263
- type="number"
264
- step="any"
265
+ scale={isFloat ? 20 : 0}
266
+ groupDigits={ui?.groupDigits ?? false}
265
267
  class="
266
268
  bg-muted-soft text-xs
267
269
  {destructive ? 'border-destructive bg-destructive/10' : ''}
@@ -271,7 +273,7 @@
271
273
  {:else}
272
274
  <Input
273
275
  placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
274
- type="number"
276
+ type="text"
275
277
  class="
276
278
  bg-muted-soft text-xs
277
279
  {destructive ? 'border-destructive bg-destructive/10' : ''}
@@ -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;
@@ -8,12 +11,13 @@
8
11
  collectionName: string;
9
12
  recordId: string;
10
13
  values?: Record<string, any>;
14
+ changes?: Changes;
15
+ onChanges?: (changes: Changes) => void;
11
16
  showRelatedRecords?: boolean;
12
17
  submitButton?: SubmitButton;
13
18
  title?: Snippet<[string]>;
14
19
  onSuccessfullSave?: (entry: any) => Promise<void>;
15
20
  onCancel?: () => Promise<void>;
16
- changes?: import("../utils").Changes | undefined;
17
21
  }
18
22
  </script>
19
23
 
@@ -26,10 +30,9 @@
26
30
 
27
31
  const { lobb, ctx } = getStudioContext();
28
32
  import { getChangedProperties } from "../../../utils";
29
- import DetailViewChildren from "./detailViewChildren.svelte";
30
- import type { Snippet } from "svelte";
33
+ import UpdateDetailViewChildren from "./updateDetailViewChildren.svelte";
31
34
  import { getDefaultEntry } from "../utils";
32
- import type { Changes, ChildrenChanges } from "../utils";
35
+ import type { ChildrenChanges } from "../utils";
33
36
  import DetailView from "../detailView.svelte";
34
37
  import Drawer from "../../drawer.svelte";
35
38
 
@@ -42,50 +45,67 @@
42
45
  title,
43
46
  submitButton,
44
47
  recordId,
45
- changes: passedChanges = $bindable<Changes | undefined>(undefined),
48
+ changes,
49
+ onChanges,
46
50
  }: UpdateDetailViewProp = $props();
47
51
 
48
- // Recording mode = changes was passed from a parent component
49
- const isRecordingMode = passedChanges !== undefined;
50
- if (!isRecordingMode) passedChanges = { data: {}, children: {} };
51
- const changes = passedChanges as Changes;
52
+ const isRecordingMode = onChanges !== undefined;
53
+ let localChanges = $state<Changes>(
54
+ untrack(() => ({ data: changes?.data ?? {}, children: changes?.children ?? {} }))
55
+ );
52
56
 
53
57
  const fieldNames = Object.keys(ctx.meta.collections[collectionName].fields);
54
- let values = $state(getDefaultEntry(ctx, fieldNames, collectionName, passedValues));
55
- const initialValues = $state.snapshot(values);
58
+ const mergedValues = untrack(() => changes?.data
59
+ ? { ...passedValues, ...changes.data }
60
+ : passedValues);
61
+ let values = $state(getDefaultEntry(ctx, fieldNames, collectionName, mergedValues));
62
+ const initialValues = $state.snapshot(getDefaultEntry(ctx, fieldNames, collectionName, passedValues));
56
63
  let fieldsErrors: Record<string, any> = $state({});
57
64
 
58
65
  const hasChanges = $derived(
59
- Object.keys(changes.data).length > 0 ||
60
- Object.values(changes.children).some(
66
+ Object.keys(localChanges.data).length > 0 ||
67
+ Object.values(localChanges.children).some(
61
68
  (ch: ChildrenChanges) => ch.created.length || ch.updated.length || ch.deleted.length || ch.linked.length || ch.unlinked.length,
62
69
  ),
63
70
  );
64
71
 
65
- // Tracks top-level field edits into changes.data.
66
- // Child ops (create/link/unlink/delete) are written directly by DataTable into changes.children.
67
72
  $effect(() => {
68
73
  const currentEntrySnap = $state.snapshot(values);
69
74
 
70
75
  untrack(() => {
71
- changes.data = getChangedProperties(initialValues, currentEntrySnap);
76
+ localChanges.data = getChangedProperties(initialValues, currentEntrySnap);
72
77
  });
73
78
  });
74
79
 
75
- // Separate logging effect — needs its own $effect so it tracks mutations to changes.children.
76
80
  $effect(() => {
77
- if (!isRecordingMode) {
78
- console.log(`[${collectionName}] changes:`, $state.snapshot(changes));
81
+ const snap = $state.snapshot(localChanges);
82
+ if (isRecordingMode) {
83
+ const hasAny = Object.keys(snap.data).length > 0 ||
84
+ Object.values(snap.children).some((c: ChildrenChanges) =>
85
+ c.created.length || c.updated.length || c.deleted.length || c.linked.length || c.unlinked.length
86
+ );
87
+ if (hasAny) untrack(() => onChanges?.(snap));
79
88
  }
80
89
  });
81
90
 
82
- function buildApiChildren(children: Record<string, ChildrenChanges>): Record<string, any> | undefined {
91
+ function buildPayload(changes: Changes): { data: Record<string, any>; children?: Record<string, any> } {
92
+ const { id: _id, ...data } = changes.data;
93
+ const children = buildChildren(changes.children);
94
+ return { data, ...(children ? { children } : {}) };
95
+ }
96
+
97
+ function buildChildren(children: Record<string, ChildrenChanges>): Record<string, any> | undefined {
83
98
  const result: Record<string, any> = {};
84
99
  for (const [collection, ops] of Object.entries(children)) {
85
- const hasOps = ops.created.length || ops.deleted.length || ops.linked.length || ops.unlinked.length;
100
+ const hasOps = ops.created.length || ops.updated.length || ops.deleted.length || ops.linked.length || ops.unlinked.length;
86
101
  if (!hasOps) continue;
87
102
  result[collection] = {
88
103
  ...(ops.created.length ? { create: ops.created.map((op) => op.data) } : {}),
104
+ ...(ops.updated.length ? { update: ops.updated.map((u) => ({
105
+ id: u.id,
106
+ ...(Object.keys(u.changes.data).length ? { data: u.changes.data } : {}),
107
+ ...(Object.keys(u.changes.children).length ? { children: buildChildren(u.changes.children) } : {}),
108
+ })) } : {}),
89
109
  ...(ops.deleted.length ? { delete: ops.deleted.map((r) => r.id) } : {}),
90
110
  ...(ops.linked.length ? { link: ops.linked.map((r) => r.id) } : {}),
91
111
  ...(ops.unlinked.length ? { unlink: ops.unlinked.map((r) => r.id) } : {}),
@@ -95,21 +115,15 @@
95
115
  }
96
116
 
97
117
  function handleCancel() {
98
- if (isRecordingMode) {
99
- changes.data = {};
100
- changes.children = {};
101
- }
102
118
  onCancel?.();
103
119
  }
104
120
 
105
121
  async function handleSave() {
106
- const snap = $state.snapshot(changes);
107
- const { id: _id, ...data } = snap.data;
108
- const children = buildApiChildren(snap.children);
109
-
110
- const response = await lobb.updateOne(collectionName, recordId, data, children, isRecordingMode);
122
+ const snap = $state.snapshot(localChanges);
123
+ const response = await lobb.updateOne(collectionName, recordId, buildPayload(snap), isRecordingMode);
111
124
 
112
125
  if (response.status === 204) {
126
+ onChanges?.(snap);
113
127
  if (onSuccessfullSave) await onSuccessfullSave(snap);
114
128
  toast.success(`The record was successfully updated`);
115
129
  onCancel?.();
@@ -129,15 +143,7 @@
129
143
  }
130
144
  }
131
145
 
132
- // Real mode: also fire separate update requests for edited children
133
- if (!isRecordingMode) {
134
- for (const [collection, ops] of Object.entries(snap.children) as [string, ChildrenChanges][]) {
135
- for (const updated of ops.updated) {
136
- await lobb.updateOne(collection, String(updated.id), updated.data);
137
- }
138
- }
139
- }
140
-
146
+ onChanges?.(snap);
141
147
  if (onSuccessfullSave) await onSuccessfullSave(snap);
142
148
  toast.success(`The record was successfully updated`);
143
149
  onCancel?.();
@@ -166,7 +172,7 @@
166
172
  <div class="flex-1 overflow-y-auto">
167
173
  <DetailView {collectionName} bind:entry={values} {fieldsErrors} />
168
174
  {#if showRelatedRecords}
169
- <DetailViewChildren {collectionName} entry={values} activeChanges={changes} />
175
+ <UpdateDetailViewChildren {collectionName} entry={values} changes={localChanges} onChanges={(children) => { localChanges.children = children; }} />
170
176
  {/if}
171
177
  </div>
172
178
  <div class="flex h-12 items-center justify-end gap-2 border-t px-4">
@@ -46,7 +46,6 @@
46
46
  <UpdateDetailView
47
47
  {...props}
48
48
  {values}
49
- changes={props.changes}
50
49
  onCancel={async () => {
51
50
  open = false;
52
51
  values = undefined;
@@ -0,0 +1,122 @@
1
+ <script lang="ts">
2
+ import DataTable from "../../dataTable/dataTable.svelte";
3
+ import { getStudioContext } from "../../../context";
4
+ import { Table, Link, Plus } from "lucide-svelte";
5
+ import { untrack } from "svelte";
6
+ import CreateDetailViewButton from "../create/createDetailViewButton.svelte";
7
+ import SelectRecord from "../../selectRecord.svelte";
8
+
9
+ const { ctx } = getStudioContext();
10
+
11
+ import type { Changes, ChildrenChanges } from "../utils";
12
+
13
+ interface LocalProp {
14
+ collectionName: string;
15
+ entry: any;
16
+ changes?: Changes;
17
+ onChanges?: (children: Changes["children"]) => void;
18
+ }
19
+
20
+ let { collectionName, entry, changes, onChanges }: LocalProp = $props();
21
+
22
+ const children = (ctx.meta.collections[collectionName]?.children ?? [])
23
+ .filter((c: any) => c.type === "fk" || c.type === "m2m" || c.type === "polymorphic");
24
+
25
+ let localChildren = $state<Changes["children"]>(untrack(() => changes?.children ?? {}));
26
+ let serverCounts = $state<Record<string, number | undefined>>({});
27
+
28
+ function handleChildChanges(collection: string, updated: ChildrenChanges) {
29
+ localChildren[collection] = updated;
30
+ onChanges?.($state.snapshot(localChildren));
31
+ }
32
+
33
+ function handleDataLoad(collection: string, total: number) {
34
+ serverCounts[collection] = total;
35
+ }
36
+
37
+ function handleEmptyCreate(collection: string, c: Changes) {
38
+ if (!localChildren[collection]) {
39
+ localChildren[collection] = { created: [], updated: [], deleted: [], linked: [], unlinked: [] };
40
+ }
41
+ localChildren[collection].created.push({ data: c.data });
42
+ onChanges?.($state.snapshot(localChildren));
43
+ }
44
+
45
+ function handleEmptyLink(collection: string, record: any) {
46
+ if (!localChildren[collection]) {
47
+ localChildren[collection] = { created: [], updated: [], deleted: [], linked: [], unlinked: [] };
48
+ }
49
+ localChildren[collection].linked.push(record);
50
+ onChanges?.($state.snapshot(localChildren));
51
+ }
52
+ </script>
53
+
54
+ {#if children.length}
55
+ <div class="flex flex-col gap-3 border-t p-4">
56
+ <div class="flex items-center gap-2">
57
+ <Link size="14" class="text-muted-foreground" />
58
+ <span class="text-sm font-medium">Sub Records</span>
59
+ </div>
60
+ {#each children as child}
61
+ {@const serverCount = serverCounts[child.collection]}
62
+ {@const localAdditions = (localChildren[child.collection]?.created.length ?? 0) + (localChildren[child.collection]?.linked.length ?? 0)}
63
+ {@const showEmpty = serverCount !== undefined && serverCount === 0 && localAdditions === 0}
64
+ {#if showEmpty}
65
+ <div class="rounded-lg border bg-muted-soft overflow-hidden flex flex-col">
66
+ <div class="flex flex-col items-center justify-center gap-3 py-6 px-4">
67
+ <div class="flex flex-col items-center gap-2 text-center">
68
+ <div class="flex items-center gap-1.5 text-sm font-medium text-muted-foreground">
69
+ <span>No records in</span>
70
+ <span class="rounded-md border bg-muted px-2 py-0.5 text-xs font-normal">{child.collection}</span>
71
+ </div>
72
+ <span class="text-xs text-muted-foreground/70">Create a new record or link an existing one.</span>
73
+ </div>
74
+ <div class="flex gap-2">
75
+ <SelectRecord
76
+ collectionName={child.collection}
77
+ variant="outline"
78
+ class="h-7 px-3 text-xs font-normal"
79
+ Icon={Link}
80
+ onSelect={(r) => handleEmptyLink(child.collection, r)}
81
+ >
82
+ Link
83
+ </SelectRecord>
84
+ <CreateDetailViewButton
85
+ collectionName={child.collection}
86
+ variant="default"
87
+ class="h-7 px-3 text-xs font-normal"
88
+ Icon={Plus}
89
+ onChanges={(c) => handleEmptyCreate(child.collection, c)}
90
+ >
91
+ Create
92
+ </CreateDetailViewButton>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ {:else}
97
+ <div class="rounded-lg border bg-background overflow-hidden flex flex-col max-h-96">
98
+ <DataTable
99
+ collectionName={child.collection}
100
+ searchParams={{ children_of: collectionName, parent_id: entry.id }}
101
+ parentContext={{ collectionName, recordId: entry.id }}
102
+ onChanges={(updated) => handleChildChanges(child.collection, updated)}
103
+ changes={localChildren[child.collection]}
104
+ showImport={false}
105
+ showHeader={true}
106
+ showFooter={true}
107
+ showDelete={child.type === "fk" || child.type === "m2m"}
108
+ tableProps={{ showLastColumnBorder: false, showLastRowBorder: true }}
109
+ onDataLoad={(total) => handleDataLoad(child.collection, total)}
110
+ >
111
+ {#snippet headerLeft()}
112
+ <div class="flex items-center gap-2 px-1">
113
+ <Table size="14" class="text-muted-foreground" />
114
+ <span class="text-sm font-medium">{child.collection}</span>
115
+ </div>
116
+ {/snippet}
117
+ </DataTable>
118
+ </div>
119
+ {/if}
120
+ {/each}
121
+ </div>
122
+ {/if}
@@ -5,7 +5,7 @@ import type { DetailFormField } from "./detailViewForm.svelte";
5
5
 
6
6
  export type ChildrenChanges = {
7
7
  created: Array<{ data: Record<string, any> }>;
8
- updated: Array<{ id: string | number; data: Record<string, any>; children: Record<string, ChildrenChanges> }>;
8
+ updated: Array<{ id: string | number; changes: Changes }>;
9
9
  deleted: Array<Record<string, any>>;
10
10
  linked: Array<Record<string, any>>;
11
11
  unlinked: Array<Record<string, any>>;
@@ -132,7 +132,7 @@
132
132
  importResults = [];
133
133
  let hasSuccess = false;
134
134
  for (const row of finalRows) {
135
- const response = await lobb.createOne(collectionName, row);
135
+ const response = await lobb.createOne(collectionName, { data: row });
136
136
  if (response.ok) {
137
137
  importResults.push({ row, error: null });
138
138
  hasSuccess = true;
@@ -90,12 +90,15 @@
90
90
  // prefix of everything); other items use startsWith so sub-paths
91
91
  // (e.g. /studio/collections/risks) still highlight their parent.
92
92
  // Popover items with children are active when any of their children match.
93
- const currentPath = $derived(page.url.pathname);
93
+ // SvelteKit's pathname can come back with a trailing slash (`/studio/`)
94
+ // depending on config, so we normalize it before comparing.
95
+ const currentPath = $derived(page.url.pathname.replace(/\/$/, "") || "/");
94
96
  function isItemActive(item: any): boolean {
95
97
  if (item.navs) return item.navs.some((c: any) => isItemActive(c));
96
98
  if (!item.href) return false;
97
- if (item.href === "/studio") return currentPath === "/studio";
98
- return currentPath === item.href || currentPath.startsWith(item.href + "/");
99
+ const itemHref = item.href.replace(/\/$/, "") || "/";
100
+ if (itemHref === "/studio") return currentPath === "/studio";
101
+ return currentPath === itemHref || currentPath.startsWith(itemHref + "/");
99
102
  }
100
103
 
101
104
  // onMount is enough — Studio gets remounted on login/logout (see
@@ -72,6 +72,8 @@
72
72
  extensions: [
73
73
  StarterKit.configure({
74
74
  heading: { levels: [1, 2, 3] },
75
+ underline: false,
76
+ link: false,
75
77
  }),
76
78
  UnderlineExt,
77
79
  LinkExt.configure({ openOnClick: false }),
@@ -8,7 +8,7 @@
8
8
  const { lobb, ctx } = getStudioContext();
9
9
  </script>
10
10
 
11
- <div class="grid overflow-auto bg-background">
11
+ <div class="grid h-full overflow-auto bg-background">
12
12
  {#key extension && page}
13
13
  <ExtensionsComponents
14
14
  name="pages.{page}"
@@ -3,29 +3,43 @@
3
3
  import { goto } from "$app/navigation";
4
4
  import { ArrowRight } from "lucide-svelte";
5
5
  import HomeFooter from "./homeFooter.svelte";
6
+ import ExtensionsComponents from "../extensionsComponents.svelte";
7
+ import { getExtensionUtils } from "../../extensions/extensionUtils";
8
+ import { getStudioContext } from "../../context";
9
+
10
+ const { lobb, ctx } = getStudioContext();
6
11
  </script>
7
12
 
8
- <div class="flex flex-col">
9
- <div
10
- class="flex flex-1 w-full flex-col items-center justify-center gap-4 text-muted-foreground"
11
- >
12
- <div class="flex flex-col items-center justify-center p-4">
13
- <div class="text-3xl">Welcome to Lobb!</div>
14
- <div class="text-xs text-center">
15
- Your journey starts here. Explore and make the most of your
16
- experience.
13
+ <!--
14
+ Any extension that registers a `pages.home` component takes over /studio
15
+ (e.g. an app-specific overview dashboard). ExtensionsComponents falls
16
+ back to rendering its children when nothing matches, so the default
17
+ Lobb welcome below stays as the safety net for projects without an
18
+ overriding extension.
19
+ -->
20
+ <ExtensionsComponents name="pages.home" utils={getExtensionUtils(lobb, ctx)}>
21
+ <div class="flex h-full flex-col">
22
+ <div
23
+ class="flex flex-1 w-full flex-col items-center justify-center gap-4 text-muted-foreground"
24
+ >
25
+ <div class="flex flex-col items-center justify-center p-4">
26
+ <div class="text-3xl">Welcome to Lobb!</div>
27
+ <div class="text-xs text-center">
28
+ Your journey starts here. Explore and make the most of your
29
+ experience.
30
+ </div>
31
+ </div>
32
+ <div class="flex flex-col items-center justify-center">
33
+ <Button
34
+ Icon={ArrowRight}
35
+ variant="outline"
36
+ class="h-7 px-3 text-xs font-normal"
37
+ onclick={() => goto("/studio/collections")}
38
+ >
39
+ Go to collections
40
+ </Button>
17
41
  </div>
18
42
  </div>
19
- <div class="flex flex-col items-center justify-center">
20
- <Button
21
- Icon={ArrowRight}
22
- variant="outline"
23
- class="h-7 px-3 text-xs font-normal"
24
- onclick={() => goto("/studio/collections")}
25
- >
26
- Go to collections
27
- </Button>
28
- </div>
43
+ <HomeFooter />
29
44
  </div>
30
- <HomeFooter />
31
- </div>
45
+ </ExtensionsComponents>