@lobb-js/studio 0.43.0 → 0.44.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.
@@ -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,33 @@
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
+ <!-- Field picker — typeahead popover, same widget Sort uses -->
480
+ <Popover.Root
481
+ bind:open={
482
+ () => fieldPickerOpen[rule.id] ?? false,
483
+ (v) => (fieldPickerOpen[rule.id] = v)
484
+ }
479
485
  >
480
- <Select.Trigger class="bg-muted h-7 w-36 text-xs">
481
- <div class="inline-flex items-center gap-1.5">
486
+ <Popover.Trigger
487
+ class="inline-flex h-7 w-36 items-center justify-between gap-1.5 rounded-md border bg-muted px-2 text-xs"
488
+ >
489
+ <div class="inline-flex items-center gap-1.5 truncate">
482
490
  <FieldIcon size="13" />
483
- {rule.field}
491
+ <span class="truncate">{rule.field}</span>
484
492
  </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>
493
+ <ChevronDown size="13" class="text-muted-foreground shrink-0" />
494
+ </Popover.Trigger>
495
+ <Popover.Content class="w-64 p-2">
496
+ <FieldPicker
497
+ {collectionName}
498
+ placeholder="Pick a field…"
499
+ onPick={(fname: string) => {
500
+ patchRuleInPlace(rule.id, { field: fname });
501
+ fieldPickerOpen[rule.id] = false;
502
+ }}
503
+ />
504
+ </Popover.Content>
505
+ </Popover.Root>
498
506
 
499
507
  <!-- Operator picker -->
500
508
  <Select.Root
@@ -64,6 +64,7 @@
64
64
  {/if}
65
65
  </Popover.Trigger>
66
66
  <Popover.Content
67
+ trapFocus={false}
67
68
  class="
68
69
  w-screen p-0
69
70
  transition-[max-width] duration-150
