@lobb-js/studio 0.42.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.
- package/dist/components/confirmationDialog/confirmationDialog.svelte +1 -1
- package/dist/components/dataTable/dataTable.svelte +49 -17
- package/dist/components/dataTable/filter.svelte +26 -23
- package/dist/components/dataTable/header.svelte +17 -13
- 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 +20 -61
- 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 +17 -13
- 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 +22 -18
- package/dist/components/drawer.svelte +15 -2
- package/dist/components/foreingKeyInput.svelte +32 -60
- package/dist/components/foreingKeyInput.svelte.d.ts +1 -1
- package/dist/components/polymorphicInput.svelte +42 -66
- package/dist/components/polymorphicInput.svelte.d.ts +1 -0
- package/package.json +2 -2
- package/src/app.css +2 -2
- package/src/lib/components/confirmationDialog/confirmationDialog.svelte +1 -1
- package/src/lib/components/dataTable/dataTable.svelte +49 -17
- package/src/lib/components/dataTable/filter.svelte +26 -23
- package/src/lib/components/dataTable/header.svelte +17 -13
- package/src/lib/components/dataTable/listViewChildren.svelte +60 -77
- package/src/lib/components/dataTable/table.svelte +20 -61
- package/src/lib/components/detailView/changeTreeUtils.ts +39 -0
- package/src/lib/components/detailView/create/createDetailView.svelte +17 -13
- 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 +22 -18
- package/src/lib/components/drawer.svelte +15 -2
- package/src/lib/components/foreingKeyInput.svelte +32 -60
- package/src/lib/components/polymorphicInput.svelte +42 -66
|
@@ -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,
|
|
@@ -39,9 +50,11 @@
|
|
|
39
50
|
role="dialog"
|
|
40
51
|
transition:slide={{ axis: position === "bottom" ? "y" : "x" }}
|
|
41
52
|
class={position === "bottom"
|
|
42
|
-
? "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"
|
|
43
54
|
: "fixed right-0 top-0 z-40 flex h-full w-full flex-col border-l bg-card"}
|
|
44
|
-
style={position === "side"
|
|
55
|
+
style={position === "side"
|
|
56
|
+
? `max-width: ${sideWidth}px;`
|
|
57
|
+
: `height: calc(60vh - ${bottomOffset}px);`}
|
|
45
58
|
>
|
|
46
59
|
{@render children?.()}
|
|
47
60
|
</div>
|
|
@@ -2,20 +2,15 @@
|
|
|
2
2
|
import { onMount } from "svelte";
|
|
3
3
|
import Input from "./ui/input/input.svelte";
|
|
4
4
|
import SelectRecord from "./selectRecord.svelte";
|
|
5
|
-
import UpdateDetailViewButton from "./detailView/update/updateDetailViewButton.svelte";
|
|
6
5
|
import CreateDetailView from "./detailView/create/createDetailView.svelte";
|
|
7
|
-
import {
|
|
8
|
-
import { getStudioContext } from "../context";
|
|
9
|
-
import { ExternalLink, Plus } 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;
|
|
17
12
|
fieldName: string;
|
|
18
|
-
value?:
|
|
13
|
+
value?: any;
|
|
19
14
|
destructive?: boolean;
|
|
20
15
|
entry: Record<string, any>;
|
|
21
16
|
}
|
|
@@ -29,57 +24,39 @@
|
|
|
29
24
|
entry,
|
|
30
25
|
}: LocalProps = $props();
|
|
31
26
|
|
|
32
|
-
let displayName = $state<string | null>(null);
|
|
33
27
|
let createDrawerOpen = $state(false);
|
|
28
|
+
let initialValue = $state<any>(undefined);
|
|
34
29
|
|
|
35
|
-
onMount(
|
|
36
|
-
if (value == null) return;
|
|
37
|
-
try {
|
|
38
|
-
const res = await lobb.findOne(collectionName, value);
|
|
39
|
-
const record = (await res.json()).data;
|
|
40
|
-
const primaryFieldName = getCollectionPrimaryField(ctx, collectionName);
|
|
41
|
-
displayName = primaryFieldName ? String(record[primaryFieldName]) : null;
|
|
42
|
-
} catch {
|
|
43
|
-
displayName = null;
|
|
44
|
-
}
|
|
45
|
-
});
|
|
30
|
+
onMount(() => { initialValue = value; });
|
|
46
31
|
|
|
47
|
-
$
|
|
48
|
-
|
|
49
|
-
|
|
32
|
+
const isPendingCreate = $derived(value && typeof value === 'object' && value.create);
|
|
33
|
+
const hasRealId = $derived(value != null && typeof value === 'number');
|
|
34
|
+
const isEmpty = $derived(value == null || value === 0);
|
|
35
|
+
const isZeroPlaceholder = $derived(value === 0);
|
|
36
|
+
const isChanged = $derived(initialValue !== undefined && value !== initialValue && !isPendingCreate);
|
|
50
37
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
38
|
+
const bgClass = $derived(
|
|
39
|
+
isPendingCreate ? '!bg-green-500/5 border-green-500/40' :
|
|
40
|
+
isChanged ? '!bg-orange-500/5' :
|
|
41
|
+
''
|
|
42
|
+
);
|
|
56
43
|
|
|
57
44
|
async function handleCreated(record: any) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
45
|
+
if (!record.id) {
|
|
46
|
+
value = { create: record };
|
|
47
|
+
} else {
|
|
48
|
+
value = record.id;
|
|
49
|
+
}
|
|
61
50
|
}
|
|
62
51
|
|
|
63
|
-
|
|
52
|
+
function handleSelect(selectedEntry: any) {
|
|
53
|
+
value = selectedEntry.id;
|
|
54
|
+
}
|
|
64
55
|
</script>
|
|
65
56
|
|
|
66
|
-
{#if !
|
|
57
|
+
{#if !isZeroPlaceholder}
|
|
67
58
|
<div class="relative">
|
|
68
|
-
<div class="flex gap-
|
|
69
|
-
{#if value != null}
|
|
70
|
-
<UpdateDetailViewButton
|
|
71
|
-
collectionName={collectionName}
|
|
72
|
-
recordId={value}
|
|
73
|
-
variant="ghost"
|
|
74
|
-
class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
|
|
75
|
-
Icon={ExternalLink}
|
|
76
|
-
></UpdateDetailViewButton>
|
|
77
|
-
{/if}
|
|
78
|
-
{#if displayName}
|
|
79
|
-
<div class="flex items-center bg-background rounded-full border h-6 px-3 shadow-sm">
|
|
80
|
-
{displayName}
|
|
81
|
-
</div>
|
|
82
|
-
{/if}
|
|
59
|
+
<div class="flex gap-1 absolute right-0 top-0 mr-9 h-full items-center text-xs">
|
|
83
60
|
<Button
|
|
84
61
|
class="h-6 px-2 font-normal text-xs"
|
|
85
62
|
variant="outline"
|
|
@@ -95,30 +72,24 @@
|
|
|
95
72
|
{collectionName}
|
|
96
73
|
{fieldName}
|
|
97
74
|
onSelect={handleSelect}
|
|
98
|
-
text="
|
|
75
|
+
text="Link"
|
|
99
76
|
{entry}
|
|
100
77
|
/>
|
|
101
78
|
</div>
|
|
79
|
+
|
|
102
80
|
<Input
|
|
103
|
-
placeholder={"NULL"}
|
|
81
|
+
placeholder={isPendingCreate ? "AUTO GENERATED" : isEmpty ? "NULL" : ""}
|
|
104
82
|
type="number"
|
|
105
|
-
class="
|
|
106
|
-
bg-muted text-xs
|
|
107
|
-
{destructive ? 'border-destructive bg-destructive/10' : ''}
|
|
108
|
-
"
|
|
83
|
+
class="bg-muted text-xs {bgClass} {destructive ? '!bg-destructive/10 border-destructive' : ''}"
|
|
109
84
|
bind:value={
|
|
110
|
-
() => value
|
|
111
|
-
(v) =>
|
|
85
|
+
() => hasRealId ? value : "",
|
|
86
|
+
(v) => { value = (v === "" || v == null) ? null : Number(v); }
|
|
112
87
|
}
|
|
113
88
|
/>
|
|
114
89
|
</div>
|
|
115
90
|
{:else}
|
|
116
91
|
<div class="relative z-10">
|
|
117
|
-
<Input
|
|
118
|
-
placeholder={"PARENT ID"}
|
|
119
|
-
class="bg-muted text-xs"
|
|
120
|
-
disabled={true}
|
|
121
|
-
/>
|
|
92
|
+
<Input placeholder="PARENT ID" class="bg-muted text-xs" disabled={true} />
|
|
122
93
|
</div>
|
|
123
94
|
{/if}
|
|
124
95
|
|
|
@@ -126,6 +97,7 @@
|
|
|
126
97
|
<CreateDetailView
|
|
127
98
|
collectionName={collectionName}
|
|
128
99
|
onCreated={handleCreated}
|
|
100
|
+
onChanges={() => {}}
|
|
129
101
|
onCancel={async () => { createDrawerOpen = false; }}
|
|
130
102
|
/>
|
|
131
103
|
{/if}
|
|
@@ -4,16 +4,13 @@
|
|
|
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
|
-
import { getStudioContext } from "../context";
|
|
9
7
|
import { ArrowLeft, Link, ChevronDown, Plus } from "lucide-svelte";
|
|
10
8
|
import CreateDetailView from "./detailView/create/createDetailView.svelte";
|
|
11
9
|
|
|
12
|
-
const { ctx, lobb } = getStudioContext();
|
|
13
|
-
|
|
14
10
|
interface Props {
|
|
15
11
|
collectionField: string;
|
|
16
12
|
idField: string;
|
|
13
|
+
virtualField: string;
|
|
17
14
|
targetCollections: string[];
|
|
18
15
|
entry: Record<string, any>;
|
|
19
16
|
destructive?: boolean;
|
|
@@ -22,64 +19,68 @@
|
|
|
22
19
|
let {
|
|
23
20
|
collectionField,
|
|
24
21
|
idField,
|
|
22
|
+
virtualField,
|
|
25
23
|
targetCollections,
|
|
26
24
|
entry = $bindable(),
|
|
27
25
|
destructive,
|
|
28
26
|
}: Props = $props();
|
|
29
27
|
|
|
30
28
|
const selectedCollection = $derived(entry[collectionField] ?? null);
|
|
31
|
-
const selectedId
|
|
29
|
+
const selectedId = $derived(entry[idField] ?? null);
|
|
30
|
+
const virtualVal = $derived(entry[virtualField] ?? null);
|
|
31
|
+
|
|
32
|
+
let initialId = $state<number | null | undefined>(undefined);
|
|
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
|
+
);
|
|
45
|
+
|
|
46
|
+
const bgClass = $derived(
|
|
47
|
+
isPendingCreate ? '!bg-green-500/5 border-green-500/40' :
|
|
48
|
+
isChanged ? '!bg-orange-500/5' :
|
|
49
|
+
''
|
|
50
|
+
);
|
|
32
51
|
|
|
33
|
-
let displayName = $state<string | null>(null);
|
|
34
52
|
let collectionPopoverOpen = $state(false);
|
|
35
53
|
let recordDrawerOpen = $state(false);
|
|
36
54
|
let createDrawerOpen = $state(false);
|
|
37
55
|
|
|
38
|
-
onMount(async () => {
|
|
39
|
-
if (selectedCollection == null || selectedId == null) return;
|
|
40
|
-
try {
|
|
41
|
-
const res = await lobb.findOne(selectedCollection, selectedId);
|
|
42
|
-
const record = await res.json();
|
|
43
|
-
const primaryFieldName = getCollectionPrimaryField(ctx, selectedCollection);
|
|
44
|
-
displayName = primaryFieldName ? String(record[primaryFieldName]) : null;
|
|
45
|
-
} catch {
|
|
46
|
-
displayName = null;
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
$effect(() => {
|
|
51
|
-
if (entry[idField] == null) displayName = null;
|
|
52
|
-
});
|
|
53
|
-
|
|
54
56
|
function onCollectionChange(col: string) {
|
|
55
57
|
collectionPopoverOpen = false;
|
|
56
58
|
if (entry[collectionField] !== col) {
|
|
57
|
-
entry = { ...entry, [collectionField]: col, [idField]: null };
|
|
59
|
+
entry = { ...entry, [collectionField]: col, [idField]: null, [virtualField]: undefined };
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
function onIdChange(e: Event) {
|
|
62
64
|
const raw = (e.target as HTMLInputElement).value;
|
|
63
65
|
const id = raw === "" ? null : Number(raw);
|
|
64
|
-
entry = { ...entry, [idField]: id };
|
|
66
|
+
entry = { ...entry, [idField]: id, [virtualField]: undefined };
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
function onRecordSelect(record: any) {
|
|
68
|
-
|
|
69
|
-
entry = { ...entry, [idField]: record.id };
|
|
70
|
-
displayName = primaryFieldName ? String(record[primaryFieldName]) : null;
|
|
70
|
+
entry = { ...entry, [idField]: record.id, [virtualField]: undefined };
|
|
71
71
|
recordDrawerOpen = false;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
async function onPolyCreated(record: any) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
if (!record.id) {
|
|
76
|
+
entry = { ...entry, [virtualField]: { collection: selectedCollection, create: record }, [collectionField]: selectedCollection, [idField]: null };
|
|
77
|
+
} else {
|
|
78
|
+
entry = { ...entry, [idField]: record.id, [virtualField]: undefined };
|
|
79
|
+
}
|
|
78
80
|
}
|
|
79
81
|
</script>
|
|
80
82
|
|
|
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 ? '
|
|
82
|
-
<!-- Collection picker -->
|
|
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' : ''}">
|
|
83
84
|
<Popover.Root bind:open={collectionPopoverOpen}>
|
|
84
85
|
<Popover.Trigger>
|
|
85
86
|
{#snippet child({ props })}
|
|
@@ -107,39 +108,22 @@
|
|
|
107
108
|
</Popover.Content>
|
|
108
109
|
</Popover.Root>
|
|
109
110
|
|
|
110
|
-
<!-- Transparent id input -->
|
|
111
111
|
<input
|
|
112
|
-
placeholder="NULL"
|
|
112
|
+
placeholder={isPendingCreate ? "AUTO GENERATED" : "NULL"}
|
|
113
113
|
type="number"
|
|
114
114
|
class="min-w-0 flex-1 bg-transparent outline-none text-xs placeholder:text-muted-foreground"
|
|
115
115
|
value={selectedId ?? ""}
|
|
116
116
|
oninput={onIdChange}
|
|
117
117
|
/>
|
|
118
118
|
|
|
119
|
-
<!-- Primary field badge -->
|
|
120
|
-
{#if displayName}
|
|
121
|
-
<div class="flex shrink-0 items-center bg-background rounded-full border h-6 px-3 shadow-sm">
|
|
122
|
-
{displayName}
|
|
123
|
-
</div>
|
|
124
|
-
{/if}
|
|
125
|
-
|
|
126
|
-
<!-- Create / Select record buttons -->
|
|
127
119
|
{#if selectedCollection}
|
|
128
|
-
<Button
|
|
129
|
-
class="h-6 shrink-0 px-2 font-normal text-xs"
|
|
130
|
-
variant="outline"
|
|
131
|
-
onclick={() => (createDrawerOpen = true)}
|
|
132
|
-
>
|
|
120
|
+
<Button class="h-6 shrink-0 px-2 font-normal text-xs" variant="outline" onclick={() => (createDrawerOpen = true)}>
|
|
133
121
|
<Plus size="13" />
|
|
134
122
|
Create
|
|
135
123
|
</Button>
|
|
136
|
-
<Button
|
|
137
|
-
class="h-6 shrink-0 px-2 font-normal text-xs"
|
|
138
|
-
variant="outline"
|
|
139
|
-
onclick={() => (recordDrawerOpen = true)}
|
|
140
|
-
>
|
|
124
|
+
<Button class="h-6 shrink-0 px-2 font-normal text-xs" variant="outline" onclick={() => (recordDrawerOpen = true)}>
|
|
141
125
|
<Link size="13" />
|
|
142
|
-
|
|
126
|
+
Link
|
|
143
127
|
</Button>
|
|
144
128
|
{/if}
|
|
145
129
|
</div>
|
|
@@ -147,26 +131,17 @@
|
|
|
147
131
|
{#if recordDrawerOpen}
|
|
148
132
|
<Drawer onHide={async () => { recordDrawerOpen = false }}>
|
|
149
133
|
<div class="flex h-12 items-center gap-4 border-b px-4">
|
|
150
|
-
<Button
|
|
151
|
-
variant="outline"
|
|
152
|
-
onclick={() => (recordDrawerOpen = false)}
|
|
153
|
-
class="h-8 w-8 rounded-full text-xs font-normal"
|
|
154
|
-
Icon={ArrowLeft}
|
|
155
|
-
/>
|
|
134
|
+
<Button variant="outline" onclick={() => (recordDrawerOpen = false)} class="h-8 w-8 rounded-full text-xs font-normal" Icon={ArrowLeft} />
|
|
156
135
|
<div class="flex items-center gap-2">
|
|
157
136
|
<div class="text-sm">Select record from</div>
|
|
158
|
-
<span class="rounded-md border bg-muted px-2 py-0.5 text-sm">
|
|
159
|
-
{selectedCollection}
|
|
160
|
-
</span>
|
|
137
|
+
<span class="rounded-md border bg-muted px-2 py-0.5 text-sm">{selectedCollection}</span>
|
|
161
138
|
</div>
|
|
162
139
|
</div>
|
|
163
140
|
<div class="flex-1 overflow-y-auto bg-muted">
|
|
164
141
|
<DataTable
|
|
165
142
|
collectionName={selectedCollection!}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
select: { onSelect: onRecordSelect },
|
|
169
|
-
}}
|
|
143
|
+
filter={selectedId != null ? { id: { $ne: selectedId } } : undefined}
|
|
144
|
+
tableProps={{ showCheckboxes: false, select: { onSelect: onRecordSelect } }}
|
|
170
145
|
/>
|
|
171
146
|
</div>
|
|
172
147
|
</Drawer>
|
|
@@ -176,6 +151,7 @@
|
|
|
176
151
|
<CreateDetailView
|
|
177
152
|
collectionName={selectedCollection}
|
|
178
153
|
onCreated={onPolyCreated}
|
|
154
|
+
onChanges={() => {}}
|
|
179
155
|
onCancel={async () => { createDrawerOpen = false; }}
|
|
180
156
|
/>
|
|
181
157
|
{/if}
|