@lobb-js/studio 0.43.0 → 0.44.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.
@@ -4,12 +4,8 @@
4
4
  import DataTable from "./dataTable/dataTable.svelte";
5
5
  import Drawer from "./drawer.svelte";
6
6
  import * as Popover from "./ui/popover/index";
7
- import { getStudioContext } from "../context";
8
- import { ArrowLeft, Link, ChevronDown, Plus, Pencil, Unlink, Trash, RotateCcw, RefreshCw } from "lucide-svelte";
7
+ import { ArrowLeft, Link, ChevronDown, Plus } from "lucide-svelte";
9
8
  import CreateDetailView from "./detailView/create/createDetailView.svelte";
10
- import UpdateDetailView from "./detailView/update/updateDetailView.svelte";
11
-
12
- const { ctx, lobb } = getStudioContext();
13
9
 
14
10
  interface Props {
15
11
  collectionField: string;
@@ -29,41 +25,33 @@
29
25
  destructive,
30
26
  }: Props = $props();
31
27
 
32
-
33
28
  const selectedCollection = $derived(entry[collectionField] ?? null);
34
29
  const selectedId = $derived(entry[idField] ?? null);
35
30
  const virtualVal = $derived(entry[virtualField] ?? null);
36
31
 
37
32
  let initialId = $state<number | null | undefined>(undefined);
38
- let unlinked = $state(false);
39
- onMount(() => { initialId = entry[idField] ?? null; });
40
-
41
- // State derived from virtual field and real fields
42
- const isPendingCreate = $derived(virtualVal && typeof virtualVal === 'object' && virtualVal.create);
43
- const isPendingEdit = $derived(virtualVal && typeof virtualVal === 'object' && virtualVal.update);
44
- const isStagedUnlink = $derived(unlinked);
45
- const isStagedDelete = $derived(virtualVal && typeof virtualVal === 'object' && virtualVal.delete === true);
46
- const hasRealValue = $derived(selectedId != null && !isPendingCreate && !isPendingEdit && !isStagedUnlink && !isStagedDelete);
47
- const isNewLink = $derived(hasRealValue && initialId !== undefined && initialId == null && selectedId !== initialId);
48
- const isReplaced = $derived(hasRealValue && initialId !== undefined && initialId != null && selectedId !== initialId);
49
- const isEmpty = $derived(!unlinked && (!selectedCollection || (selectedId == null && !isPendingCreate && !isPendingEdit && !isStagedDelete)));
33
+ let initialCollection = $state<string | null | undefined>(undefined);
34
+ onMount(() => {
35
+ initialId = entry[idField] ?? null;
36
+ initialCollection = entry[collectionField] ?? null;
37
+ });
38
+
39
+ const isPendingCreate = $derived(virtualVal && typeof virtualVal === 'object' && virtualVal.create);
40
+ const isChanged = $derived(
41
+ initialId !== undefined &&
42
+ !isPendingCreate &&
43
+ (selectedId !== initialId || selectedCollection !== initialCollection)
44
+ );
50
45
 
51
46
  const bgClass = $derived(
52
- isPendingCreate ? '!bg-green-500/5 border-green-500/40' :
53
- isNewLink ? '!bg-blue-500/5 border-blue-500/40' :
54
- isReplaced ? '!bg-orange-500/5 border-orange-500/40' :
55
- isPendingEdit ? '!bg-orange-500/5 border-orange-500/40' :
56
- isStagedUnlink ? '!bg-slate-500/5 border-slate-500/40' :
57
- isStagedDelete ? '!bg-red-500/5 border-red-500/40' :
47
+ isPendingCreate ? '!bg-green-500/5 border-green-500/40' :
48
+ isChanged ? '!bg-orange-500/5' :
58
49
  ''
59
50
  );
60
51
 
61
52
  let collectionPopoverOpen = $state(false);
62
53
  let recordDrawerOpen = $state(false);
63
54
  let createDrawerOpen = $state(false);
64
- let editDrawerOpen = $state(false);
65
- let editValues: Record<string, any> | undefined = $state(undefined);
66
- let originalSnapshot: { collection: string; id: number } | null = $state(null);
67
55
 
68
56
  function onCollectionChange(col: string) {
69
57
  collectionPopoverOpen = false;
@@ -75,7 +63,7 @@
75
63
  function onIdChange(e: Event) {
76
64
  const raw = (e.target as HTMLInputElement).value;
77
65
  const id = raw === "" ? null : Number(raw);
78
- entry = { ...entry, [idField]: id };
66
+ entry = { ...entry, [idField]: id, [virtualField]: undefined };
79
67
  }
80
68
 
81
69
  function onRecordSelect(record: any) {
@@ -90,50 +78,9 @@
90
78
  entry = { ...entry, [idField]: record.id, [virtualField]: undefined };
91
79
  }
92
80
  }
