@lobb-js/studio 0.41.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/dist/components/confirmationDialog/confirmationDialog.svelte +1 -1
  2. package/dist/components/dataTable/dataTable.svelte +182 -50
  3. package/dist/components/dataTable/header.svelte +5 -2
  4. package/dist/components/dataTable/header.svelte.d.ts +1 -0
  5. package/dist/components/dataTable/table.svelte +10 -21
  6. package/dist/components/dataTable/table.svelte.d.ts +1 -0
  7. package/dist/components/detailView/create/createDetailView.svelte +43 -1
  8. package/dist/components/detailView/create/createDetailView.svelte.d.ts +1 -0
  9. package/dist/components/detailView/update/updateDetailView.svelte +39 -12
  10. package/dist/components/detailView/update/updateDetailViewButton.svelte +7 -0
  11. package/dist/components/detailView/update/updateDetailViewButton.svelte.d.ts +1 -0
  12. package/dist/components/drawer.svelte +1 -0
  13. package/dist/components/foreingKeyInput.svelte +27 -1
  14. package/dist/components/polymorphicInput.svelte +27 -3
  15. package/package.json +2 -2
  16. package/src/lib/components/confirmationDialog/confirmationDialog.svelte +1 -1
  17. package/src/lib/components/dataTable/dataTable.svelte +182 -50
  18. package/src/lib/components/dataTable/header.svelte +5 -2
  19. package/src/lib/components/dataTable/table.svelte +10 -21
  20. package/src/lib/components/detailView/create/createDetailView.svelte +43 -1
  21. package/src/lib/components/detailView/update/updateDetailView.svelte +39 -12
  22. package/src/lib/components/detailView/update/updateDetailViewButton.svelte +7 -0
  23. package/src/lib/components/drawer.svelte +1 -0
  24. package/src/lib/components/foreingKeyInput.svelte +27 -1
  25. package/src/lib/components/polymorphicInput.svelte +27 -3
@@ -41,6 +41,9 @@
41
41
  parentWidth?: number;
42
42
  select?: Select;
43
43
  tableWidth?: number;
44
+
45
+ // recording mode row visuals — cellIndex 0 = tools cell, 1+ = data/action cells
46
+ onCellClass?: (entry: Entry, cellIndex: number) => string;
44
47
  }
45
48
  </script>
46
49
 
@@ -79,6 +82,7 @@
79
82
  collapsible,
80
83
  select,
81
84
  tableWidth = $bindable(),
85
+ onCellClass,
82
86
  }: TableProps = $props();
83
87
 
84
88
  let expandedRows: boolean[] = $state(new Array(data.length).fill(false));
@@ -183,7 +187,7 @@
183
187
  flex items-center p-2.5 text-xs h-10
184
188
  border-r border-b gap-2
185
189
  {headerBorderTop ? 'border-t' : ''}
186
- bg-muted
190
+ bg-muted/50
187
191
  "
188
192
  >
189
193
  <!-- collapsable toggle -->
@@ -208,7 +212,7 @@
208
212
  class="
209
213
  sticky top-0 z-10
210
214
  flex items-center p-2.5 text-xs h-10
211
- bg-muted
215
+ bg-muted/50
212
216
  {lastColumn && !showLastColumnBorder ? '' : 'border-r'}
213
217
  border-b gap-2
214
218
  {headerBorderTop ? 'border-t' : ''}
@@ -235,7 +239,7 @@
235
239
  class="
236
240
  sticky top-0 right-0 z-20
237
241
  flex items-center p-2.5 h-10
238
- bg-muted
242
+ bg-muted/50
239
243
  border-l border-b
240
244
  {headerBorderTop ? 'border-t' : ''}
241
245
  "
@@ -247,12 +251,7 @@
247
251
  {@const lastRow = data.length - 1 === index}
