@lobb-js/studio 0.41.0 → 0.43.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.
- package/dist/components/confirmationDialog/confirmationDialog.svelte +1 -1
- package/dist/components/dataTable/dataTable.svelte +215 -57
- package/dist/components/dataTable/header.svelte +5 -2
- package/dist/components/dataTable/header.svelte.d.ts +1 -0
- package/dist/components/dataTable/listViewChildren.svelte +60 -77
- package/dist/components/dataTable/listViewChildren.svelte.d.ts +1 -1
- package/dist/components/dataTable/table.svelte +18 -77
- package/dist/components/dataTable/table.svelte.d.ts +2 -2
- package/dist/components/detailView/changeTreeUtils.d.ts +7 -0
- package/dist/components/detailView/changeTreeUtils.js +47 -0
- package/dist/components/detailView/create/createDetailView.svelte +49 -3
- package/dist/components/detailView/create/createDetailView.svelte.d.ts +1 -0
- package/dist/components/detailView/detailView.svelte +7 -2
- package/dist/components/detailView/detailView.svelte.d.ts +1 -0
- package/dist/components/detailView/fieldInput.svelte +10 -9
- package/dist/components/detailView/fieldInput.svelte.d.ts +1 -0
- package/dist/components/detailView/update/updateDetailView.svelte +46 -15
- package/dist/components/detailView/update/updateDetailViewButton.svelte +7 -0
- package/dist/components/detailView/update/updateDetailViewButton.svelte.d.ts +1 -0
- package/dist/components/drawer.svelte +16 -2
- package/dist/components/foreingKeyInput.svelte +177 -56
- package/dist/components/foreingKeyInput.svelte.d.ts +1 -1
- package/dist/components/polymorphicInput.svelte +128 -55
- package/dist/components/polymorphicInput.svelte.d.ts +1 -0
- package/package.json +2 -2
- package/src/lib/components/confirmationDialog/confirmationDialog.svelte +1 -1
- package/src/lib/components/dataTable/dataTable.svelte +215 -57
- package/src/lib/components/dataTable/header.svelte +5 -2
- package/src/lib/components/dataTable/listViewChildren.svelte +60 -77
- package/src/lib/components/dataTable/table.svelte +18 -77
- package/src/lib/components/detailView/changeTreeUtils.ts +39 -0
- package/src/lib/components/detailView/create/createDetailView.svelte +49 -3
- package/src/lib/components/detailView/detailView.svelte +7 -2
- package/src/lib/components/detailView/fieldInput.svelte +10 -9
- package/src/lib/components/detailView/update/updateDetailView.svelte +46 -15
- package/src/lib/components/detailView/update/updateDetailViewButton.svelte +7 -0
- package/src/lib/components/drawer.svelte +16 -2
- package/src/lib/components/foreingKeyInput.svelte +177 -56
- package/src/lib/components/polymorphicInput.svelte +128 -55
|
@@ -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}
|
|
@@ -6,6 +6,7 @@ interface LocalProp extends UpdateDetailViewProp {
|
|
|
6
6
|
class?: ButtonProps["class"];
|
|
7
7
|
Icon?: ButtonProps["Icon"];
|
|
8
8
|
children?: ButtonProps["children"];
|
|
9
|
+
"aria-label"?: string;
|
|
9
10
|
}
|
|
10
11
|
declare const UpdateDetailViewButton: import("svelte").Component<LocalProp, {}, "">;
|
|
11
12
|
type UpdateDetailViewButton = ReturnType<typeof UpdateDetailViewButton>;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { fade } from "svelte/transition";
|
|
5
5
|
import { cubicOut } from "svelte/easing";
|
|
6
6
|
import Portal from "svelte-portal";
|
|
7
|
+
import { getContext, setContext, onDestroy } from "svelte";
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
9
10
|
children?: Snippet<[]>;
|
|
@@ -13,6 +14,16 @@
|
|
|
13
14
|
|
|
14
15
|
let { onHide, children, position = "side" }: Props = $props();
|
|
15
16
|
|
|
17
|
+
// Track nesting depth for stacking effect
|
|
18
|
+
const DEPTH_KEY = 'drawer-depth';
|
|
19
|
+
const parentDepth: number = getContext(DEPTH_KEY) ?? 0;
|
|
20
|
+
const depth = parentDepth + 1;
|
|
21
|
+
setContext(DEPTH_KEY, depth);
|
|
22
|
+
|
|
23
|
+
// Side drawers get narrower, bottom drawers get shorter — both offset by 48px per level
|
|
24
|
+
const sideWidth = $derived(calculateDrawerWidth() - (depth - 1) * 48);
|
|
25
|
+
const bottomOffset = $derived((depth - 1) * 48);
|
|
26
|
+
|
|
16
27
|
function slide(_node: Element, { duration = 250, axis }: { duration?: number; axis: "x" | "y" }) {
|
|
17
28
|
return {
|
|
18
29
|
duration,
|
|
@@ -36,11 +47,14 @@
|
|
|
36
47
|
></button>
|
|
37
48
|
|
|
38
49
|
<div
|
|
50
|
+
role="dialog"
|
|
39
51
|
transition:slide={{ axis: position === "bottom" ? "y" : "x" }}
|
|
40
52
|
class={position === "bottom"
|
|
41
|
-
? "fixed bottom-0 left-0 z-40 flex
|
|
53
|
+
? "fixed bottom-0 left-0 z-40 flex w-full flex-col border-t bg-card"
|
|
42
54
|
: "fixed right-0 top-0 z-40 flex h-full w-full flex-col border-l bg-card"}
|
|
43
|
-
style={position === "side"
|
|
55
|
+
style={position === "side"
|
|
56
|
+
? `max-width: ${sideWidth}px;`
|
|
57
|
+
: `height: calc(60vh - ${bottomOffset}px);`}
|
|
44
58
|
>
|
|
45
59
|
{@render children?.()}
|
|
46
60
|
</div>
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
import { onMount } from "svelte";
|
|
3
3
|
import Input from "./ui/input/input.svelte";
|
|
4
4
|
import SelectRecord from "./selectRecord.svelte";
|
|
5
|
-
import
|
|
5
|
+
import CreateDetailView from "./detailView/create/createDetailView.svelte";
|
|
6
|
+
import UpdateDetailView from "./detailView/update/updateDetailView.svelte";
|
|
6
7
|
import { getCollectionPrimaryField } from "./dataTable/utils";
|
|
7
8
|
import { getStudioContext } from "../context";
|
|
8
|
-
import {
|
|
9
|
+
import { Plus, Link, Pencil, Unlink, Trash, RotateCcw, RefreshCw } from "lucide-svelte";
|
|
10
|
+
import Button from "./ui/button/button.svelte";
|
|
9
11
|
|
|
10
12
|
const { lobb, ctx } = getStudioContext();
|
|
11
13
|
|
|
@@ -13,7 +15,7 @@
|
|
|
13
15
|
parentCollectionName: string;
|
|
14
16
|
collectionName: string;
|
|
15
17
|
fieldName: string;
|
|
16
|
-
value?:
|
|
18
|
+
value?: any;
|
|
17
19
|
destructive?: boolean;
|
|
18
20
|
entry: Record<string, any>;
|
|
19
21
|
}
|
|
@@ -27,79 +29,198 @@
|
|
|
27
29
|
entry,
|
|
28
30
|
}: LocalProps = $props();
|
|
29
31
|
|
|
30
|
-
let
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
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
|
+
let initialValue = $state<any>(undefined);
|
|
37
|
+
let unlinked = $state(false);
|
|
38
|
+
|
|
39
|
+
onMount(() => { initialValue = value; });
|
|
40
|
+
|
|
41
|
+
// Derived state from value
|
|
42
|
+
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
|
+
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);
|
|
50
|
+
const isZeroPlaceholder = $derived(value === 0);
|
|
51
|
+
|
|
52
|
+
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' :
|
|
59
|
+
''
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const displayId = $derived(
|
|
63
|
+
isPendingEdit ? (value as any).id :
|
|
64
|
+
isStagedUnlink ? originalId :
|
|
65
|
+
isStagedDelete ? originalId :
|
|
66
|
+
hasRealId ? value :
|
|
67
|
+
null
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
async function handleCreated(record: any) {
|
|
71
|
+
// dry-run result has no id — store as pending create
|
|
72
|
+
if (!record.id) {
|
|
73
|
+
value = { create: record };
|
|
74
|
+
} else {
|
|
75
|
+
value = record.id;
|
|
41
76
|
}
|
|
42
|
-
}
|
|
77
|
+
}
|
|
43
78
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
}
|
|
47
93
|
|
|
48
94
|
function handleSelect(selectedEntry: any) {
|
|
49
|
-
const primaryFieldName = getCollectionPrimaryField(ctx, collectionName);
|
|
50
95
|
value = selectedEntry.id;
|
|
51
|
-
displayName = primaryFieldName ? String(selectedEntry[primaryFieldName]) : null;
|
|
52
96
|
}
|
|
53
97
|
|
|
54
|
-
|
|
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
|
+
}
|
|
55
122
|
</script>
|
|
56
123
|
|
|
57
|
-
{#if !
|
|
124
|
+
{#if !isZeroPlaceholder}
|
|
58
125
|
<div class="relative">
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
126
|
+
<!-- Action buttons overlay on the right -->
|
|
127
|
+
<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"
|
|
64
131
|
variant="ghost"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
{
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
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
|
+
/>
|
|
73
188
|
{/if}
|
|
74
|
-
<SelectRecord
|
|
75
|
-
class="h-6 px-2 font-normal text-xs"
|
|
76
|
-
variant="outline"
|
|
77
|
-
{parentCollectionName}
|
|
78
|
-
{collectionName}
|
|
79
|
-
{fieldName}
|
|
80
|
-
onSelect={handleSelect}
|
|
81
|
-
{entry}
|
|
82
|
-
/>
|
|
83
189
|
</div>
|
|
190
|
+
|
|
191
|
+
<!-- Input field colored by state -->
|
|
84
192
|
<Input
|
|
85
|
-
placeholder={"NULL"}
|
|
193
|
+
placeholder={isEmpty ? "NULL" : ""}
|
|
86
194
|
type="number"
|
|
87
|
-
class="
|
|
88
|
-
|
|
89
|
-
{destructive ? 'border-destructive bg-destructive/10' : ''}
|
|
90
|
-
"
|
|
195
|
+
class="bg-muted text-xs {bgClass} {destructive ? '!bg-destructive/10 border-destructive' : ''}"
|
|
196
|
+
disabled={isPendingCreate || isPendingEdit}
|
|
91
197
|
bind:value={
|
|
92
|
-
() =>
|
|
93
|
-
(v) => (value = (v === "" || v == null) ? null : Number(v)
|
|
198
|
+
() => displayId ?? "",
|
|
199
|
+
(v) => { if (hasRealId || isEmpty) value = (v === "" || v == null) ? null : Number(v); }
|
|
94
200
|
}
|
|
95
201
|
/>
|
|
96
202
|
</div>
|
|
97
203
|
{:else}
|
|
98
204
|
<div class="relative z-10">
|
|
99
|
-
<Input
|
|
100
|
-
placeholder={"PARENT ID"}
|
|
101
|
-
class="bg-muted text-xs"
|
|
102
|
-
disabled={true}
|
|
103
|
-
/>
|
|
205
|
+
<Input placeholder="PARENT ID" class="bg-muted text-xs" disabled={true} />
|
|
104
206
|
</div>
|
|
105
207
|
{/if}
|
|
208
|
+
|
|
209
|
+
{#if createDrawerOpen}
|
|
210
|
+
<CreateDetailView
|
|
211
|
+
collectionName={collectionName}
|
|
212
|
+
onCreated={handleCreated}
|
|
213
|
+
onChanges={() => {}}
|
|
214
|
+
onCancel={async () => { createDrawerOpen = false; }}
|
|
215
|
+
/>
|
|
216
|
+
{/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,15 +4,17 @@
|
|
|
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 { getCollectionPrimaryField } from "./dataTable/utils";
|
|
8
7
|
import { getStudioContext } from "../context";
|
|
9
|
-
import { ArrowLeft, Link, ChevronDown } from "lucide-svelte";
|
|
8
|
+
import { ArrowLeft, Link, ChevronDown, Plus, Pencil, Unlink, Trash, RotateCcw, RefreshCw } from "lucide-svelte";
|
|
9
|
+
import CreateDetailView from "./detailView/create/createDetailView.svelte";
|
|
10
|
+
import UpdateDetailView from "./detailView/update/updateDetailView.svelte";
|
|
10
11
|
|
|
11
12
|
const { ctx, lobb } = getStudioContext();
|
|
12
13
|
|
|
13
14
|
interface Props {
|
|
14
15
|
collectionField: string;
|
|
15
16
|
idField: string;
|
|
17
|
+
virtualField: string;
|
|
16
18
|
targetCollections: string[];
|
|
17
19
|
entry: Record<string, any>;
|
|
18
20
|
destructive?: boolean;
|
|
@@ -21,38 +23,52 @@
|
|
|
21
23
|
let {
|
|
22
24
|
collectionField,
|
|
23
25
|
idField,
|
|
26
|
+
virtualField,
|
|
24
27
|
targetCollections,
|
|
25
28
|
entry = $bindable(),
|
|
26
29
|
destructive,
|
|
27
30
|
}: Props = $props();
|
|
28
31
|
|
|
32
|
+
|
|
29
33
|
const selectedCollection = $derived(entry[collectionField] ?? null);
|
|
30
|
-
const selectedId
|
|
34
|
+
const selectedId = $derived(entry[idField] ?? null);
|
|
35
|
+
const virtualVal = $derived(entry[virtualField] ?? null);
|
|
31
36
|
|
|
32
|
-
let
|
|
33
|
-
let
|
|
34
|
-
|
|
37
|
+
let initialId = $state<number | null | undefined>(undefined);
|
|
38
|
+
let unlinked = $state(false);
|
|
39
|
+
onMount(() => { initialId = entry[idField] ?? null; });
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
});
|
|
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)));
|
|
47
50
|
|
|
48
|
-
$
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
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' :
|
|
58
|
+
''
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
let collectionPopoverOpen = $state(false);
|
|
62
|
+
let recordDrawerOpen = $state(false);
|
|
63
|
+
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);
|
|
51
67
|
|
|
52
68
|
function onCollectionChange(col: string) {
|
|
53
69
|
collectionPopoverOpen = false;
|
|
54
70
|
if (entry[collectionField] !== col) {
|
|
55
|
-
entry = { ...entry, [collectionField]: col, [idField]: null };
|
|
71
|
+
entry = { ...entry, [collectionField]: col, [idField]: null, [virtualField]: undefined };
|
|
56
72
|
}
|
|
57
73
|
}
|
|
58
74
|
|
|
@@ -63,14 +79,60 @@
|
|
|
63
79
|
}
|
|
64
80
|
|
|
65
81
|
function onRecordSelect(record: any) {
|
|
66
|
-
|
|
67
|
-
entry = { ...entry, [idField]: record.id };
|
|
68
|
-
displayName = primaryFieldName ? String(record[primaryFieldName]) : null;
|
|
82
|
+
entry = { ...entry, [idField]: record.id, [virtualField]: undefined };
|
|
69
83
|
recordDrawerOpen = false;
|
|
70
84
|
}
|
|
85
|
+
|
|
86
|
+
async function onPolyCreated(record: any) {
|
|
87
|
+
if (!record.id) {
|
|
88
|
+
entry = { ...entry, [virtualField]: { collection: selectedCollection, create: record }, [collectionField]: selectedCollection, [idField]: null };
|
|
89
|
+
} else {
|
|
90
|
+
entry = { ...entry, [idField]: record.id, [virtualField]: undefined };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
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
|
+
}
|
|
71
133
|
</script>
|
|
72
134
|
|
|
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 {destructive ? '
|
|
135
|
+
<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' : ''}">
|
|
74
136
|
<!-- Collection picker -->
|
|
75
137
|
<Popover.Root bind:open={collectionPopoverOpen}>
|
|
76
138
|
<Popover.Trigger>
|
|
@@ -99,31 +161,32 @@
|
|
|
99
161
|
</Popover.Content>
|
|
100
162
|
</Popover.Root>
|
|
101
163
|
|
|
102
|
-
<!--
|
|
164
|
+
<!-- ID input (only editable when real value or empty) -->
|
|
103
165
|
<input
|
|
104
166
|
placeholder="NULL"
|
|
105
167
|
type="number"
|
|
106
168
|
class="min-w-0 flex-1 bg-transparent outline-none text-xs placeholder:text-muted-foreground"
|
|
107
|
-
value={selectedId ?? ""}
|
|
169
|
+
value={isStagedUnlink || isStagedDelete ? (virtualVal?.id ?? '') : (selectedId ?? "")}
|
|
170
|
+
disabled={isPendingCreate || isPendingEdit}
|
|
108
171
|
oninput={onIdChange}
|
|
109
172
|
/>
|
|
110
173
|
|
|
111
|
-
<!--
|
|
112
|
-
{#if
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
{
|
|
120
|
-
<Button
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
>
|
|
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}
|
|
183
|
+
<Button class="h-6 shrink-0 px-2 font-normal text-xs" variant="outline" onclick={() => (createDrawerOpen = true)}>
|
|
184
|
+
<Plus size="13" />
|
|
185
|
+
Create
|
|
186
|
+
</Button>
|
|
187
|
+
<Button class="h-6 shrink-0 px-2 font-normal text-xs" variant="outline" onclick={() => (recordDrawerOpen = true)}>
|
|
125
188
|
<Link size="13" />
|
|
126
|
-
|
|
189
|
+
Link
|
|
127
190
|
</Button>
|
|
128
191
|
{/if}
|
|
129
192
|
</div>
|
|
@@ -131,27 +194,37 @@
|
|
|
131
194
|
{#if recordDrawerOpen}
|
|
132
195
|
<Drawer onHide={async () => { recordDrawerOpen = false }}>
|
|
133
196
|
<div class="flex h-12 items-center gap-4 border-b px-4">
|
|
134
|
-
<Button
|
|
135
|
-
variant="outline"
|
|
136
|
-
onclick={() => (recordDrawerOpen = false)}
|
|
137
|
-
class="h-8 w-8 rounded-full text-xs font-normal"
|
|
138
|
-
Icon={ArrowLeft}
|
|
139
|
-
/>
|
|
197
|
+
<Button variant="outline" onclick={() => (recordDrawerOpen = false)} class="h-8 w-8 rounded-full text-xs font-normal" Icon={ArrowLeft} />
|
|
140
198
|
<div class="flex items-center gap-2">
|
|
141
199
|
<div class="text-sm">Select record from</div>
|
|
142
|
-
<span class="rounded-md border bg-muted px-2 py-0.5 text-sm">
|
|
143
|
-
{selectedCollection}
|
|
144
|
-
</span>
|
|
200
|
+
<span class="rounded-md border bg-muted px-2 py-0.5 text-sm">{selectedCollection}</span>
|
|
145
201
|
</div>
|
|
146
202
|
</div>
|
|
147
203
|
<div class="flex-1 overflow-y-auto bg-muted">
|
|
148
204
|
<DataTable
|
|
149
205
|
collectionName={selectedCollection!}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
select: { onSelect: onRecordSelect },
|
|
153
|
-
}}
|
|
206
|
+
filter={selectedId != null ? { id: { $ne: selectedId } } : undefined}
|
|
207
|
+
tableProps={{ showCheckboxes: false, select: { onSelect: onRecordSelect } }}
|
|
154
208
|
/>
|
|
155
209
|
</div>
|
|
156
210
|
</Drawer>
|
|
157
211
|
{/if}
|
|
212
|
+
|
|
213
|
+
{#if createDrawerOpen && selectedCollection}
|
|
214
|
+
<CreateDetailView
|
|
215
|
+
collectionName={selectedCollection}
|
|
216
|
+
onCreated={onPolyCreated}
|
|
217
|
+
onChanges={() => {}}
|
|
218
|
+
onCancel={async () => { createDrawerOpen = false; }}
|
|
219
|
+
/>
|
|
220
|
+
{/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.
|
|
4
|
+
"version": "0.43.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"postpublish": "./scripts/postpublish.sh"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@lobb-js/core": "^0.
|
|
48
|
+
"@lobb-js/core": "^0.38.0",
|
|
49
49
|
"@chromatic-com/storybook": "^4.1.2",
|
|
50
50
|
"@playwright/test": "^1.60.0",
|
|
51
51
|
"@storybook/addon-a11y": "^10.0.1",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
<AlertDialog.Content>
|
|
18
18
|
<AlertDialog.Header>
|
|
19
19
|
<AlertDialog.Title>{title}</AlertDialog.Title>
|
|
20
|
-
<AlertDialog.Description>
|
|
20
|
+
<AlertDialog.Description class="whitespace-pre-wrap font-mono text-xs">
|
|
21
21
|
{description}
|
|
22
22
|
</AlertDialog.Description>
|
|
23
23
|
</AlertDialog.Header>
|