93
-
94
- async function openEdit() {
95
- const res = await lobb.findAll(selectedCollection!, { filter: { id: selectedId }, limit: 1 });
96
- const result = await res.json();
97
- editValues = result.data[0];
98
- editDrawerOpen = true;
99
- }
100
-
101
- function handleEditChanges(changes: import('./detailView/utils').Changes) {
102
- if (Object.keys(changes.data).length === 0) {
103
- entry = { ...entry, [virtualField]: undefined };
104
- } else {
105
- entry = { ...entry, [virtualField]: { collection: selectedCollection, id: selectedId, update: changes.data } };
106
- }
107
- }
108
-
109
- function handleUnlink() {
110
- if (hasRealValue) {
111
- originalSnapshot = { collection: selectedCollection!, id: selectedId! };
112
- unlinked = true;
113
- entry = { ...entry, [collectionField]: null, [idField]: null, [virtualField]: undefined };
114
- }
115
- }
116
-
117
- async function handleDelete() {
118
- if (hasRealValue) {
119
- originalSnapshot = { collection: selectedCollection!, id: selectedId! };
120
- entry = { ...entry, [virtualField]: { delete: true } };
121
- }
122
- }
123
-
124
- function handleRevert() {
125
- if (originalSnapshot) {
126
- entry = { ...entry, [collectionField]: originalSnapshot.collection, [idField]: originalSnapshot.id, [virtualField]: undefined };
127
- originalSnapshot = null;
128
- } else {
129
- entry = { ...entry, [collectionField]: null, [idField]: null, [virtualField]: undefined };
130
- }
131
- unlinked = false;
132
- }
133
81
  </script>
134
82
 
135
83
  <div class="flex h-9 w-full items-center gap-1.5 rounded-md border pl-1.5 pr-9 text-xs bg-muted {bgClass} {destructive ? '!bg-destructive/10 border-destructive' : ''}">
136
- <!-- Collection picker -->
137
84
  <Popover.Root bind:open={collectionPopoverOpen}>
138
85
  <Popover.Trigger>