248
252
  {#if selectedRecords || tools}
249
253
  <div
250
- class="
251
- sticky left-0
252
- flex items-center p-2.5 text-xs h-10
253
- bg-card
254
- border-r gap-2
255
- "
254
+ class="sticky left-0 flex items-center p-2.5 text-xs h-10 bg-card border-r gap-2 {onCellClass?.(entry, 0) ?? ''}"
256
255
  >
257
256
  <!-- collapsable toggle -->
258
257
  {#if showCollapsible}
@@ -294,12 +293,7 @@
294
293
  onclick={() => {
295
294
  select?.onSelect(entry);
296
295
  }}
297
- class="
298
- flex items-center p-2.5 text-xs h-10 text-nowrap overflow-clip
299
- {select ? 'cursor-pointer hover:bg-accent' : ''}
300
- bg-card
301
- {lastColumn && !showLastColumnBorder ? '' : 'border-r'}
302
- "
296
+ class="flex items-center p-2.5 text-xs h-10 text-nowrap overflow-clip bg-card {select ? 'cursor-pointer hover:bg-accent' : ''} {lastColumn && !showLastColumnBorder ? '' : 'border-r'} {onCellClass?.(entry, index + 1) ?? ''}"
303
297
  >
304
298
  {#if overrideCell}
305
299
  {@render overrideCell(fieldValue, column, entry)}
@@ -310,12 +304,7 @@
310
304
  {/each}
311
305
  {#if rowActions}
312
306
  <div
313
- class="
314
- sticky right-0 z-10
315
- flex items-center p-2.5 text-xs h-10
316
- border-l gap-2
317
- bg-card
318
- "
307
+ class="sticky right-0 z-10 flex items-center p-2.5 text-xs h-10 border-l gap-2 bg-card {onCellClass?.(entry, columns.length + 1) ?? ''}"
319
308
  >
320
309
  {@render rowActions?.(entry, index)}
321
310
  </div>
@@ -14,6 +14,7 @@
14
14
  submitButton?: SubmitButton;
15
15
  title?: Snippet<[string]>;
16
16
  onSuccessfullSave?: (entry: any) => Promise<void>;
17
+ onCreated?: (record: any) => Promise<void>;
17
18
  onCancel?: () => Promise<void>;
18
19
  onChanges?: (changes: Changes) => void;
19
20
  }
@@ -25,6 +26,8 @@
25
26
  import { getStudioContext } from "../../../context";
26
27
  import { toast } from "svelte-sonner";
27
28
  import { untrack } from "svelte";
29
+ import type { ChildrenChanges } from "../utils";
30
+ import { showDialog } from "../../../actions";
28
31
 
29
32
  const { lobb, ctx } = getStudioContext();
30
33
  import CreateDetailViewChildren from "./createDetailViewChildren.svelte";
@@ -38,6 +41,7 @@
38
41
  showRelatedRecords = true,
39
42
  onCancel,
40
43
  onSuccessfullSave,
44
+ onCreated,
41
45
  title,
42
46
  submitButton,
43
47
  onChanges,
@@ -46,6 +50,31 @@
46
50
  const isRecordingMode = onChanges !== undefined;
47
51
  let changes = $state<Changes>({ data: {}, children: {} });
48
52
 
53
+ const totalChangeCount = $derived.by(() => {
54
+ let count = Object.keys(changes.data).length;
55
+ for (const ch of Object.values(changes.children) as ChildrenChanges[]) {
56
+ count += ch.created.length + ch.updated.length + ch.deleted.length + ch.linked.length + ch.unlinked.length;
57
+ }
58
+ return count;
59
+ });
60
+
61
+ const hasChildChanges = $derived(
62
+ Object.values(changes.children).some((ch: ChildrenChanges) =>
63
+ ch.created.length || ch.linked.length
64
+ )
65
+ );
66
+
67
+ const changeSummaryLines = $derived.by(() => {
68
+ const lines: string[] = [];
69
+ const fieldCount = Object.keys(changes.data).length;
70
+ if (fieldCount > 0) lines.push(`${fieldCount} field${fieldCount > 1 ? 's' : ''} filled`);
71
+ for (const [col, ch] of Object.entries(changes.children) as [string, ChildrenChanges][]) {
72
+ if (ch.created.length) lines.push(`${ch.created.length} created in ${col}`);
73
+ if (ch.linked.length) lines.push(`${ch.linked.length} linked in ${col}`);
74
+ }
75
+ return lines;
76
+ });
77
+
49
78
  const fieldNames = Object.keys(ctx.meta.collections[collectionName].fields);
50
79
  let values = $state(getDefaultEntry(ctx, fieldNames, collectionName, passedValues));
51
80
  let fieldsErrors: Record<string, any> = $state({});
@@ -82,17 +111,27 @@
82
111
  }
83
112
 
84
113
  async function handleSave() {
114
+ if (!isRecordingMode && hasChildChanges && changeSummaryLines.length > 0) {
115
+ const confirmed = await showDialog(
116
+ "Confirm changes",
117
+ changeSummaryLines.map(l => `• ${l}`).join('\n')
118
+ );
119
+ if (!confirmed) return;
120
+ }
121
+
85
122
  const snap = $state.snapshot(changes);
86
123
  const response = await lobb.createOne(collectionName, buildPayload(snap), undefined, isRecordingMode);
87
124
 
88
125
  if (response.status === 204) {
89
126
  onChanges?.(snap);
90
127
  if (onSuccessfullSave) await onSuccessfullSave(snap);
128
+ await onCreated?.(snap.data);
91
129
  toast.success(`The record was successfully created`);
92
130
  handleCancel();
93
131
  return;
94
132
  }
95
133
 
134
+ let createdRecord: any = null;
96
135
  if (!response.bodyUsed) {
97
136
  const result = await response.json();
98
137
  if (response.status >= 400) {
@@ -104,10 +143,12 @@
104
143
  return;
105
144
  }
106
145
  }
146
+ createdRecord = result.data ?? result;
107
147
  }
108
148
 
109
149
  onChanges?.(snap);
110
150
  if (onSuccessfullSave) await onSuccessfullSave(snap);
151
+ await onCreated?.(createdRecord ?? snap.data);
111
152
  toast.success(`The record was successfully created`);
112
153
  onCancel?.();
113
154
  }
@@ -152,9 +193,10 @@
152
193
  variant="default"
153
194
  size="sm"
154
195
  Icon={submitButton?.icon ? submitButton.icon : Plus}
196
+ aria-label={submitButton?.text ?? "Create record"}
155
197
  onclick={handleSave}
156
198
  >
157
- {submitButton?.text ? submitButton.text : "Create"}
199
+ {submitButton?.text ?? "Create"}{totalChangeCount > 0 ? ` (${totalChangeCount})` : ''}
158
200
  </Button>
159
201
  </div>
160
202
  </div>
@@ -27,6 +27,7 @@
27
27
  import { getStudioContext } from "../../../context";
28
28
  import { toast } from "svelte-sonner";
29
29
  import { untrack } from "svelte";
30
+ import { showDialog } from "../../../actions";
30
31
 
31
32
  const { lobb, ctx } = getStudioContext();
32
33
  import { getChangedProperties } from "../../../utils";
@@ -69,6 +70,34 @@
69
70
  ),
