@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.
@@ -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}
@@ -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}