@lobb-js/studio 0.29.0 → 0.29.1

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 (109) hide show
  1. package/README.md +1 -0
  2. package/dist/actions.d.ts +2 -0
  3. package/dist/components/Studio.svelte +39 -43
  4. package/dist/components/StudioRoot.svelte +19 -0
  5. package/dist/components/StudioRoot.svelte.d.ts +6 -0
  6. package/dist/components/breadCrumbs.svelte +5 -4
  7. package/dist/components/codeEditor.svelte +1 -1
  8. package/dist/components/dataTable/dataTable.svelte +35 -20
  9. package/dist/components/dataTable/dataTable.svelte.d.ts +2 -1
  10. package/dist/components/dataTable/dataTableTabs.svelte +4 -2
  11. package/dist/components/dataTable/dataTableTabs.svelte.d.ts +2 -0
  12. package/dist/components/dataTable/header.svelte +15 -11
  13. package/dist/components/dataTable/header.svelte.d.ts +1 -0
  14. package/dist/components/dataTable/listViewChildren.svelte +4 -6
  15. package/dist/components/dataTable/listViewChildren.svelte.d.ts +0 -1
  16. package/dist/components/dataTable/table.svelte +8 -10
  17. package/dist/components/dataTable/table.svelte.d.ts +0 -1
  18. package/dist/components/dataTableDrawer/dataTableDrawer.svelte +4 -1
  19. package/dist/components/dataTableDrawer/dataTableDrawer.svelte.d.ts +2 -0
  20. package/dist/components/detailView/create/children.svelte +1 -1
  21. package/dist/components/detailView/create/createDetailView.svelte +19 -61
  22. package/dist/components/detailView/create/createManyView.svelte +2 -4
  23. package/dist/components/detailView/detailView.svelte +81 -0
  24. package/dist/components/detailView/detailView.svelte.d.ts +8 -0
  25. package/dist/components/detailView/fieldInput.svelte +10 -10
  26. package/dist/components/detailView/fieldInputReplacement.svelte +7 -7
  27. package/dist/components/detailView/passwordInput.svelte +1 -1
  28. package/dist/components/detailView/update/updateDetailView.svelte +32 -69
  29. package/dist/components/diffViewer.svelte +1 -1
  30. package/dist/components/extensionsComponents.svelte +3 -1
  31. package/dist/components/foreingKeyInput.svelte +2 -2
  32. package/dist/components/importButton.svelte +12 -9
  33. package/dist/components/landing.svelte +7 -0
  34. package/dist/components/landing.svelte.d.ts +6 -14
  35. package/dist/components/miniSidebar.svelte +86 -15
  36. package/dist/components/miniSidebar.svelte.d.ts +2 -17
  37. package/dist/components/polymorphicInput.svelte +1 -1
  38. package/dist/components/rangeCalendarButton.svelte +10 -10
  39. package/dist/components/richTextEditor.svelte +1 -1
  40. package/dist/components/routes/collections/collections.svelte +32 -10
  41. package/dist/components/routes/data_model/dataModel.svelte +6 -28
  42. package/dist/components/routes/data_model/dataModel.svelte.d.ts +17 -2
  43. package/dist/components/routes/extensions/publicExtension.svelte +19 -0
  44. package/dist/components/routes/extensions/publicExtension.svelte.d.ts +13 -0
  45. package/dist/components/routes/home.svelte +2 -2
  46. package/dist/components/routes/workflows/workflows.svelte +4 -4
  47. package/dist/components/sidebar/sidebar.svelte +1 -1
  48. package/dist/components/sidebar/sidebarElements.svelte +4 -4
  49. package/dist/components/singletone.svelte +4 -6
  50. package/dist/components/ui/button/button.svelte +2 -3
  51. package/dist/components/workflowEditor.svelte +2 -2
  52. package/dist/eventSystem.d.ts +1 -1
  53. package/dist/eventSystem.js +7 -5
  54. package/dist/extensions/extension.types.d.ts +38 -14
  55. package/dist/extensions/extensionUtils.js +4 -2
  56. package/dist/index.d.ts +3 -1
  57. package/dist/index.js +3 -1
  58. package/dist/store.types.d.ts +1 -1
  59. package/dist/studioLifecycle.svelte.d.ts +2 -0
  60. package/dist/studioLifecycle.svelte.js +15 -0
  61. package/package.json +3 -4
  62. package/src/app.css +3 -0
  63. package/src/lib/actions.ts +2 -0
  64. package/src/lib/components/Studio.svelte +39 -43
  65. package/src/lib/components/StudioRoot.svelte +19 -0
  66. package/src/lib/components/breadCrumbs.svelte +5 -4
  67. package/src/lib/components/codeEditor.svelte +1 -1
  68. package/src/lib/components/dataTable/dataTable.svelte +35 -20
  69. package/src/lib/components/dataTable/dataTableTabs.svelte +4 -2
  70. package/src/lib/components/dataTable/header.svelte +15 -11
  71. package/src/lib/components/dataTable/listViewChildren.svelte +4 -6
  72. package/src/lib/components/dataTable/table.svelte +8 -10
  73. package/src/lib/components/dataTableDrawer/dataTableDrawer.svelte +4 -1
  74. package/src/lib/components/detailView/create/children.svelte +1 -1
  75. package/src/lib/components/detailView/create/createDetailView.svelte +19 -61
  76. package/src/lib/components/detailView/create/createManyView.svelte +2 -4
  77. package/src/lib/components/detailView/detailView.svelte +81 -0
  78. package/src/lib/components/detailView/fieldInput.svelte +10 -10
  79. package/src/lib/components/detailView/fieldInputReplacement.svelte +7 -7
  80. package/src/lib/components/detailView/passwordInput.svelte +1 -1
  81. package/src/lib/components/detailView/update/updateDetailView.svelte +32 -69
  82. package/src/lib/components/diffViewer.svelte +1 -1
  83. package/src/lib/components/extensionsComponents.svelte +3 -1
  84. package/src/lib/components/foreingKeyInput.svelte +2 -2
  85. package/src/lib/components/importButton.svelte +12 -9
  86. package/src/lib/components/landing.svelte +7 -0
  87. package/src/lib/components/miniSidebar.svelte +86 -15
  88. package/src/lib/components/polymorphicInput.svelte +1 -1
  89. package/src/lib/components/rangeCalendarButton.svelte +10 -10
  90. package/src/lib/components/richTextEditor.svelte +1 -1
  91. package/src/lib/components/routes/collections/collections.svelte +32 -10
  92. package/src/lib/components/routes/data_model/dataModel.svelte +6 -28
  93. package/src/lib/components/routes/extensions/publicExtension.svelte +19 -0
  94. package/src/lib/components/routes/home.svelte +2 -2
  95. package/src/lib/components/routes/workflows/workflows.svelte +4 -4
  96. package/src/lib/components/sidebar/sidebar.svelte +1 -1
  97. package/src/lib/components/sidebar/sidebarElements.svelte +4 -4
  98. package/src/lib/components/singletone.svelte +4 -6
  99. package/src/lib/components/ui/button/button.svelte +2 -3
  100. package/src/lib/components/workflowEditor.svelte +2 -2
  101. package/src/lib/eventSystem.ts +8 -7
  102. package/src/lib/extensions/extension.types.ts +39 -6
  103. package/src/lib/extensions/extensionUtils.ts +4 -2
  104. package/src/lib/index.ts +3 -1
  105. package/src/lib/store.types.ts +1 -1
  106. package/src/lib/studioLifecycle.svelte.ts +17 -0
  107. package/dist/components/routes/data_model/syncManager.svelte +0 -94
  108. package/dist/components/routes/data_model/syncManager.svelte.d.ts +0 -3
  109. package/src/lib/components/routes/data_model/syncManager.svelte +0 -94