70
71
  );
71
72
 
73
+ const totalChangeCount = $derived.by(() => {
74
+ let count = Object.keys(localChanges.data).length;
75
+ for (const ch of Object.values(localChanges.children) as ChildrenChanges[]) {
76
+ count += ch.created.length + ch.updated.length + ch.deleted.length + ch.linked.length + ch.unlinked.length;
77
+ }
78
+ return count;
79
+ });
80
+
81
+ const hasChildChanges = $derived(
82
+ Object.values(localChanges.children).some((ch: ChildrenChanges) =>
83
+ ch.created.length || ch.updated.length || ch.deleted.length || ch.linked.length || ch.unlinked.length
84
+ )
85
+ );
86
+
87
+ const changeSummaryLines = $derived.by(() => {
88
+ const lines: string[] = [];
89
+ const fieldCount = Object.keys(localChanges.data).length;
90
+ if (fieldCount > 0) lines.push(`${fieldCount} field${fieldCount > 1 ? 's' : ''} changed`);
91
+ for (const [col, ch] of Object.entries(localChanges.children) as [string, ChildrenChanges][]) {
92
+ if (ch.created.length) lines.push(`${ch.created.length} created in ${col}`);
93
+ if (ch.linked.length) lines.push(`${ch.linked.length} linked in ${col}`);
94
+ if (ch.updated.length) lines.push(`${ch.updated.length} edited in ${col}`);
95
+ if (ch.deleted.length) lines.push(`${ch.deleted.length} deleted from ${col}`);
96
+ if (ch.unlinked.length) lines.push(`${ch.unlinked.length} unlinked from ${col}`);
97
+ }
98
+ return lines;
99
+ });
100
+
72
101
  $effect(() => {
73
102
  const currentEntrySnap = $state.snapshot(values);
74
103
 
@@ -77,17 +106,6 @@
77
106
  });