139
86
  {#snippet child({ props })}
@@ -161,25 +108,15 @@
161
108
  </Popover.Content>
162
109
  </Popover.Root>
163
110
 
164
- <!-- ID input (only editable when real value or empty) -->
165
111
  <input
166
- placeholder="NULL"
112
+ placeholder={isPendingCreate ? "AUTO GENERATED" : "NULL"}
167
113
  type="number"
168
114
  class="min-w-0 flex-1 bg-transparent outline-none text-xs placeholder:text-muted-foreground"
169
- value={isStagedUnlink || isStagedDelete ? (virtualVal?.id ?? '') : (selectedId ?? "")}
170
- disabled={isPendingCreate || isPendingEdit}
115
+ value={selectedId ?? ""}
171
116
  oninput={onIdChange}
172
117
  />
173
118
 
174
- <!-- Action buttons -->
175
- {#if isStagedUnlink || isStagedDelete || isPendingCreate || isPendingEdit}
176
- <Button class="h-5 w-5 px-0 shrink-0 hover:bg-transparent text-muted-foreground" variant="ghost" Icon={RotateCcw} onclick={handleRevert} title="Revert"></Button>
177
- {:else if hasRealValue}
178
- <Button class="h-5 w-5 px-0 shrink-0 hover:bg-transparent text-muted-foreground" variant="ghost" Icon={Trash} onclick={handleDelete} title="Delete record"></Button>
179
- <Button class="h-5 w-5 px-0 shrink-0 hover:bg-transparent text-muted-foreground" variant="ghost" Icon={Unlink} onclick={handleUnlink} title="Unlink"></Button>
180
- <Button class="h-5 w-5 px-0 shrink-0 hover:bg-transparent text-muted-foreground" variant="ghost" Icon={Pencil} onclick={openEdit} title="Edit record"></Button>
181
- <Button class="h-5 w-5 px-0 shrink-0 hover:bg-transparent text-muted-foreground" variant="ghost" Icon={RefreshCw} onclick={() => (recordDrawerOpen = true)} title="Replace"></Button>
182
- {:else if selectedCollection && !isPendingCreate}
119
+ {#if selectedCollection}
183
120
  <Button class="h-6 shrink-0 px-2 font-normal text-xs" variant="outline" onclick={() => (createDrawerOpen = true)}>
184
121
  <Plus size="13" />
185
122
  Create
@@ -218,13 +155,3 @@
218
155
  onCancel={async () => { createDrawerOpen = false; }}
219
156
  />
220
157
  {/if}
221
-
222
- {#if editDrawerOpen && editValues && selectedCollection}
223
- <UpdateDetailView
224
- collectionName={selectedCollection}
225
- recordId={String(editValues.id)}
226
- values={editValues}
227
- onChanges={handleEditChanges}
228
- onCancel={async () => { editDrawerOpen = false; editValues = undefined; }}
229
- />
230
- {/if}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lobb-js/studio",
3
3
  "license": "UNLICENSED",
4
- "version": "0.43.0",
4
+ "version": "0.44.0",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"
package/src/app.css CHANGED
@@ -24,7 +24,7 @@
24
24
  --secondary: oklch(0.97 0.001 138);
25
25
  --secondary-foreground: oklch(0.20 0.012 138);
26
26
 
27
- --muted: oklch(0.97 0.001 138);
27
+ --muted: oklch(0.98 0.001 138);
28
28
  --muted-foreground: oklch(0.55 0.012 138);
29
29
 
30
30
  --accent: oklch(0.95 0.001 138);
@@ -52,7 +52,7 @@
52
52
  --secondary: oklch(0.24 0.012 138);
53
53
  --secondary-foreground: oklch(0.94 0.012 138);
54
54
 
55
- --muted: oklch(0.24 0.012 138);
55
+ --muted: oklch(0.28 0.012 138);
56
56
  --muted-foreground: oklch(0.65 0.012 138);
57
57
 
58
58
  --accent: oklch(0.27 0.012 138);
@@ -75,6 +75,7 @@
75
75
  }: Props = $props();
76
76
 
77
77
  const isRecordingMode = onChanges !== undefined;
78
+ const isSelectMode = $derived(tableProps?.select != null);
78
79
  let localChanges = $state<ChildrenChanges>(
79
80
  untrack(() => changes) ?? { created: [], updated: [], deleted: [], linked: [], unlinked: [] }
80
81
  );
@@ -222,6 +223,9 @@
222
223
  (ctx.meta.collections[collectionName]?.children ?? [])
223
224
  .some((c: any) => c.type === "fk" || c.type === "m2m" || c.type === "polymorphic")
224
225
  );
226
+ // Select-mode drawer has no checkbox, edit, delete, or unlink — the
227
+ // left-sticky tools column would render empty, so drop it entirely.
228
+ const showLeftTools = $derived(!isSelectMode);
225
229
  let childrenDrawerEntry = $state<Record<string, any> | null>(null);
226
230
 
227
231
  // requests the data from the server when the params is changed
@@ -407,6 +411,7 @@
407
411
  bind:selectedRecords
408
412
  {showImport}
409
413
  {showFilter}
414
+ showCreate={!isSelectMode}
410
415
  {loading}
411
416
  {parentContext}
412
417
  onLink={isRecordingMode ? handleLink : undefined}
@@ -463,6 +468,7 @@
463
468
  bind:selectedRecords
464
469
  bind:tableWidth={dataTableWidth}
465
470
  {...tableProps}
471
+ {showLeftTools}
466
472
  rowActions={hasRowActions ? rowActionsSnippet : undefined}
467
473
  onCellClass={isRecordingMode ? onCellClass : undefined}>
468
474
  {#snippet preTools(entry)}
@@ -480,7 +486,7 @@
480
486
  {/snippet}
481
487
  {#snippet tools(entry)}
482
488
  {#if entry._recordingState !== 'deleted' && entry._recordingState !== 'unlinked'}
483
- {#if showEdit}
489
+ {#if showEdit && !isSelectMode}
484
490
  {@const isPending = entry._recordingState === 'created'}
485
491
  <CanAccess collection={collectionName} action="update">
486
492
  <UpdateDetailViewButton
@@ -20,11 +20,13 @@
20
20
  // • anything else → explicit $and/$or arrays.
21
21
 
22
22
  import * as Select from "../ui/select/index.js";
23
+ import * as Popover from "../ui/popover/index.js";
23
24
  import Button from "../ui/button/button.svelte";
24
- import { Plus, X, Boxes, Settings2 } from "lucide-svelte";
25
+ import { ChevronDown, Plus, X, Boxes, Settings2 } from "lucide-svelte";
25
26
  import { getStudioContext } from "../../context";
26
27
  import { getFieldIcon } from "./utils";
27
28
  import { getFieldRelationTarget } from "../../relations";
29
+ import FieldPicker from "./fieldPicker.svelte";
28
30
 
29
31
  const { ctx } = getStudioContext();
30
32
 
@@ -43,6 +45,9 @@
43
45
  isEmpty = $bindable(true),
44
46
  }: Props = $props();
45
47
 
48
+ // Per-rule open state for the field-picker popovers.
49
+ let fieldPickerOpen = $state<Record<string, boolean>>({});
50
+
46
51
  type OperatorDef = {
47
52
  value: string;
48
53
  label: string;
@@ -471,30 +476,28 @@
471
476
  {@const currentOp = ops.find((o) => o.value === rule.operator) ?? ops[0]}
472
477
  {@const kind = getFieldKind(rule.field)}
473
478
  {@const FieldIcon = getFieldIcon(ctx, rule.field, collectionName)}
474
- <!-- Field picker -->
475
- <Select.Root
476
- type="single"
477
- value={rule.field}
478
- onValueChange={(v) => v && patchRuleInPlace(rule.id, { field: v })}
479
- >
480
- <Select.Trigger class="bg-muted h-7 w-36 text-xs">
481
- <div class="inline-flex items-center gap-1.5">
479
+ <!-- Field picker — typeahead popover, same widget Sort uses -->
480
+ <Popover.Root bind:open={fieldPickerOpen[rule.id]}>
481
+ <Popover.Trigger
482
+ class="inline-flex h-7 w-36 items-center justify-between gap-1.5 rounded-md border bg-muted px-2 text-xs"
483
+ >
484
+ <div class="inline-flex items-center gap-1.5 truncate">
482
485
  <FieldIcon size="13" />
483
- {rule.field}
486
+ <span class="truncate">{rule.field}</span>
484
487
  </div>
485
- </Select.Trigger>
486
- <Select.Content>
487
- {#each allFieldNames as fname}
488
- {@const OptionIcon = getFieldIcon(ctx, fname, collectionName)}
489
- <Select.Item value={fname}>
490
- <div class="inline-flex items-center gap-1.5">
491
- <OptionIcon size="13" />
492
- {fname}
493
- </div>
494
- </Select.Item>
495
- {/each}
496
- </Select.Content>
497
- </Select.Root>
488
+ <ChevronDown size="13" class="text-muted-foreground shrink-0" />
489
+ </Popover.Trigger>
490
+ <Popover.Content class="w-64 p-2">
491
+ <FieldPicker
492
+ {collectionName}
493
+ placeholder="Pick a field…"
494
+ onPick={(fname: string) => {
495
+ patchRuleInPlace(rule.id, { field: fname });
496
+ fieldPickerOpen[rule.id] = false;
497
+ }}
498
+ />
499
+ </Popover.Content>
500
+ </Popover.Root>
498
501
 
499
502
  <!-- Operator picker -->
500
503
  <Select.Root
@@ -26,6 +26,7 @@
26
26
  onCreate?: (changes: Changes) => void;
27
27
  showImport?: boolean;
28
28
  showFilter?: boolean;
29
+ showCreate?: boolean;
29
30
  loading?: boolean;
30
31
  left?: Snippet<[]>;
31
32
  excludeIds?: (string | number)[];
@@ -40,6 +41,7 @@
40
41
  onCreate,
41
42
  showImport = true,
42
43
  showFilter = true,
44
+ showCreate = true,
43
45
  loading = false,
44
46
  left,
45
47
  excludeIds = [],
@@ -154,7 +156,7 @@
154
156
  {headerIsSmall ? "" : "Refresh"}
155
157
  {/if}
156
158
  </Button>
157
- {#if showImport}
159
+ {#if showImport && showCreate}
158
160
  <CanAccess collection={collectionName} action="create">
159
161
  <ImportButton
160
162
  {collectionName}
@@ -183,17 +185,19 @@
183
185
  {headerIsSmall ? "" : "Link"}
184
186
  </SelectRecord>
185
187
  {/if}
186
- <CanAccess collection={collectionName} action="create">
187
- <CreateDetailViewButton
188
- {collectionName}
189
- variant="default"
190
- size="sm"
191
- Icon={Plus}
192
- onChanges={onCreate ? handleCreate : undefined}
193
- onSuccessfullSave={onCreate ? undefined : handleCreate}
194
- >
195
- {headerIsSmall ? "" : "Create"}
196
- </CreateDetailViewButton>
197
- </CanAccess>
188
+ {#if showCreate}
189
+ <CanAccess collection={collectionName} action="create">
190
+ <CreateDetailViewButton
191
+ {collectionName}
192
+ variant="default"
193
+ size="sm"
194
+ Icon={Plus}
195
+ onChanges={onCreate ? handleCreate : undefined}
196
+ onSuccessfullSave={onCreate ? undefined : handleCreate}
197
+ >
198
+ {headerIsSmall ? "" : "Create"}
199
+ </CreateDetailViewButton>
200
+ </CanAccess>
201
+ {/if}
198
202
  </div>
199
203
  </div>
@@ -39,6 +39,11 @@
39
39
  select?: Select;
40
40
  tableWidth?: number;
41
41
 
42
+ // Hide the left sticky tools column entirely when the caller knows
43
+ // nothing inside it would render (e.g. select-mode drawer with no
44
+ // checkboxes, no edit/delete buttons, and no children-network icon).
45
+ showLeftTools?: boolean;
46
+
42
47
  // recording mode row visuals — cellIndex 0 = tools cell, 1+ = data/action cells
43
48
  onCellClass?: (entry: Entry, cellIndex: number) => string;
44
49
  }
@@ -77,13 +82,14 @@
77
82
  rowActions,
78
83
  select,
79
84
  tableWidth = $bindable(),
85
+ showLeftTools = true,
80
86
  onCellClass,
81
87
  }: TableProps = $props();
82
88
 
83
89
  // calculate columns count
84
- const toolsExists = selectedRecords || tools || preTools ? 1 : 0;
90
+ const toolsExists = $derived(showLeftTools && (selectedRecords || tools || preTools) ? 1 : 0);
85
91
  const rowActionsExists = $derived(rowActions ? 1 : 0);
86
- const columnsLength = columns.length + toolsExists;
92
+ const columnsLength = $derived(columns.length + toolsExists);
87
93
 
88
94
  // set table width
89
95
  let columnsWidths: number[] = $state([]);
@@ -166,13 +172,14 @@
166
172
  </script>
167
173
 
168
174
  <div
175
+ class="min-w-max"
169
176
  style="
170
177
  display: grid;
171
- grid-template-columns: minmax(auto, 10rem) repeat({columnsLength - 1}, minmax(auto, 15rem)){rowActionsExists ? ' minmax(auto, 7.5rem)' : ''};
178
+ grid-template-columns: fit-content(10rem) repeat({columnsLength - 1}, fit-content(15rem)){rowActionsExists ? ' fit-content(7.5rem)' : ''};
172
179
  grid-template-rows: 2.5rem;
173
180
  "
174
181
  >
175
- {#if selectedRecords || tools || preTools}
182
+ {#if showLeftTools && (selectedRecords || tools || preTools)}
176
183
  <div
177
184
  bind:clientWidth={columnsWidths[0]}
178
185
  class="
@@ -180,7 +187,7 @@
180
187
  flex items-center p-2.5 text-xs h-10
181
188
  border-r border-b gap-2
182
189
  {headerBorderTop ? 'border-t' : ''}
183
- bg-muted/50
190
+ bg-muted
184
191
  "
185
192
  >
186
193
  {#if selectedRecords && showCheckboxes}
@@ -201,7 +208,7 @@
201
208
  class="
202
209
  sticky top-0 z-10
203
210
  flex items-center p-2.5 text-xs h-10
204
- bg-muted/50
211
+ bg-muted
205
212
  {lastColumn && !showLastColumnBorder ? '' : 'border-r'}
206
213
  border-b gap-2
207
214
  {headerBorderTop ? 'border-t' : ''}
@@ -228,7 +235,7 @@
228
235
  class="
229
236
  sticky top-0 right-0 z-20
230
237
  flex items-center p-2.5 h-10
231
- bg-muted/50
238
+ bg-muted
232
239
  border-l border-b
233
240
  {headerBorderTop ? 'border-t' : ''}
234
241
  "
@@ -238,7 +245,7 @@
238
245
  {#each data as entry, index}
239
246
  {@const isDisabled = Boolean(entry.__disabled)}
240
247
  {@const lastRow = data.length - 1 === index}
241
- {#if selectedRecords || tools || preTools}
248
+ {#if showLeftTools && (selectedRecords || tools || preTools)}
242
249
  <div
243
250
  class="sticky left-0 flex items-center p-2.5 text-xs h-10 bg-card border-r gap-2 {onCellClass?.(entry, 0) ?? ''}"
244
251
  >