@@ -86,7 +86,7 @@
86
86
  {:else if field.label === "id"}
87
87
  <Input
88
88
  placeholder="AUTO GENERATED"
89
- class="bg-muted/30 text-xs"
89
+ class="bg-muted-soft text-xs"
90
90
  bind:value
91
91
  />
92
92
  {:else if fieldRelationTarget && entry}
@@ -126,7 +126,7 @@
126
126
  >
127
127
  <Select.Trigger
128
128
  class="
129
- h-9 w-full bg-muted/30 pr-8
129
+ h-9 w-full bg-muted-soft pr-8
130
130
  {destructive ? 'border-destructive bg-destructive/10' : ''}
131
131
  "
132
132
  >
@@ -158,7 +158,7 @@
158
158
  placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
159
159
  type="text"
160
160
  class="
161
- bg-muted/30 text-xs
161
+ bg-muted-soft text-xs
162
162
  {destructive ? 'border-destructive bg-destructive/10' : ''}
163
163
  "
164
164
  bind:value
@@ -168,7 +168,7 @@
168
168
  placeholder={ui?.placeholder ? ui.placeholder : value === "" ? "EMPTY STRING" : "NULL"}
169
169
  rows={5}
170
170
  class="
171
- bg-muted/30 text-xs
171
+ bg-muted-soft text-xs
172
172
  {destructive ? 'border-destructive bg-destructive/10' : ''}
173
173
  "
174
174
  bind:value
@@ -178,7 +178,7 @@
178
178
  type="date"
179
179
  placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