78
107
  });
79
108
 
80
- $effect(() => {
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));
88
- }
89
- });
90
-
91
109
  function buildPayload(changes: Changes): { data: Record<string, any>; children?: Record<string, any> } {
92
110
  const { id: _id, ...data } = changes.data;
93
111
  const children = buildChildren(changes.children);
@@ -119,6 +137,14 @@
119
137
  }
120
138
 
121
139
  async function handleSave() {
140
+ if (!isRecordingMode && hasChildChanges && changeSummaryLines.length > 0) {
141
+ const confirmed = await showDialog(
142
+ "Confirm changes",
143
+ changeSummaryLines.map(l => `• ${l}`).join('\n')
144
+ );
145
+ if (!confirmed) return;
146
+ }
147
+
122
148
  const snap = $state.snapshot(localChanges);
123
149
  const response = await lobb.updateOne(collectionName, recordId, buildPayload(snap), isRecordingMode);
124
150
 
@@ -189,10 +215,11 @@
189
215
  variant="default"
190
216
  size="sm"
191
217
  Icon={submitButton?.icon ? submitButton.icon : Pencil}
218
+ aria-label={submitButton?.text ?? "Update"}
192
219
  onclick={handleSave}
193
220
  disabled={!hasChanges}
194
221
  >
195
- {submitButton?.text ? submitButton.text : "Update"}
222
+ {submitButton?.text ?? "Update"}{totalChangeCount > 0 ? ` (${totalChangeCount})` : ''}
196
223
  </Button>
197
224
  </div>
198
225
  </div>
@@ -11,6 +11,7 @@
11
11
  class?: ButtonProps["class"];
12
12
  Icon?: ButtonProps["Icon"];
13
13
  children?: ButtonProps["children"];
14
+ "aria-label"?: string;
14
15
  }
15
16
 
16
17
  let props: LocalProp = $props();
@@ -20,6 +21,11 @@
20
21
  const { lobb } = getStudioContext();
21
22
 