@@ -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>
@@ -10,6 +10,7 @@ interface Props {
10
10
  onCreate?: (changes: Changes) => void;
11
11
  showImport?: boolean;
12
12
  showFilter?: boolean;
13
+ showCreate?: boolean;
13
14
  loading?: boolean;
14
15
  left?: Snippet<[]>;
15
16
  excludeIds?: (string | number)[];
@@ -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,8 @@
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
+ min-w-0
212
+ bg-muted
205
213
  {lastColumn && !showLastColumnBorder ? '' : 'border-r'}
206
214
  border-b gap-2
207
215
  {headerBorderTop ? 'border-t' : ''}
@@ -228,7 +236,7 @@
228
236
  class="
229
237
  sticky top-0 right-0 z-20
230
238
  flex items-center p-2.5 h-10
231
- bg-muted/50
239
+ bg-muted
232
240
  border-l border-b
233
241
  {headerBorderTop ? 'border-t' : ''}
234
242
  "
@@ -238,7 +246,7 @@
238
246
  {#each data as entry, index}
239
247
  {@const isDisabled = Boolean(entry.__disabled)}
240
248
  {@const lastRow = data.length - 1 === index}
241
- {#if selectedRecords || tools || preTools}
249
+ {#if showLeftTools && (selectedRecords || tools || preTools)}
242
250
  <div
243
251
  class="sticky left-0 flex items-center p-2.5 text-xs h-10 bg-card border-r gap-2 {onCellClass?.(entry, 0) ?? ''}"
244
252
  >
@@ -269,7 +277,7 @@
269
277
  onclick={() => {
270
278
  select?.onSelect(entry);
271
279
  }}
272
- 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) ?? ''}"
280
+ class="flex items-center p-2.5 text-xs h-10 text-nowrap overflow-clip min-w-0 bg-card {select ? 'cursor-pointer hover:bg-accent' : ''} {lastColumn && !showLastColumnBorder ? '' : 'border-r'} {onCellClass?.(entry, index + 1) ?? ''}"
273
281
  >
274
282
  {#if overrideCell}
275
283
  {@render overrideCell(fieldValue, column, entry)}
@@ -26,6 +26,7 @@ export interface TableProps {
26
26
  parentWidth?: number;
27
27
  select?: Select;
28
28
  tableWidth?: number;
29
+ showLeftTools?: boolean;
29
30
  onCellClass?: (entry: Entry, cellIndex: number) => string;
30
31
  }
31
32
  import type { Snippet } from "svelte";
@@ -3,14 +3,9 @@
3
3
  import Input from "./ui/input/input.svelte";
4
4
  import SelectRecord from "./selectRecord.svelte";
5
5
  import CreateDetailView from "./detailView/create/createDetailView.svelte";
6
- import UpdateDetailView from "./detailView/update/updateDetailView.svelte";
7
- import { getCollectionPrimaryField } from "./dataTable/utils";
8
- import { getStudioContext } from "../context";
9
- import { Plus, Link, Pencil, Unlink, Trash, RotateCcw, RefreshCw } from "lucide-svelte";
6
+ import { Plus } from "lucide-svelte";
10
7
  import Button from "./ui/button/button.svelte";
11
8
 
12
- const { lobb, ctx } = getStudioContext();
13
-
14
9
  interface LocalProps {
15
10
  parentCollectionName: string;
16
11
  collectionName: string;
@@ -30,45 +25,23 @@
30
25
  }: LocalProps = $props();
31
26
 
32
27
  let createDrawerOpen = $state(false);
33
- let editDrawerOpen = $state(false);
34
- let editValues: Record<string, any> | undefined = $state(undefined);
35
- let originalId = $state<number | null>(null);
36
28
  let initialValue = $state<any>(undefined);
37
- let unlinked = $state(false);
38
29
 
39
30
  onMount(() => { initialValue = value; });
40
31
 
41
- // Derived state from value
42
32
  const isPendingCreate = $derived(value && typeof value === 'object' && value.create);
43
- const isPendingEdit = $derived(value && typeof value === 'object' && value.id && value.update);
44
- const isStagedDelete = $derived(value && typeof value === 'object' && value.delete === true);
45
- const isStagedUnlink = $derived(unlinked);
46
33
  const hasRealId = $derived(value != null && typeof value === 'number');
47
- const isNewLink = $derived(hasRealId && initialValue !== undefined && initialValue == null && value !== initialValue);
48
- const isReplaced = $derived(hasRealId && initialValue !== undefined && initialValue != null && value !== initialValue);
49
- const isEmpty = $derived((value == null || value === 0) && !unlinked);
34
+ const isEmpty = $derived(value == null || value === 0);
50
35
  const isZeroPlaceholder = $derived(value === 0);
36
+ const isChanged = $derived(initialValue !== undefined && value !== initialValue && !isPendingCreate);
51
37
 
52
38
  const bgClass = $derived(
53
- isPendingCreate ? '!bg-green-500/5 border-green-500/40' :
54
- isNewLink ? '!bg-blue-500/5 border-blue-500/40' :
55
- isReplaced ? '!bg-orange-500/5 border-orange-500/40' :
56
- isPendingEdit ? '!bg-orange-500/5 border-orange-500/40' :
57
- isStagedUnlink ? '!bg-slate-500/5 border-slate-500/40' :
58
- isStagedDelete ? '!bg-red-500/5 border-red-500/40' :
39
+ isPendingCreate ? '!bg-green-500/5 border-green-500/40' :
40
+ isChanged ? '!bg-orange-500/5' :
59
41
  ''
60
42
  );
61
43
 
62
- const displayId = $derived(
63
- isPendingEdit ? (value as any).id :
64
- isStagedUnlink ? originalId :
65
- isStagedDelete ? originalId :
66
- hasRealId ? value :
67
- null
68
- );
69
-
70
44
  async function handleCreated(record: any) {
71
- // dry-run result has no id — store as pending create
72
45
  if (!record.id) {
73
46
  value = { create: record };
74
47
  } else {
@@ -76,127 +49,41 @@
76
49
  }
77
50
  }
78
51
 
79
- async function openEdit() {
80
- const res = await lobb.findAll(collectionName, { filter: { id: value }, limit: 1 });
81
- const result = await res.json();
82
- editValues = result.data[0];
83
- editDrawerOpen = true;
84
- }
85
-
86
- function handleEditChanges(changes: import('./detailView/utils').Changes) {
87
- if (Object.keys(changes.data).length === 0) {
88
- value = (editValues as any)?.id ?? value;
89
- } else {
90
- value = { id: (editValues as any)?.id ?? value, update: changes.data };
91
- }
92
- }
93
-
94
52
  function handleSelect(selectedEntry: any) {
95
53
  value = selectedEntry.id;
96
54
  }
97
-
98
- function handleUnlink() {
99
- if (hasRealId) {
100
- originalId = value as number;
101
- unlinked = true;
102
- value = null;
103
- }
104
- }
105
-
106
- async function handleDelete() {
107
- if (hasRealId) {
108
- originalId = value as number;
109
- value = { delete: true };
110
- }
111
- }
112
-
113
- function handleRevert() {
114
- if (originalId != null) {
115
- value = originalId;
116
- originalId = null;
117
- } else {
118
- value = null;
119
- }
120
- unlinked = false;
121
- }
122
55
  </script>
123
56
 
124
57
  {#if !isZeroPlaceholder}
125
58
  <div class="relative">
126
- <!-- Action buttons overlay on the right -->
127
59
  <div class="flex gap-1 absolute right-0 top-0 mr-9 h-full items-center text-xs">
128
- {#if isStagedUnlink || isStagedDelete || isPendingCreate || isPendingEdit}
129
- <Button
130
- class="h-5 w-5 px-0 py-0 hover:bg-transparent text-muted-foreground"
131
- variant="ghost"
132
- Icon={RotateCcw}
133
- onclick={handleRevert}
134
- title="Revert"
135
- ></Button>
136
- {:else if hasRealId}
137
- <Button
138
- class="h-5 w-5 px-0 py-0 hover:bg-transparent text-muted-foreground"
139
- variant="ghost"
140
- Icon={Trash}
141
- onclick={handleDelete}
142
- title="Delete record"
143
- ></Button>
144
- <Button
145
- class="h-5 w-5 px-0 py-0 hover:bg-transparent text-muted-foreground"
146
- variant="ghost"
147
- Icon={Unlink}
148
- onclick={handleUnlink}
149
- title="Unlink"
150
- ></Button>
151
- <Button
152
- class="h-5 w-5 px-0 py-0 hover:bg-transparent text-muted-foreground"
153
- variant="ghost"
154
- Icon={Pencil}
155
- onclick={openEdit}
156
- title="Edit record"
157
- ></Button>
158
- <SelectRecord
159
- class="h-5 w-5 px-0 py-0 hover:bg-transparent text-muted-foreground"
160
- variant="ghost"
161
- {collectionName}
162
- onSelect={handleSelect}
163
- additionalFilter={{ id: { $ne: value } }}
164
- title="Replace"
165
- {entry}
166
- >
167
- {#snippet children()}<RefreshCw size="13" />{/snippet}
168
- </SelectRecord>
169
- {:else if isEmpty}
170
- <Button
171
- class="h-6 px-2 font-normal text-xs"
172
- variant="outline"
173
- Icon={Plus}
174
- onclick={() => (createDrawerOpen = true)}
175
- >
176
- Create
177
- </Button>
178
- <SelectRecord
179
- class="h-6 px-2 font-normal text-xs"
180
- variant="outline"
181
- {parentCollectionName}
182
- {collectionName}
183
- {fieldName}
184
- onSelect={handleSelect}
185
- text="Link"
186
- {entry}
187
- />
188
- {/if}
60
+ <Button
61
+ class="h-6 px-2 font-normal text-xs"
62
+ variant="outline"
63
+ Icon={Plus}
64
+ onclick={() => (createDrawerOpen = true)}
65
+ >
66
+ Create
67
+ </Button>
68
+ <SelectRecord
69
+ class="h-6 px-2 font-normal text-xs"
70
+ variant="outline"
71
+ {parentCollectionName}
72
+ {collectionName}
73
+ {fieldName}
74
+ onSelect={handleSelect}
75
+ text="Link"
76
+ {entry}
77
+ />
189
78
  </div>
190
79
 
191
- <!-- Input field colored by state -->
192
80
  <Input
193
- placeholder={isEmpty ? "NULL" : ""}
81
+ placeholder={isPendingCreate ? "AUTO GENERATED" : isEmpty ? "NULL" : ""}
194
82
  type="number"
195
83
  class="bg-muted text-xs {bgClass} {destructive ? '!bg-destructive/10 border-destructive' : ''}"
196
- disabled={isPendingCreate || isPendingEdit}
197
84
  bind:value={
198
- () => displayId ?? "",
199
- (v) => { if (hasRealId || isEmpty) value = (v === "" || v == null) ? null : Number(v); }
85
+ () => hasRealId ? value : "",
86
+ (v) => { value = (v === "" || v == null) ? null : Number(v); }
200
87
  }
201
88
  />
202
89
  </div>
@@ -214,13 +101,3 @@
214
101
  onCancel={async () => { createDrawerOpen = false; }}
215
102
  />
216
103
  {/if}
217
-
218
- {#if editDrawerOpen && editValues}
219
- <UpdateDetailView
220
- collectionName={collectionName}
221
- recordId={String(editValues.id)}
222
- values={editValues}
223
- onChanges={handleEditChanges}
224
- onCancel={async () => { editDrawerOpen = false; editValues = undefined; }}
225
- />
226
- {/if}