180
180
  class="
181
- dateInput block w-full bg-muted/30 pr-9 text-xs
181
+ dateInput block w-full bg-muted-soft pr-9 text-xs
182
182
  {destructive ? 'border-destructive bg-destructive/10' : ''}
183
183
  "
184
184
  bind:value={
@@ -197,7 +197,7 @@
197
197
  type="time"
198
198
  placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
199
199
  class="
200
- dateInput block w-full bg-muted/30 pr-9 text-xs
200
+ dateInput block w-full bg-muted-soft pr-9 text-xs
201
201
  {destructive ? 'border-destructive bg-destructive/10' : ''}
202
202
  "
203
203
  bind:value={
@@ -215,7 +215,7 @@
215
215
  type="datetime-local"
216
216
  placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
217
217
  class="
218
- dateInput block w-full bg-muted/30 pr-9 text-xs
218
+ dateInput block w-full bg-muted-soft pr-9 text-xs
219
219
  {destructive ? 'border-destructive bg-destructive/10' : ''}
220
220
  "
221
221
  bind:value={
@@ -236,7 +236,7 @@
236
236
  <Select.Trigger
237
237
  placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
238
238
  class="
239
- bg-muted/30 pr-9
239
+ bg-muted-soft pr-9
240
240
  {destructive ? 'border-destructive bg-destructive/10' : ''}
241
241
  "
242
242
  >
@@ -263,7 +263,7 @@
263
263
  type="number"
264
264
  step="any"
265
265
  class="
266
- bg-muted/30 text-xs
266
+ bg-muted-soft text-xs
267
267
  {destructive ? 'border-destructive bg-destructive/10' : ''}
268
268
  "
269
269
  bind:value
@@ -273,7 +273,7 @@
273
273
  placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
274
274
  type="number"
275
275
  class="
276
- bg-muted/30 text-xs
276
+ bg-muted-soft text-xs
277
277
  {destructive ? 'border-destructive bg-destructive/10' : ''}
278
278
  "
279
279
  bind:value
@@ -49,7 +49,7 @@
49
49
  placeholder={field.placeholder ? field.placeholder : "NULL"}
50
50
  type="text"
51
51
  class="
52
- bg-muted/30 text-xs
52
+ bg-muted-soft text-xs
53
53
  {destructive ? 'border-destructive bg-destructive/10' : ''}
54
54
  "
55
55
  bind:value
@@ -64,7 +64,7 @@
64
64
  <Select.Trigger
65
65
  placeholder={field.placeholder ? field.placeholder : "NULL"}
66
66
  class="
67
- h-9 w-full bg-muted/30 pr-8
67
+ h-9 w-full bg-muted-soft pr-8
68
68
  {destructive ? 'border-destructive bg-destructive/10' : ''}
69
69
  "
70
70
  >
@@ -84,7 +84,7 @@
84
84
  placeholder={field.placeholder ? field.placeholder : value === "" ? "EMPTY STRING" : "NULL"}
85
85
  rows={5}
86
86
  class="
87
- bg-muted/30 text-xs
87
+ bg-muted-soft text-xs
88
88
  {destructive ? 'border-destructive bg-destructive/10' : ''}
89
89
  "
90
90
  bind:value
@@ -98,7 +98,7 @@
98
98
  <Input
99
99
  type="date"
100
100
  class="
101
- dateInput block w-full bg-muted/30 pr-9 text-xs
101
+ dateInput block w-full bg-muted-soft pr-9 text-xs
102
102
  {destructive ? 'border-destructive bg-destructive/10' : ''}
103
103
  "
104
104
  bind:value={
@@ -116,7 +116,7 @@
116
116
  <Input
117
117
  type="time"
118
118
  class="
119
- dateInput block w-full bg-muted/30 pr-9 text-xs
119
+ dateInput block w-full bg-muted-soft pr-9 text-xs
120
120
  {destructive ? 'border-destructive bg-destructive/10' : ''}
121
121
  "
122
122
  bind:value={
@@ -133,7 +133,7 @@
133
133
  <Input
134
134
  type="datetime-local"
135
135
  class="
136
- dateInput block w-full bg-muted/30 pr-9 text-xs
136
+ dateInput block w-full bg-muted-soft pr-9 text-xs
137
137
  {destructive ? 'border-destructive bg-destructive/10' : ''}
138
138
  "
139
139
  bind:value={
@@ -154,7 +154,7 @@
154
154
  <Select.Root type="single" bind:value>
155
155
  <Select.Trigger
156
156
  class="
157
- bg-muted/30 pr-9
157
+ bg-muted-soft pr-9
158
158
  {destructive ? 'border-destructive bg-destructive/10' : ''}
159
159
  "
160
160
  >
@@ -21,7 +21,7 @@
21
21
  <Input
22
22
  type="password"
23
23
  placeholder="••••••"
24
- class="bg-muted/30 text-xs {destructive ? 'border-destructive bg-destructive/10' : ''}"
24
+ class="bg-muted-soft text-xs {destructive ? 'border-destructive bg-destructive/10' : ''}"
25
25
  value={displayValue}
26
26
  oninput={onInput}
27
27
  />
@@ -22,67 +22,70 @@
22
22
  import Button from "../../ui/button/button.svelte";
23
23
  import { getStudioContext } from "../../../context";
24
24
  import { toast } from "svelte-sonner";
25
- import ExtensionsComponents from "../../extensionsComponents.svelte";
26
- import { getExtensionUtils } from "../../../extensions/extensionUtils";
27
25
  import { untrack } from "svelte";
28
26
 
29
27
  const { lobb, ctx } = getStudioContext();
30
28
  import { getChangedProperties } from "../../../utils";
31
- import { getField, getFieldIcon } from "../../dataTable/utils";
32
29
  import DetailViewChildren from "./detailViewChildren.svelte";
33
30
  import type { Snippet } from "svelte";
34
31
  import { getDefaultEntry } from "../utils";
35
32
  import type { Changes, ChildrenChanges } from "../utils";
36
- import FieldInput from "../fieldInput.svelte";
33
+ import DetailView from "../detailView.svelte";
37
34
  import Drawer from "../../drawer.svelte";
38
35
 
39
36
  let {
40
37
  collectionName,
41
- values = {},
38
+ values: passedValues = {} as Record<string, any>,
42
39
  showRelatedRecords = true,
43
40
  onCancel,
44
41
  onSuccessfullSave,
45
42
  title,
46
43
  submitButton,
47
44
  recordId,
48
- changes = $bindable<Changes | undefined>(undefined),
45
+ changes: passedChanges = $bindable<Changes | undefined>(undefined),
49
46
  }: UpdateDetailViewProp = $props();
50
47
 
51
- // Internal changes — used when not in recording mode, passed down to children
52
- let _changes = $state<Changes>({ data: {}, children: {} });
53
-
54
48
  // Recording mode = changes was passed from a parent component
55
- const isRecordingMode = $derived(changes !== undefined);
49
+ const isRecordingMode = passedChanges !== undefined;
50
+ if (!isRecordingMode) passedChanges = { data: {}, children: {} };
51
+ const changes = passedChanges as Changes;
56
52
 
57
53
  const fieldNames = Object.keys(ctx.meta.collections[collectionName].fields);
58
- let entry: Record<string, any> = $state(
59
- getDefaultEntry(ctx, fieldNames, collectionName, values),
60
- );
61
- const initialEntry = $state.snapshot(entry);
54
+ let values = $state(getDefaultEntry(ctx, fieldNames, collectionName, passedValues));
55
+ const initialValues = $state.snapshot(values);
62
56
  let fieldsErrors: Record<string, any> = $state({});
63
57
 
64
- // Tracks field edits into the active changes object.
58
+ const hasChanges = $derived(
59
+ Object.keys(changes.data).length > 0 ||
60
+ Object.values(changes.children).some(
61
+ (ch: ChildrenChanges) => ch.created.length || ch.updated.length || ch.deleted.length || ch.linked.length || ch.unlinked.length,
62
+ ),
63
+ );
64
+
65
+ // Tracks top-level field edits into changes.data.
65
66
  // Child ops (create/link/unlink/delete) are written directly by DataTable into changes.children.
66
67
  $effect(() => {
67
- const currentEntrySnap = $state.snapshot(entry);
68
+ const currentEntrySnap = $state.snapshot(values);
68
69
 
69
70
  untrack(() => {
70
- const target = changes ?? _changes;
71
- target.data = getChangedProperties(initialEntry, currentEntrySnap);
72
-
73
- if (!isRecordingMode) {
74
- console.log(`[${collectionName}] changes:`, $state.snapshot(target));
75
- }
71
+ changes.data = getChangedProperties(initialValues, currentEntrySnap);
76
72
  });
77
73
  });
78
74
 
75
+ // Separate logging effect — needs its own $effect so it tracks mutations to changes.children.
76
+ $effect(() => {
77
+ if (!isRecordingMode) {
78
+ console.log(`[${collectionName}] changes:`, $state.snapshot(changes));
79
+ }
80
+ });
81
+
79
82
  function buildApiChildren(children: Record<string, ChildrenChanges>): Record<string, any> | undefined {
80
83
  const result: Record<string, any> = {};
81
84
  for (const [collection, ops] of Object.entries(children)) {
82
85
  const hasOps = ops.created.length || ops.deleted.length || ops.linked.length || ops.unlinked.length;
83
86
  if (!hasOps) continue;
84
87
  result[collection] = {
85
- ...(ops.created.length ? { create: ops.created.map((c) => c.data) } : {}),
88
+ ...(ops.created.length ? { create: ops.created.map((op) => op.data) } : {}),
86
89
  ...(ops.deleted.length ? { delete: ops.deleted.map((r) => r.id) } : {}),
87
90
  ...(ops.linked.length ? { link: ops.linked.map((r) => r.id) } : {}),
88
91
  ...(ops.unlinked.length ? { unlink: ops.unlinked.map((r) => r.id) } : {}),
@@ -92,7 +95,7 @@
92
95
  }
93
96
 
94
97
  function handleCancel() {
95
- if (changes !== undefined) {
98
+ if (isRecordingMode) {
96
99
  changes.data = {};
97
100
  changes.children = {};
98
101
  }
@@ -100,8 +103,7 @@
100
103
  }
101
104
 
102
105
  async function handleSave() {
103
- const target = changes ?? _changes;
104
- const snap = $state.snapshot(target);
106
+ const snap = $state.snapshot(changes);
105
107
  const { id: _id, ...data } = snap.data;
106
108
  const children = buildApiChildren(snap.children);
107
109
 
@@ -121,6 +123,7 @@
121
123
  fieldsErrors = result.details;
122
124
  return;
123
125
  } else if (result.message) {
126
+ toast.error(result.message);
124
127
  return;
125
128
  }
126
129
  }
@@ -128,7 +131,7 @@
128
131
 
129
132
  // Real mode: also fire separate update requests for edited children
130
133
  if (!isRecordingMode) {
131
- for (const [collection, ops] of Object.entries(snap.children)) {
134
+ for (const [collection, ops] of Object.entries(snap.children) as [string, ChildrenChanges][]) {
132
135
  for (const updated of ops.updated) {
133
136
  await lobb.updateOne(collection, String(updated.id), updated.data);
134
137
  }
@@ -139,14 +142,6 @@
139
142
  toast.success(`The record was successfully updated`);
140
143
  onCancel?.();
141
144
  }
142
-
143
- const activeChanges = $derived(changes ?? _changes);
144
- const hasChanges = $derived(
145
- Object.keys(activeChanges.data).length > 0 ||
146
- Object.values(activeChanges.children).some(
147
- (c) => c.created.length || c.updated.length || c.deleted.length || c.linked.length || c.unlinked.length,
148
- ),
149
- );
150
145
  </script>
151
146
 
152
147
  <Drawer onHide={handleCancel}>
@@ -169,41 +164,9 @@
169
164
  </div>
170
165
  </div>
171
166
  <div class="flex-1 overflow-y-auto">
172
- <div class="flex flex-col gap-4 p-4">
173
- {#each fieldNames as fieldName}
174
- {#if !ctx.meta.collections[collectionName].fields[fieldName]?.ui?.hidden}
175
- {@const field = getField(ctx, fieldName, collectionName)}
176
- {@const FieldIcon = getFieldIcon(ctx, fieldName, collectionName)}
177
- <div class="flex flex-col gap-2">
178
- <div class="flex flex-1 items-end justify-between gap-2 text-xs">
179
- <div class="flex gap-2">
180
- <div class="h-fit">{field.label}</div>
181
- <div class="flex h-fit items-center gap-1 text-[0.7rem] text-muted-foreground">
182
- <FieldIcon size="12" />
183
- {field.type}
184
- </div>
185
- </div>
186
- <div>
187
- <ExtensionsComponents
188
- name="dvFields.topRight.{collectionName}.{fieldName}"
189
- utils={getExtensionUtils(lobb, ctx)}
190
- bind:value={entry[fieldName]}
191
- />
192
- </div>
193
- </div>
194
- <FieldInput
195
- {collectionName}
196
- {fieldName}
197
- bind:value={entry[fieldName]}
198
- bind:entry
199
- errorMessages={fieldsErrors[fieldName]}
200
- />
201
- </div>
202
- {/if}
203
- {/each}
204
- </div>
167
+ <DetailView {collectionName} bind:entry={values} {fieldsErrors} />
205
168
  {#if showRelatedRecords}
206
- <DetailViewChildren {collectionName} {entry} {activeChanges} />
169
+ <DetailViewChildren {collectionName} entry={values} activeChanges={changes} />
207
170
  {/if}
208
171
  </div>
209
172
  <div class="flex h-12 items-center justify-end gap-2 border-t px-4">
@@ -96,7 +96,7 @@
96
96
  });
97
97
  </script>
98
98
 
99
- <div class={cn("w-full resize-y rounded-md border bg-muted/30 shadow-sm", className)}>
99
+ <div class={cn("w-full resize-y rounded-md border bg-muted-soft shadow-sm", className)}>
100
100
  <div
101
101
  bind:this={editorContainer}
102
102
  class="editor pl-2"
@@ -26,7 +26,9 @@
26
26
 
27
27
  {#if Components.length}
28
28
  {#each Components as Component}
29
- <Component bind:value {...props} />
29
+ <Component bind:value {...props}>
30
+ {#if children}{@render children()}{/if}
31
+ </Component>
30
32
  {/each}
31
33
  {:else}
32
34
  {@render children?.()}
@@ -85,7 +85,7 @@
85
85
  placeholder={"NULL"}
86
86
  type="number"
87
87
  class="
88
- bg-muted/30 text-xs
88
+ bg-muted-soft text-xs
89
89
  {destructive ? 'border-destructive bg-destructive/10' : ''}
90
90
  "
91
91
  bind:value={
@@ -98,7 +98,7 @@
98
98
  <div class="relative z-10">
99
99
  <Input
100
100
  placeholder={"PARENT ID"}
101
- class="bg-muted/30 text-xs"
101
+ class="bg-muted-soft text-xs"
102
102
  disabled={true}
103
103
  />
104
104
  </div>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { AlertCircle, Check, FileText, LoaderCircle, Upload, X } from "lucide-svelte";
2
+ import { AlertCircle, Check, Download, FileText, LoaderCircle, Upload, X } from "lucide-svelte";
3
3
  import Button, { type ButtonProps } from "./ui/button/button.svelte";
4
4
  import { toast } from "svelte-sonner";
5
5
  import { getStudioContext } from "../context";
@@ -170,10 +170,15 @@
170
170
  }
171
171
  </script>
172
172
 
173
- <Button variant={rest.variant} class={rest.class} Icon={rest.Icon} onclick={showDrawer}>
174
- {#if rest.children}
175
- {@render rest.children()}
176
- {/if}
173
+ <Button variant={rest.variant} class={rest.class} onclick={showDrawer}>
174
+ <ExtensionsComponents
175
+ name="collections.import.button.content"
176
+ utils={getExtensionUtils(lobb, ctx)}
177
+ {collectionName}
178
+ >
179
+ <rest.Icon />
180
+ Import
181
+ </ExtensionsComponents>
177
182
  </Button>
178
183
 
179
184
  <Dialog.Root
@@ -231,7 +236,7 @@
231
236
  tabindex="0"
232
237
  onkeydown={(e) => e.key === "Enter" && fileInput.click()}
233
238
  >
234
- <Upload class="mb-3 h-8 w-8 text-muted-foreground" />
239
+ <Download class="mb-3 h-8 w-8 text-muted-foreground" />
235
240
  <p class="text-sm font-medium">Drop a file here or click to browse</p>
236
241
  <p class="mt-1 text-xs text-muted-foreground">Supports .csv and .json</p>
237
242
  </div>
@@ -244,7 +249,7 @@
244
249
  />
245
250
  {:else}
246
251
  <textarea
247
- class="block h-56 w-full resize-none rounded-md border bg-muted/30 p-3 font-mono text-sm focus:outline-none focus:ring-1 focus:ring-ring"
252
+ class="block h-56 w-full resize-none rounded-md border bg-muted-soft p-3 font-mono text-sm focus:outline-none focus:ring-1 focus:ring-ring"
248
253
  placeholder="Paste CSV or JSON here..."
249
254
  bind:value={pasteContent}
250
255
  ></textarea>
@@ -279,7 +284,6 @@
279
284
  data={transformedRows}
280
285
  columns={collectionColumns.filter((c) => c.id !== "id")}
281
286
  showCheckboxes={false}
282
- unifiedBgColor="bg-background"
283
287
  headerBorderTop={true}
284
288
  />
285
289
  </div>
@@ -328,7 +332,6 @@
328
332
  data={failedData}
329
333
  columns={[{ id: "__error", icon: AlertCircle }, ...collectionColumns.filter((c) => c.id !== "id")]}
330
334
  showCheckboxes={false}
331
- unifiedBgColor="bg-background"
332
335
  headerBorderTop={true}
333
336
  >
334
337
  {#snippet overrideCell(value, column)}
@@ -1,3 +1,10 @@
1
+ <script lang="ts">
2
+ import { onMount } from "svelte";
3
+ onMount(() => {
4
+ document.getElementById("app-loading")?.remove();
5
+ });
6
+ </script>
7
+
1
8
  <div class="min-h-screen flex items-center justify-center bg-background text-foreground p-6">
2
9
  <div class="max-w-md text-center space-y-4">
3
10
  <h1 class="text-2xl font-semibold">Lobb Studio</h1>
@@ -16,16 +16,19 @@
16
16
  import Separator from "./ui/separator/separator.svelte";
17
17
  import * as Tooltip from "./ui/tooltip";
18
18
  import * as Accordion from "./ui/accordion/index.js";
19
+ import { onMount } from "svelte";
19
20
 
20
21
  import { getStudioContext } from "../context";
21
22
  import { getDashboardNavs } from "../extensions/extensionUtils";
23
+ import { emitEvent } from "../eventSystem";
22
24
 
23
- const { ctx } = getStudioContext();
25
+ const { lobb, ctx } = getStudioContext();
24
26
  import { mediaQueries } from "../utils";
25
27
  import * as Popover from "./ui/popover";
26
- import { location } from "@wjfe/n-savant";
28
+ import { page } from "$app/state";
29
+ import { goto } from "$app/navigation";
27
30
 
28
- const sections: any = [
31
+ const rawSections: any[][] = [
29
32
  [
30
33
  {
31
34
  label: "Home",
@@ -39,13 +42,15 @@
39
42
  },
40
43
  {
41
44
  label: "Data Model",
42
- href: "/studio/datamodel/graph",
45
+ href: "/studio/datamodel",
43
46
  icon: Layers,
47
+ represents: "core_data_model",
44
48
  },
45
49
  {
46
50
  label: "Workflows",
47
51
  href: "/studio/workflows",
48
52
  icon: Workflow,
53
+ represents: "core_workflows",
49
54
  },
50
55
  ],
51
56
  [],
@@ -55,20 +60,72 @@
55
60
  const navs = getDashboardNavs(ctx);
56
61
 
57
62
  if (navs.top) {
58
- sections[0] = [...sections[0], ...navs.top];
63
+ rawSections[0] = [...rawSections[0], ...navs.top];
59
64
  }
60
65
  if (navs.middle) {
61
- sections[1] = [...sections[1], ...navs.middle];
66
+ rawSections[1] = [...rawSections[1], ...navs.middle];
62
67
  }
63
68
  if (navs.bottom) {
64
- sections[2] = [...sections[2], ...navs.bottom];
69
+ rawSections[2] = [...rawSections[2], ...navs.bottom];
65
70
  }
66
71
 
72
+ // Items without a `represents` are always visible. Items with one are
73
+ // gated by emitting auth.canAccess — the auth extension (or any
74
+ // drop-in replacement) decides based on the current user's session.
75
+ // Start empty so nothing flashes before the answers come back.
76
+ let sections: any[][] = $state([[], [], []]);
77
+
78
+ async function isItemVisible(item: any): Promise<boolean> {
79
+ if (!item.represents) return true;
80
+ const res = await emitEvent(
81
+ { lobb, ctx },
82
+ "auth.canAccess",
83
+ { collection: item.represents, action: "read" },
84
+ );
85
+ return res === true;
86
+ }
87
+
88
+ // Highlight the nav item matching the current URL. "/studio" requires an
89
+ // exact match (otherwise it would light up on every sub-route since it's a
90
+ // prefix of everything); other items use startsWith so sub-paths
91
+ // (e.g. /studio/collections/risks) still highlight their parent.
92
+ // Popover items with children are active when any of their children match.
93
+ const currentPath = $derived(page.url.pathname);
94
+ function isItemActive(item: any): boolean {
95
+ if (item.navs) return item.navs.some((c: any) => isItemActive(c));
96
+ if (!item.href) return false;
97
+ if (item.href === "/studio") return currentPath === "/studio";
98
+ return currentPath === item.href || currentPath.startsWith(item.href + "/");
99
+ }
100
+
101
+ // onMount is enough — Studio gets remounted on login/logout (see
102
+ // remountStudio in @lobb-js/studio), so by the time this component
103
+ // mounts, ctx.extensions.auth.user / permissions are already populated.
104
+ onMount(async () => {
105
+ const result: any[][] = [[], [], []];
106
+ for (let i = 0; i < rawSections.length; i++) {
107
+ for (const item of rawSections[i]) {
108
+ if (item.navs) {
109
+ const visibleChildren: any[] = [];
110
+ for (const child of item.navs) {
111
+ if (await isItemVisible(child)) visibleChildren.push(child);
112
+ }
113
+ if (visibleChildren.length && (await isItemVisible(item))) {
114
+ result[i].push({ ...item, navs: visibleChildren });
115
+ }
116
+ } else if (await isItemVisible(item)) {
117
+ result[i].push(item);
118
+ }
119
+ }
120
+ }
121
+ sections = result;
122
+ });
67
123
  </script>
68
124
 
69
125
  {#snippet section(section: any)}
70
126
  <div class="flex flex-col {isSmallScreen ? 'gap-0' : 'gap-2'}">
71
127
  {#each section as item}
128
+ {@const active = isItemActive(item)}
72
129
  {#if isSmallScreen}
73
130
  {#if !item.navs}
74
131
  <Button
@@ -76,11 +133,13 @@
76
133
  if (item.onclick) {
77
134
  item.onclick();
78
135
  } else {
79
- location.navigate(item.href);
136
+ goto(item.href);
80
137
  }
81
138
  isCollapsed = true;
82
139
  }}
83
- class="flex items-center justify-start flex-nowrap text-muted-foreground text-nowrap h-10 w-full"
140
+ class="flex items-center justify-start flex-nowrap text-nowrap h-10 w-full {active
141
+ ? 'bg-accent text-accent-foreground'
142
+ : 'text-muted-foreground'}"
84
143
  variant="ghost"
85
144
  size="icon"
86
145
  Icon={item.icon}
@@ -92,7 +151,9 @@
92
151
  <Accordion.Item class="border-b-0">
93
152
  <Accordion.Trigger class="justify-between p-0 h-10">
94
153
  <div
95
- class="flex items-center gap-2 text-muted-foreground"
154
+ class="flex items-center gap-2 {active
155
+ ? 'text-accent-foreground'
156
+ : 'text-muted-foreground'}"
96
157
  >
97
158
  <item.icon size="18" />
98
159
  <div class="text-nowrap">{item.label}</div>
@@ -100,16 +161,19 @@
100
161
  </Accordion.Trigger>
101
162
  <Accordion.Content class="pl-2 border-l">
102
163
  {#each item.navs as childItem}
164
+ {@const childActive = isItemActive(childItem)}
103
165
  <Button
104
166
  onclick={() => {
105
167
  if (childItem.onclick) {
106
168
  childItem.onclick();
107
169
  } else {
108
- location.navigate(item.href);
170
+ goto(item.href);
109
171
  }
110
172
  isCollapsed = true;
111
173
  }}
112
- class="flex items-center justify-start flex-nowrap text-muted-foreground text-nowrap h-8 w-full"
174
+ class="flex items-center justify-start flex-nowrap text-nowrap h-8 w-full {childActive
175
+ ? 'bg-accent text-accent-foreground'
176
+ : 'text-muted-foreground'}"
113
177
  variant="ghost"
114
178
  size="icon"
115
179
  Icon={childItem.icon}
@@ -133,7 +197,9 @@
133
197
  isCollapsed = true;
134
198
  }}
135
199
  href={item.href}
136
- class="text-muted-foreground"
200
+ class={active
201
+ ? 'bg-accent text-accent-foreground'
202
+ : 'text-muted-foreground'}
137
203
  variant="ghost"
138
204
  size="icon"
139
205
  Icon={item.icon}
@@ -142,7 +208,9 @@
142
208
  <Popover.Root>
143
209
  <Popover.Trigger>
144
210
  <Button
145
- class="text-muted-foreground"
211
+ class={active
212
+ ? 'bg-accent text-accent-foreground'
213
+ : 'text-muted-foreground'}
146
214
  variant="ghost"
147
215
  size="icon"
148
216
  Icon={item.icon}
@@ -155,12 +223,15 @@
155
223
  >
156
224
  <div class="py-1">
157
225
  {#each item.navs as childItem}
226
+ {@const childActive = isItemActive(childItem)}
158
227
  <div
159
228
  class="px-1 text-xs text-muted-foreground"
160
229
  >
161
230
  <Button
162
231
  variant="ghost"
163
- class="flex h-7 w-full justify-start p-2 text-xs font-normal text-muted-foreground"
232
+ class="flex h-7 w-full justify-start p-2 text-xs font-normal {childActive
233
+ ? 'bg-accent text-accent-foreground'
234
+ : 'text-muted-foreground'}"
164
235
  Icon={childItem.icon}
165
236
  onclick={() => {
166
237
  if (childItem.onclick) {
@@ -70,7 +70,7 @@
70
70
  }
71
71
  </script>
72
72
 
73
- <div class="flex h-9 w-full items-center gap-1.5 rounded-md border pl-1.5 pr-9 text-xs bg-muted/30 {destructive ? 'border-destructive bg-destructive/10' : ''}">
73
+ <div class="flex h-9 w-full items-center gap-1.5 rounded-md border pl-1.5 pr-9 text-xs bg-muted-soft {destructive ? 'border-destructive bg-destructive/10' : ''}">
74
74
  <!-- Collection picker -->
75
75
  <Popover.Root bind:open={collectionPopoverOpen}>
76
76
  <Popover.Trigger>