22
23
  async function openView() {
24
+ if (props.values) {
25
+ values = props.values;
26
+ open = true;
27
+ return;
28
+ }
23
29
  const params = {
24
30
  fields: "*",
25
31
  filter: { id: props.recordId },
@@ -37,6 +43,7 @@
37
43
  size={props.size}
38
44
  class={props.class}
39
45
  Icon={props.Icon}
46
+ aria-label={props["aria-label"]}
40
47
  onclick={openView}
41
48
  >
42
49
  {#if props.children}
@@ -36,6 +36,7 @@
36
36
  ></button>
37
37
 
38
38
  <div
39
+ role="dialog"
39
40
  transition:slide={{ axis: position === "bottom" ? "y" : "x" }}
40
41
  class={position === "bottom"
41
42
  ? "fixed bottom-0 left-0 z-40 flex h-[60vh] w-full flex-col border-t bg-card"
@@ -3,9 +3,11 @@
3
3
  import Input from "./ui/input/input.svelte";
4
4
  import SelectRecord from "./selectRecord.svelte";
5
5
  import UpdateDetailViewButton from "./detailView/update/updateDetailViewButton.svelte";
6
+ import CreateDetailView from "./detailView/create/createDetailView.svelte";
6
7
  import { getCollectionPrimaryField } from "./dataTable/utils";
7
8
  import { getStudioContext } from "../context";
8
- import { ExternalLink } from "lucide-svelte";
9
+ import { ExternalLink, Plus } from "lucide-svelte";
10
+ import Button from "./ui/button/button.svelte";
9
11
 
10
12
  const { lobb, ctx } = getStudioContext();
11
13
 
@@ -28,6 +30,7 @@
28
30
  }: LocalProps = $props();
29
31
 
30
32
  let displayName = $state<string | null>(null);
33
+ let createDrawerOpen = $state(false);
31
34
 
32
35
  onMount(async () => {
33
36
  if (value == null) return;
@@ -51,6 +54,12 @@
51
54
  displayName = primaryFieldName ? String(selectedEntry[primaryFieldName]) : null;
52
55
  }
53
56
 
57
+ async function handleCreated(record: any) {
58
+ const primaryFieldName = getCollectionPrimaryField(ctx, collectionName);
59
+ value = record.id;
60
+ displayName = primaryFieldName ? String(record[primaryFieldName]) : null;
61
+ }
62
+
54
63
  const idIsZero = $derived(value === 0);
55
64
  </script>
56
65
 
@@ -71,6 +80,14 @@
71
80
  {displayName}
72
81
  </div>
73
82
  {/if}
83
+ <Button
84
+ class="h-6 px-2 font-normal text-xs"
85
+ variant="outline"
86
+ Icon={Plus}
87
+ onclick={() => (createDrawerOpen = true)}
88
+ >
89
+ Create
90
+ </Button>
74
91
  <SelectRecord
75
92
  class="h-6 px-2 font-normal text-xs"
76
93
  variant="outline"
@@ -78,6 +95,7 @@
78
95
  {collectionName}
79
96
  {fieldName}
80
97
  onSelect={handleSelect}
98
+ text="Select"
81
99
  {entry}
82
100
  />
83
101
  </div>
@@ -103,3 +121,11 @@
103
121
  />
104
122
  </div>
105
123
  {/if}
124
+
125
+ {#if createDrawerOpen}
126
+ <CreateDetailView
127
+ collectionName={collectionName}
128
+ onCreated={handleCreated}
129
+ onCancel={async () => { createDrawerOpen = false; }}
130
+ />
131
+ {/if}
@@ -6,7 +6,8 @@
6
6
  import * as Popover from "./ui/popover/index";
7
7
  import { getCollectionPrimaryField } from "./dataTable/utils";
8
8
  import { getStudioContext } from "../context";
9
- import { ArrowLeft, Link, ChevronDown } from "lucide-svelte";
9
+ import { ArrowLeft, Link, ChevronDown, Plus } from "lucide-svelte";
10
+ import CreateDetailView from "./detailView/create/createDetailView.svelte";
10
11
 
11
12
  const { ctx, lobb } = getStudioContext();
12
13
 
@@ -32,6 +33,7 @@
32
33
  let displayName = $state<string | null>(null);
33
34
  let collectionPopoverOpen = $state(false);
34
35
  let recordDrawerOpen = $state(false);
36
+ let createDrawerOpen = $state(false);
35
37
 
36
38
  onMount(async () => {
37
39
  if (selectedCollection == null || selectedId == null) return;
@@ -68,6 +70,12 @@
68
70
  displayName = primaryFieldName ? String(record[primaryFieldName]) : null;
69
71
  recordDrawerOpen = false;
70
72
  }
73
+
74
+ async function onPolyCreated(record: any) {
75
+ const primaryFieldName = getCollectionPrimaryField(ctx, selectedCollection!);
76
+ entry = { ...entry, [idField]: record.id };
77
+ displayName = primaryFieldName ? String(record[primaryFieldName]) : null;
78
+ }
71
79
  </script>
72
80
 
73
81
  <div class="flex h-9 w-full items-center gap-1.5 rounded-md border pl-1.5 pr-9 text-xs bg-muted {destructive ? 'border-destructive bg-destructive/10' : ''}">
@@ -115,15 +123,23 @@
115
123
  </div>
116
124
  {/if}
117
125
 
118
- <!-- Select record button -->
126
+ <!-- Create / Select record buttons -->
119
127
  {#if selectedCollection}
128
+ <Button
129
+ class="h-6 shrink-0 px-2 font-normal text-xs"
130
+ variant="outline"
131
+ onclick={() => (createDrawerOpen = true)}
132
+ >
133
+ <Plus size="13" />
134
+ Create
135
+ </Button>
120
136
  <Button
121
137
  class="h-6 shrink-0 px-2 font-normal text-xs"
122
138
  variant="outline"
123
139
  onclick={() => (recordDrawerOpen = true)}
124
140
  >
125
141
  <Link size="13" />
126
- Select Record
142
+ Select
127
143
  </Button>
128
144
  {/if}
129
145
  </div>
@@ -155,3 +171,11 @@
155
171
  </div>
156
172
  </Drawer>
157
173
  {/if}
174
+
175
+ {#if createDrawerOpen && selectedCollection}
176
+ <CreateDetailView
177
+ collectionName={selectedCollection}
178
+ onCreated={onPolyCreated}
179
+ onCancel={async () => { createDrawerOpen = false; }}
180
+ />
181
+ {/if}