@lobb-js/studio 0.13.0 → 0.15.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/dataTable/dataTable.svelte +81 -75
- package/dist/components/dataTable/enumBadge.svelte +33 -12
- package/dist/components/dataTable/header.svelte +8 -0
- package/dist/components/detailView/fieldInput.svelte +34 -49
- package/dist/components/singletone.svelte +15 -20
- package/dist/extensions/extension.types.d.ts +1 -1
- package/package.json +2 -2
- package/src/lib/components/dataTable/dataTable.svelte +81 -75
- package/src/lib/components/dataTable/enumBadge.svelte +33 -12
- package/src/lib/components/dataTable/header.svelte +8 -0
- package/src/lib/components/detailView/fieldInput.svelte +34 -49
- package/src/lib/components/singletone.svelte +15 -20
- package/src/lib/extensions/extension.types.ts +2 -1
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import ExtensionsComponents from "../extensionsComponents.svelte";
|
|
19
19
|
import { getExtensionUtils, loadExtensionComponents } from "../../extensions/extensionUtils";
|
|
20
20
|
import Tabs from "./dataTableTabs.svelte";
|
|
21
|
+
import { fade } from "svelte/transition";
|
|
21
22
|
|
|
22
23
|
const { lobb, ctx } = getStudioContext();
|
|
23
24
|
|
|
@@ -84,6 +85,7 @@
|
|
|
84
85
|
});
|
|
85
86
|
|
|
86
87
|
async function loadData(params: any) {
|
|
88
|
+
loading = true;
|
|
87
89
|
// parsing sort before sending the request
|
|
88
90
|
const paramsCopy = $state.snapshot(params);
|
|
89
91
|
const sort: TableProps["sort"] = paramsCopy.sort;
|
|
@@ -154,7 +156,6 @@
|
|
|
154
156
|
/>
|
|
155
157
|
{/snippet}
|
|
156
158
|
|
|
157
|
-
<Tabs {collectionName} {filter} bind:activeTabFilter />
|
|
158
159
|
{#if showHeader}
|
|
159
160
|
<Header bind:params {collectionName} bind:selectedRecords>
|
|
160
161
|
{#snippet left()}
|
|
@@ -162,83 +163,88 @@
|
|
|
162
163
|
{/snippet}
|
|
163
164
|
</Header>
|
|
164
165
|
{/if}
|
|
166
|
+
<Tabs {collectionName} {filter} bind:activeTabFilter />
|
|
165
167
|
<div class="relative flex-1 overflow-auto w-full">
|
|
166
|
-
{#
|
|
167
|
-
<div class="
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
{
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
{
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
Icon={Pencil}
|
|
193
|
-
onSuccessfullSave={async () => {
|
|
194
|
-
params = { ...params };
|
|
195
|
-
}}
|
|
196
|
-
></UpdateDetailViewButton>
|
|
197
|
-
{#if showDelete}
|
|
198
|
-
<Button
|
|
199
|
-
class="h-6 w-6 text-muted-foreground hover:bg-transparent"
|
|
200
|
-
variant="ghost"
|
|
201
|
-
size="icon"
|
|
202
|
-
onclick={() => handleDelete(entry.id)}
|
|
203
|
-
Icon={Trash}
|
|
204
|
-
></Button>
|
|
205
|
-
{/if}
|
|
206
|
-
{#await getWorkflowTools($state.snapshot(entry))}
|
|
207
|
-
<div></div>
|
|
208
|
-
{:then workflowTools}
|
|
209
|
-
{#each workflowTools as workflowTool}
|
|
210
|
-
<Button
|
|
168
|
+
{#key activeTabFilter}
|
|
169
|
+
<div class="h-full w-full" in:fade={{ duration: 120 }}>
|
|
170
|
+
{#if loading}
|
|
171
|
+
<div class="flex flex-col gap-2 p-2 w-full">
|
|
172
|
+
<Skeleton class="h-8 w-full" />
|
|
173
|
+
<Skeleton class="h-8 w-[80%]" />
|
|
174
|
+
<Skeleton class="h-8 w-[60%]" />
|
|
175
|
+
</div>
|
|
176
|
+
{:else}
|
|
177
|
+
<Table
|
|
178
|
+
{data}
|
|
179
|
+
{columns}
|
|
180
|
+
showCollapsible={doesCollectionHasChildren}
|
|
181
|
+
selectByColumn="id"
|
|
182
|
+
showLastRowBorder={true}
|
|
183
|
+
showLastColumnBorder={true}
|
|
184
|
+
bind:sort={params.sort}
|
|
185
|
+
bind:selectedRecords
|
|
186
|
+
{unifiedBgColor}
|
|
187
|
+
bind:tableWidth={dataTableWidth}
|
|
188
|
+
{...tableProps}
|
|
189
|
+
rowActions={hasRowActions ? rowActionsSnippet : undefined}>
|
|
190
|
+
{#snippet tools(entry)}
|
|
191
|
+
<UpdateDetailViewButton
|
|
192
|
+
{collectionName}
|
|
193
|
+
recordId={entry.id}
|
|
211
194
|
variant="ghost"
|
|
212
195
|
class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
|
|
213
|
-
Icon={
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
></
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
196
|
+
Icon={Pencil}
|
|
197
|
+
onSuccessfullSave={async () => {
|
|
198
|
+
params = { ...params };
|
|
199
|
+
}}
|
|
200
|
+
></UpdateDetailViewButton>
|
|
201
|
+
{#if showDelete}
|
|
202
|
+
<Button
|
|
203
|
+
class="h-6 w-6 text-muted-foreground hover:bg-transparent"
|
|
204
|
+
variant="ghost"
|
|
205
|
+
size="icon"
|
|
206
|
+
onclick={() => handleDelete(entry.id)}
|
|
207
|
+
Icon={Trash}
|
|
208
|
+
></Button>
|
|
209
|
+
{/if}
|
|
210
|
+
{#await getWorkflowTools($state.snapshot(entry))}
|
|
211
|
+
<div></div>
|
|
212
|
+
{:then workflowTools}
|
|
213
|
+
{#each workflowTools as workflowTool}
|
|
214
|
+
<Button
|
|
215
|
+
variant="ghost"
|
|
216
|
+
class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
|
|
217
|
+
Icon={icons[
|
|
218
|
+
workflowTool.icon as keyof typeof icons
|
|
219
|
+
]}
|
|
220
|
+
onclick={workflowTool.onclick}
|
|
221
|
+
></Button>
|
|
222
|
+
{/each}
|
|
223
|
+
{/await}
|
|
224
|
+
{/snippet}
|
|
225
|
+
{#snippet overrideCell(value, column, entry)}
|
|
226
|
+
<FieldCell
|
|
227
|
+
{collectionName}
|
|
228
|
+
fieldName={column.id}
|
|
229
|
+
{value}
|
|
230
|
+
{entry}
|
|
231
|
+
tableParams={params}
|
|
232
|
+
/>
|
|
233
|
+
{/snippet}
|
|
234
|
+
{#snippet collapsible(entry)}
|
|
235
|
+
<ChildRecords
|
|
236
|
+
{collectionName}
|
|
237
|
+
recordId={entry.id}
|
|
238
|
+
width={dataTableWidth > dataTableContainerWidth
|
|
239
|
+
? dataTableContainerWidth
|
|
240
|
+
: dataTableWidth}
|
|
241
|
+
unifiedBgColor={unifiedBgColor ?? "bg-background"}
|
|
242
|
+
/>
|
|
243
|
+
{/snippet}
|
|
244
|
+
</Table>
|
|
245
|
+
{/if}
|
|
246
|
+
</div>
|
|
247
|
+
{/key}
|
|
242
248
|
</div>
|
|
243
249
|
{#if showFooter}
|
|
244
250
|
<Footer
|
|
@@ -8,26 +8,47 @@
|
|
|
8
8
|
|
|
9
9
|
const { value, enum: enumOptions }: Props = $props();
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
const colorClasses: Record<string, string> = {
|
|
12
|
+
red: "bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-300 dark:border-red-800",
|
|
13
|
+
rose: "bg-rose-100 text-rose-700 border-rose-200 dark:bg-rose-900/30 dark:text-rose-300 dark:border-rose-800",
|
|
14
|
+
pink: "bg-pink-100 text-pink-700 border-pink-200 dark:bg-pink-900/30 dark:text-pink-300 dark:border-pink-800",
|
|
15
|
+
fuchsia: "bg-fuchsia-100 text-fuchsia-700 border-fuchsia-200 dark:bg-fuchsia-900/30 dark:text-fuchsia-300 dark:border-fuchsia-800",
|
|
16
|
+
purple: "bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-900/30 dark:text-purple-300 dark:border-purple-800",
|
|
17
|
+
violet: "bg-violet-100 text-violet-700 border-violet-200 dark:bg-violet-900/30 dark:text-violet-300 dark:border-violet-800",
|
|
18
|
+
indigo: "bg-indigo-100 text-indigo-700 border-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-300 dark:border-indigo-800",
|
|
19
|
+
blue: "bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-800",
|
|
20
|
+
sky: "bg-sky-100 text-sky-700 border-sky-200 dark:bg-sky-900/30 dark:text-sky-300 dark:border-sky-800",
|
|
21
|
+
cyan: "bg-cyan-100 text-cyan-700 border-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-300 dark:border-cyan-800",
|
|
22
|
+
teal: "bg-teal-100 text-teal-700 border-teal-200 dark:bg-teal-900/30 dark:text-teal-300 dark:border-teal-800",
|
|
23
|
+
emerald: "bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-300 dark:border-emerald-800",
|
|
24
|
+
green: "bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-300 dark:border-green-800",
|
|
25
|
+
lime: "bg-lime-100 text-lime-700 border-lime-200 dark:bg-lime-900/30 dark:text-lime-300 dark:border-lime-800",
|
|
26
|
+
yellow: "bg-yellow-100 text-yellow-700 border-yellow-300 dark:bg-yellow-900/30 dark:text-yellow-300 dark:border-yellow-800",
|
|
27
|
+
amber: "bg-amber-100 text-amber-700 border-amber-200 dark:bg-amber-900/30 dark:text-amber-300 dark:border-amber-800",
|
|
28
|
+
orange: "bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-900/30 dark:text-orange-300 dark:border-orange-800",
|
|
29
|
+
slate: "bg-slate-100 text-slate-600 border-slate-200 dark:bg-slate-800/50 dark:text-slate-300 dark:border-slate-700",
|
|
30
|
+
zinc: "bg-zinc-100 text-zinc-600 border-zinc-200 dark:bg-zinc-800/50 dark:text-zinc-300 dark:border-zinc-700",
|
|
31
|
+
gray: "bg-gray-100 text-gray-600 border-gray-200 dark:bg-gray-800/50 dark:text-gray-300 dark:border-gray-700",
|
|
32
|
+
stone: "bg-stone-100 text-stone-600 border-stone-200 dark:bg-stone-800/50 dark:text-stone-300 dark:border-stone-700",
|
|
33
|
+
neutral: "bg-neutral-100 text-neutral-600 border-neutral-200 dark:bg-neutral-800/50 dark:text-neutral-300 dark:border-neutral-700",
|
|
18
34
|
};
|
|
19
35
|
|
|
20
36
|
const normalizedOptions = $derived(
|
|
21
|
-
(enumOptions as Array<string | EnumOption>).map((e) =>
|
|
22
|
-
typeof e === "
|
|
37
|
+
(enumOptions as Array<string | number | EnumOption>).map((e) =>
|
|
38
|
+
typeof e === "object" ? e : { value: e, color: undefined, description: undefined },
|
|
23
39
|
),
|
|
24
40
|
);
|
|
25
41
|
|
|
26
|
-
const enumOption = $derived(normalizedOptions.find((e) => e.value === value));
|
|
42
|
+
const enumOption = $derived(normalizedOptions.find((e) => String(e.value) === String(value)));
|
|
27
43
|
</script>
|
|
28
44
|
|
|
29
45
|
{#if enumOption}
|
|
30
|
-
<span class="
|
|
31
|
-
{
|
|
46
|
+
<span class="flex items-center gap-1.5">
|
|
47
|
+
<span class="px-2 py-0.5 rounded-full text-xs font-medium border {enumOption.color ? colorClasses[enumOption.color] : colorClasses['gray']}">
|
|
48
|
+
{enumOption.value}
|
|
49
|
+
</span>
|
|
50
|
+
{#if enumOption.description}
|
|
51
|
+
<span class="text-xs text-muted-foreground">{enumOption.description}</span>
|
|
52
|
+
{/if}
|
|
32
53
|
</span>
|
|
33
54
|
{/if}
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import CreateManyButton from "../createManyButton.svelte";
|
|
11
11
|
import { showDialog } from "../confirmationDialog/store.svelte";
|
|
12
12
|
import CreateDetailViewButton from "../detailView/create/createDetailViewButton.svelte";
|
|
13
|
+
import ExtensionsComponents from "../extensionsComponents.svelte";
|
|
14
|
+
import { getExtensionUtils } from "../../extensions/extensionUtils";
|
|
13
15
|
import type { Snippet } from "svelte";
|
|
14
16
|
|
|
15
17
|
interface Props {
|
|
@@ -142,6 +144,12 @@
|
|
|
142
144
|
Icon={SquareStack}
|
|
143
145
|
onSuccessfullSave={() => (params = { ...params })}
|
|
144
146
|
></CreateManyButton>
|
|
147
|
+
<ExtensionsComponents
|
|
148
|
+
name="listView.header.actions"
|
|
149
|
+
utils={getExtensionUtils(lobb, ctx)}
|
|
150
|
+
{collectionName}
|
|
151
|
+
refresh={() => { params = { ...params }; }}
|
|
152
|
+
/>
|
|
145
153
|
<CreateDetailViewButton
|
|
146
154
|
{collectionName}
|
|
147
155
|
variant="default"
|
|
@@ -93,75 +93,60 @@
|
|
|
93
93
|
{destructive}
|
|
94
94
|
/>
|
|
95
95
|
</ExtensionsComponents>
|
|
96
|
-
{:else if field.
|
|
97
|
-
{@const rawEnum = field.enum as (string | EnumOption)[]}
|
|
98
|
-
{@const
|
|
96
|
+
{:else if field.enum}
|
|
97
|
+
{@const rawEnum = field.enum as (string | number | EnumOption)[]}
|
|
98
|
+
{@const isEnumOption = rawEnum.length > 0 && typeof rawEnum[0] === "object"}
|
|
99
|
+
{@const enumOptions = isEnumOption ? rawEnum as EnumOption[] : undefined}
|
|
99
100
|
<Select.Root
|
|
100
101
|
type="single"
|
|
101
|
-
|
|
102
|
-
value
|
|
103
|
-
|
|
102
|
+
bind:value={
|
|
103
|
+
() => value != null ? String(value) : null,
|
|
104
|
+
(v) => {
|
|
105
|
+
if (v == null) { value = null; return; }
|
|
106
|
+
const isNumeric = field.type === "integer" || field.type === "long" || field.type === "decimal" || field.type === "float";
|
|
107
|
+
value = isNumeric ? Number(v) : v;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
104
110
|
>
|
|
105
111
|
<Select.Trigger
|
|
106
|
-
placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
|
|
107
112
|
class="
|
|
108
113
|
h-9 w-full bg-muted/30 pr-8
|
|
109
114
|
{destructive ? 'border-destructive bg-destructive/10' : ''}
|
|
110
115
|
"
|
|
111
116
|
>
|
|
112
|
-
{#if value}
|
|
113
|
-
<EnumBadge {value} enum={enumOptions} />
|
|
117
|
+
{#if value != null && enumOptions}
|
|
118
|
+
<EnumBadge value={String(value)} enum={enumOptions} />
|
|
119
|
+
{:else if value != null}
|
|
120
|
+
{value}
|
|
114
121
|
{:else}
|
|
115
|
-
NULL
|
|
122
|
+
<span class="text-muted-foreground">{ui?.placeholder ?? "NULL"}</span>
|
|
116
123
|
{/if}
|
|
117
124
|
</Select.Trigger>
|
|
118
125
|
<Select.Content>
|
|
119
126
|
<Select.Group>
|
|
120
|
-
{#each
|
|
121
|
-
|
|
122
|
-
|
|
127
|
+
{#each rawEnum as option}
|
|
128
|
+
{@const optionValue = typeof option === "object" ? String(option.value) : String(option)}
|
|
129
|
+
<Select.Item value={optionValue} label={optionValue}>
|
|
130
|
+
{#if enumOptions}
|
|
131
|
+
<EnumBadge value={optionValue} enum={enumOptions} />
|
|
132
|
+
{:else}
|
|
133
|
+
{optionValue}
|
|
134
|
+
{/if}
|
|
123
135
|
</Select.Item>
|
|
124
136
|
{/each}
|
|
125
137
|
</Select.Group>
|
|
126
138
|
</Select.Content>
|
|
127
139
|
</Select.Root>
|
|
128
140
|
{:else if field.type === "string"}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
|
|
139
|
-
class="
|
|
140
|
-
h-9 w-full bg-muted/30 pr-8
|
|
141
|
-
{destructive ? 'border-destructive bg-destructive/10' : ''}
|
|
142
|
-
"
|
|
143
|
-
>
|
|
144
|
-
{value ? value : "NULL"}
|
|
145
|
-
</Select.Trigger>
|
|
146
|
-
<Select.Content>
|
|
147
|
-
<Select.Group>
|
|
148
|
-
{#each field.validators.enum as option}
|
|
149
|
-
<Select.Item value={option} label={option} />
|
|
150
|
-
{/each}
|
|
151
|
-
</Select.Group>
|
|
152
|
-
</Select.Content>
|
|
153
|
-
</Select.Root>
|
|
154
|
-
{:else}
|
|
155
|
-
<Input
|
|
156
|
-
placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
|
|
157
|
-
type="text"
|
|
158
|
-
class="
|
|
159
|
-
bg-muted/30 text-xs
|
|
160
|
-
{destructive ? 'border-destructive bg-destructive/10' : ''}
|
|
161
|
-
"
|
|
162
|
-
bind:value
|
|
163
|
-
/>
|
|
164
|
-
{/if}
|
|
141
|
+
<Input
|
|
142
|
+
placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
|
|
143
|
+
type="text"
|
|
144
|
+
class="
|
|
145
|
+
bg-muted/30 text-xs
|
|
146
|
+
{destructive ? 'border-destructive bg-destructive/10' : ''}
|
|
147
|
+
"
|
|
148
|
+
bind:value
|
|
149
|
+
/>
|
|
165
150
|
{:else if field.type === "text"}
|
|
166
151
|
<Textarea
|
|
167
152
|
placeholder={ui?.placeholder ? ui.placeholder : value === "" ? "EMPTY STRING" : "NULL"}
|
|
@@ -6,30 +6,27 @@
|
|
|
6
6
|
import Button from "./ui/button/button.svelte";
|
|
7
7
|
import { onMount } from "svelte";
|
|
8
8
|
import { getStudioContext } from "../context";
|
|
9
|
+
import { toast } from "svelte-sonner";
|
|
10
|
+
import Skeleton from "./ui/skeleton/skeleton.svelte";
|
|
9
11
|
|
|
10
12
|
const { lobb, ctx } = getStudioContext();
|
|
11
|
-
import { toast } from "svelte-sonner";
|
|
12
|
-
import Skeleton from "./ui/skeleton/skeleton.svelte";
|
|
13
13
|
|
|
14
14
|
interface Props {
|
|
15
15
|
collectionName: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
let {
|
|
19
|
-
collectionName,
|
|
20
|
-
}: Props = $props();
|
|
18
|
+
let { collectionName }: Props = $props();
|
|
21
19
|
|
|
22
20
|
let entry = $state({});
|
|
23
21
|
let loading = $state(true);
|
|
24
|
-
let singletonExists = $state(true);
|
|
25
22
|
const formFields = getCollectionFields(ctx, collectionName);
|
|
26
23
|
|
|
27
24
|
onMount(async () => {
|
|
28
|
-
const result = await lobb.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
const result = await fetch(`${lobb.lobbUrl}/api/collections/${collectionName}/singleton`, {
|
|
26
|
+
headers: lobb.getHeaders() as HeadersInit,
|
|
27
|
+
});
|
|
28
|
+
if (result.status !== 404) {
|
|
29
|
+
const json = await result.json();
|
|
33
30
|
entry = json.data;
|
|
34
31
|
}
|
|
35
32
|
loading = false;
|
|
@@ -43,29 +40,27 @@
|
|
|
43
40
|
}
|
|
44
41
|
</script>
|
|
45
42
|
|
|
46
|
-
<div>
|
|
47
|
-
<div class="flex justify-between items-center gap-2 p-2 border-b
|
|
43
|
+
<div class="flex flex-col h-full bg-background">
|
|
44
|
+
<div class="flex justify-between items-center gap-2 p-2 border-b h-10 shrink-0">
|
|
48
45
|
<div class="flex items-center gap-1">
|
|
49
46
|
<SidebarTrigger />
|
|
50
47
|
</div>
|
|
51
48
|
<div>
|
|
52
|
-
<Button
|
|
53
|
-
class="h-7 px-2 font-normal text-xs"
|
|
54
|
-
Icon={Save}
|
|
55
|
-
onclick={handleSave}
|
|
56
|
-
>
|
|
49
|
+
<Button class="h-7 px-2 font-normal text-xs" Icon={Save} onclick={handleSave}>
|
|
57
50
|
Save
|
|
58
51
|
</Button>
|
|
59
52
|
</div>
|
|
60
53
|
</div>
|
|
61
54
|
|
|
62
55
|
{#if loading}
|
|
63
|
-
<div class="flex flex-col gap-2 p-
|
|
56
|
+
<div class="flex flex-col gap-2 p-4 w-full">
|
|
64
57
|
<Skeleton class="h-8 w-full" />
|
|
65
58
|
<Skeleton class="h-8 w-[80%]" />
|
|
66
59
|
<Skeleton class="h-8 w-[60%]" />
|
|
67
60
|
</div>
|
|
68
61
|
{:else}
|
|
69
|
-
<
|
|
62
|
+
<div class="flex-1 overflow-y-auto max-w-xl">
|
|
63
|
+
<DetailViewForm bind:value={entry} fields={formFields} />
|
|
64
|
+
</div>
|
|
70
65
|
{/if}
|
|
71
66
|
</div>
|
|
@@ -73,7 +73,7 @@ export interface ExtensionProps {
|
|
|
73
73
|
utils: ExtensionUtils;
|
|
74
74
|
[key: string]: any;
|
|
75
75
|
}
|
|
76
|
-
export type ExtensionComponentKey = `pages.${string}` | "studio.listView" | `dvFields.topRight.${string}.${string}` | `detailView.update.subRecords.${string}` | `detailView.create.subRecords.${string}` | `detailView.fields.topRight.${string}.${string}` | `detailView.fields.foreignKey.${string}` | `listView.entry.children.${string}` | `listView.entry.actions`;
|
|
76
|
+
export type ExtensionComponentKey = `pages.${string}` | "studio.listView" | `dvFields.topRight.${string}.${string}` | `detailView.update.subRecords.${string}` | `detailView.create.subRecords.${string}` | `detailView.fields.topRight.${string}.${string}` | `detailView.fields.foreignKey.${string}` | `listView.entry.children.${string}` | `listView.entry.actions` | `listView.header.actions`;
|
|
77
77
|
export type ExtensionComponent = any | {
|
|
78
78
|
component: any;
|
|
79
79
|
when: (props: Record<string, any>) => boolean;
|
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.15.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"postpublish": "./scripts/postpublish.sh"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@lobb-js/core": "^0.
|
|
45
|
+
"@lobb-js/core": "^0.21.0",
|
|
46
46
|
"@chromatic-com/storybook": "^4.1.2",
|
|
47
47
|
"@storybook/addon-a11y": "^10.0.1",
|
|
48
48
|
"@storybook/addon-docs": "^10.0.1",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import ExtensionsComponents from "../extensionsComponents.svelte";
|
|
19
19
|
import { getExtensionUtils, loadExtensionComponents } from "../../extensions/extensionUtils";
|
|
20
20
|
import Tabs from "./dataTableTabs.svelte";
|
|
21
|
+
import { fade } from "svelte/transition";
|
|
21
22
|
|
|
22
23
|
const { lobb, ctx } = getStudioContext();
|
|
23
24
|
|
|
@@ -84,6 +85,7 @@
|
|
|
84
85
|
});
|
|
85
86
|
|
|
86
87
|
async function loadData(params: any) {
|
|
88
|
+
loading = true;
|
|
87
89
|
// parsing sort before sending the request
|
|
88
90
|
const paramsCopy = $state.snapshot(params);
|
|
89
91
|
const sort: TableProps["sort"] = paramsCopy.sort;
|
|
@@ -154,7 +156,6 @@
|
|
|
154
156
|
/>
|
|
155
157
|
{/snippet}
|
|
156
158
|
|
|
157
|
-
<Tabs {collectionName} {filter} bind:activeTabFilter />
|
|
158
159
|
{#if showHeader}
|
|
159
160
|
<Header bind:params {collectionName} bind:selectedRecords>
|
|
160
161
|
{#snippet left()}
|
|
@@ -162,83 +163,88 @@
|
|
|
162
163
|
{/snippet}
|
|
163
164
|
</Header>
|
|
164
165
|
{/if}
|
|
166
|
+
<Tabs {collectionName} {filter} bind:activeTabFilter />
|
|
165
167
|
<div class="relative flex-1 overflow-auto w-full">
|
|
166
|
-
{#
|
|
167
|
-
<div class="
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
{
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
{
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
Icon={Pencil}
|
|
193
|
-
onSuccessfullSave={async () => {
|
|
194
|
-
params = { ...params };
|
|
195
|
-
}}
|
|
196
|
-
></UpdateDetailViewButton>
|
|
197
|
-
{#if showDelete}
|
|
198
|
-
<Button
|
|
199
|
-
class="h-6 w-6 text-muted-foreground hover:bg-transparent"
|
|
200
|
-
variant="ghost"
|
|
201
|
-
size="icon"
|
|
202
|
-
onclick={() => handleDelete(entry.id)}
|
|
203
|
-
Icon={Trash}
|
|
204
|
-
></Button>
|
|
205
|
-
{/if}
|
|
206
|
-
{#await getWorkflowTools($state.snapshot(entry))}
|
|
207
|
-
<div></div>
|
|
208
|
-
{:then workflowTools}
|
|
209
|
-
{#each workflowTools as workflowTool}
|
|
210
|
-
<Button
|
|
168
|
+
{#key activeTabFilter}
|
|
169
|
+
<div class="h-full w-full" in:fade={{ duration: 120 }}>
|
|
170
|
+
{#if loading}
|
|
171
|
+
<div class="flex flex-col gap-2 p-2 w-full">
|
|
172
|
+
<Skeleton class="h-8 w-full" />
|
|
173
|
+
<Skeleton class="h-8 w-[80%]" />
|
|
174
|
+
<Skeleton class="h-8 w-[60%]" />
|
|
175
|
+
</div>
|
|
176
|
+
{:else}
|
|
177
|
+
<Table
|
|
178
|
+
{data}
|
|
179
|
+
{columns}
|
|
180
|
+
showCollapsible={doesCollectionHasChildren}
|
|
181
|
+
selectByColumn="id"
|
|
182
|
+
showLastRowBorder={true}
|
|
183
|
+
showLastColumnBorder={true}
|
|
184
|
+
bind:sort={params.sort}
|
|
185
|
+
bind:selectedRecords
|
|
186
|
+
{unifiedBgColor}
|
|
187
|
+
bind:tableWidth={dataTableWidth}
|
|
188
|
+
{...tableProps}
|
|
189
|
+
rowActions={hasRowActions ? rowActionsSnippet : undefined}>
|
|
190
|
+
{#snippet tools(entry)}
|
|
191
|
+
<UpdateDetailViewButton
|
|
192
|
+
{collectionName}
|
|
193
|
+
recordId={entry.id}
|
|
211
194
|
variant="ghost"
|
|
212
195
|
class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
|
|
213
|
-
Icon={
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
></
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
196
|
+
Icon={Pencil}
|
|
197
|
+
onSuccessfullSave={async () => {
|
|
198
|
+
params = { ...params };
|
|
199
|
+
}}
|
|
200
|
+
></UpdateDetailViewButton>
|
|
201
|
+
{#if showDelete}
|
|
202
|
+
<Button
|
|
203
|
+
class="h-6 w-6 text-muted-foreground hover:bg-transparent"
|
|
204
|
+
variant="ghost"
|
|
205
|
+
size="icon"
|
|
206
|
+
onclick={() => handleDelete(entry.id)}
|
|
207
|
+
Icon={Trash}
|
|
208
|
+
></Button>
|
|
209
|
+
{/if}
|
|
210
|
+
{#await getWorkflowTools($state.snapshot(entry))}
|
|
211
|
+
<div></div>
|
|
212
|
+
{:then workflowTools}
|
|
213
|
+
{#each workflowTools as workflowTool}
|
|
214
|
+
<Button
|
|
215
|
+
variant="ghost"
|
|
216
|
+
class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
|
|
217
|
+
Icon={icons[
|
|
218
|
+
workflowTool.icon as keyof typeof icons
|
|
219
|
+
]}
|
|
220
|
+
onclick={workflowTool.onclick}
|
|
221
|
+
></Button>
|
|
222
|
+
{/each}
|
|
223
|
+
{/await}
|
|
224
|
+
{/snippet}
|
|
225
|
+
{#snippet overrideCell(value, column, entry)}
|
|
226
|
+
<FieldCell
|
|
227
|
+
{collectionName}
|
|
228
|
+
fieldName={column.id}
|
|
229
|
+
{value}
|
|
230
|
+
{entry}
|
|
231
|
+
tableParams={params}
|
|
232
|
+
/>
|
|
233
|
+
{/snippet}
|
|
234
|
+
{#snippet collapsible(entry)}
|
|
235
|
+
<ChildRecords
|
|
236
|
+
{collectionName}
|
|
237
|
+
recordId={entry.id}
|
|
238
|
+
width={dataTableWidth > dataTableContainerWidth
|
|
239
|
+
? dataTableContainerWidth
|
|
240
|
+
: dataTableWidth}
|
|
241
|
+
unifiedBgColor={unifiedBgColor ?? "bg-background"}
|
|
242
|
+
/>
|
|
243
|
+
{/snippet}
|
|
244
|
+
</Table>
|
|
245
|
+
{/if}
|
|
246
|
+
</div>
|
|
247
|
+
{/key}
|
|
242
248
|
</div>
|
|
243
249
|
{#if showFooter}
|
|
244
250
|
<Footer
|
|
@@ -8,26 +8,47 @@
|
|
|
8
8
|
|
|
9
9
|
const { value, enum: enumOptions }: Props = $props();
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
const colorClasses: Record<string, string> = {
|
|
12
|
+
red: "bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-300 dark:border-red-800",
|
|
13
|
+
rose: "bg-rose-100 text-rose-700 border-rose-200 dark:bg-rose-900/30 dark:text-rose-300 dark:border-rose-800",
|
|
14
|
+
pink: "bg-pink-100 text-pink-700 border-pink-200 dark:bg-pink-900/30 dark:text-pink-300 dark:border-pink-800",
|
|
15
|
+
fuchsia: "bg-fuchsia-100 text-fuchsia-700 border-fuchsia-200 dark:bg-fuchsia-900/30 dark:text-fuchsia-300 dark:border-fuchsia-800",
|
|
16
|
+
purple: "bg-purple-100 text-purple-700 border-purple-200 dark:bg-purple-900/30 dark:text-purple-300 dark:border-purple-800",
|
|
17
|
+
violet: "bg-violet-100 text-violet-700 border-violet-200 dark:bg-violet-900/30 dark:text-violet-300 dark:border-violet-800",
|
|
18
|
+
indigo: "bg-indigo-100 text-indigo-700 border-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-300 dark:border-indigo-800",
|
|
19
|
+
blue: "bg-blue-100 text-blue-700 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-800",
|
|
20
|
+
sky: "bg-sky-100 text-sky-700 border-sky-200 dark:bg-sky-900/30 dark:text-sky-300 dark:border-sky-800",
|
|
21
|
+
cyan: "bg-cyan-100 text-cyan-700 border-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-300 dark:border-cyan-800",
|
|
22
|
+
teal: "bg-teal-100 text-teal-700 border-teal-200 dark:bg-teal-900/30 dark:text-teal-300 dark:border-teal-800",
|
|
23
|
+
emerald: "bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-300 dark:border-emerald-800",
|
|
24
|
+
green: "bg-green-100 text-green-700 border-green-200 dark:bg-green-900/30 dark:text-green-300 dark:border-green-800",
|
|
25
|
+
lime: "bg-lime-100 text-lime-700 border-lime-200 dark:bg-lime-900/30 dark:text-lime-300 dark:border-lime-800",
|
|
26
|
+
yellow: "bg-yellow-100 text-yellow-700 border-yellow-300 dark:bg-yellow-900/30 dark:text-yellow-300 dark:border-yellow-800",
|
|
27
|
+
amber: "bg-amber-100 text-amber-700 border-amber-200 dark:bg-amber-900/30 dark:text-amber-300 dark:border-amber-800",
|
|
28
|
+
orange: "bg-orange-100 text-orange-700 border-orange-200 dark:bg-orange-900/30 dark:text-orange-300 dark:border-orange-800",
|
|
29
|
+
slate: "bg-slate-100 text-slate-600 border-slate-200 dark:bg-slate-800/50 dark:text-slate-300 dark:border-slate-700",
|
|
30
|
+
zinc: "bg-zinc-100 text-zinc-600 border-zinc-200 dark:bg-zinc-800/50 dark:text-zinc-300 dark:border-zinc-700",
|
|
31
|
+
gray: "bg-gray-100 text-gray-600 border-gray-200 dark:bg-gray-800/50 dark:text-gray-300 dark:border-gray-700",
|
|
32
|
+
stone: "bg-stone-100 text-stone-600 border-stone-200 dark:bg-stone-800/50 dark:text-stone-300 dark:border-stone-700",
|
|
33
|
+
neutral: "bg-neutral-100 text-neutral-600 border-neutral-200 dark:bg-neutral-800/50 dark:text-neutral-300 dark:border-neutral-700",
|
|
18
34
|
};
|
|
19
35
|
|
|
20
36
|
const normalizedOptions = $derived(
|
|
21
|
-
(enumOptions as Array<string | EnumOption>).map((e) =>
|
|
22
|
-
typeof e === "
|
|
37
|
+
(enumOptions as Array<string | number | EnumOption>).map((e) =>
|
|
38
|
+
typeof e === "object" ? e : { value: e, color: undefined, description: undefined },
|
|
23
39
|
),
|
|
24
40
|
);
|
|
25
41
|
|
|
26
|
-
const enumOption = $derived(normalizedOptions.find((e) => e.value === value));
|
|
42
|
+
const enumOption = $derived(normalizedOptions.find((e) => String(e.value) === String(value)));
|
|
27
43
|
</script>
|
|
28
44
|
|
|
29
45
|
{#if enumOption}
|
|
30
|
-
<span class="
|
|
31
|
-
{
|
|
46
|
+
<span class="flex items-center gap-1.5">
|
|
47
|
+
<span class="px-2 py-0.5 rounded-full text-xs font-medium border {enumOption.color ? colorClasses[enumOption.color] : colorClasses['gray']}">
|
|
48
|
+
{enumOption.value}
|
|
49
|
+
</span>
|
|
50
|
+
{#if enumOption.description}
|
|
51
|
+
<span class="text-xs text-muted-foreground">{enumOption.description}</span>
|
|
52
|
+
{/if}
|
|
32
53
|
</span>
|
|
33
54
|
{/if}
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
import CreateManyButton from "../createManyButton.svelte";
|
|
11
11
|
import { showDialog } from "../confirmationDialog/store.svelte";
|
|
12
12
|
import CreateDetailViewButton from "../detailView/create/createDetailViewButton.svelte";
|
|
13
|
+
import ExtensionsComponents from "../extensionsComponents.svelte";
|
|
14
|
+
import { getExtensionUtils } from "../../extensions/extensionUtils";
|
|
13
15
|
import type { Snippet } from "svelte";
|
|
14
16
|
|
|
15
17
|
interface Props {
|
|
@@ -142,6 +144,12 @@
|
|
|
142
144
|
Icon={SquareStack}
|
|
143
145
|
onSuccessfullSave={() => (params = { ...params })}
|
|
144
146
|
></CreateManyButton>
|
|
147
|
+
<ExtensionsComponents
|
|
148
|
+
name="listView.header.actions"
|
|
149
|
+
utils={getExtensionUtils(lobb, ctx)}
|
|
150
|
+
{collectionName}
|
|
151
|
+
refresh={() => { params = { ...params }; }}
|
|
152
|
+
/>
|
|
145
153
|
<CreateDetailViewButton
|
|
146
154
|
{collectionName}
|
|
147
155
|
variant="default"
|
|
@@ -93,75 +93,60 @@
|
|
|
93
93
|
{destructive}
|
|
94
94
|
/>
|
|
95
95
|
</ExtensionsComponents>
|
|
96
|
-
{:else if field.
|
|
97
|
-
{@const rawEnum = field.enum as (string | EnumOption)[]}
|
|
98
|
-
{@const
|
|
96
|
+
{:else if field.enum}
|
|
97
|
+
{@const rawEnum = field.enum as (string | number | EnumOption)[]}
|
|
98
|
+
{@const isEnumOption = rawEnum.length > 0 && typeof rawEnum[0] === "object"}
|
|
99
|
+
{@const enumOptions = isEnumOption ? rawEnum as EnumOption[] : undefined}
|
|
99
100
|
<Select.Root
|
|
100
101
|
type="single"
|
|
101
|
-
|
|
102
|
-
value
|
|
103
|
-
|
|
102
|
+
bind:value={
|
|
103
|
+
() => value != null ? String(value) : null,
|
|
104
|
+
(v) => {
|
|
105
|
+
if (v == null) { value = null; return; }
|
|
106
|
+
const isNumeric = field.type === "integer" || field.type === "long" || field.type === "decimal" || field.type === "float";
|
|
107
|
+
value = isNumeric ? Number(v) : v;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
104
110
|
>
|
|
105
111
|
<Select.Trigger
|
|
106
|
-
placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
|
|
107
112
|
class="
|
|
108
113
|
h-9 w-full bg-muted/30 pr-8
|
|
109
114
|
{destructive ? 'border-destructive bg-destructive/10' : ''}
|
|
110
115
|
"
|
|
111
116
|
>
|
|
112
|
-
{#if value}
|
|
113
|
-
<EnumBadge {value} enum={enumOptions} />
|
|
117
|
+
{#if value != null && enumOptions}
|
|
118
|
+
<EnumBadge value={String(value)} enum={enumOptions} />
|
|
119
|
+
{:else if value != null}
|
|
120
|
+
{value}
|
|
114
121
|
{:else}
|
|
115
|
-
NULL
|
|
122
|
+
<span class="text-muted-foreground">{ui?.placeholder ?? "NULL"}</span>
|
|
116
123
|
{/if}
|
|
117
124
|
</Select.Trigger>
|
|
118
125
|
<Select.Content>
|
|
119
126
|
<Select.Group>
|
|
120
|
-
{#each
|
|
121
|
-
|
|
122
|
-
|
|
127
|
+
{#each rawEnum as option}
|
|
128
|
+
{@const optionValue = typeof option === "object" ? String(option.value) : String(option)}
|
|
129
|
+
<Select.Item value={optionValue} label={optionValue}>
|
|
130
|
+
{#if enumOptions}
|
|
131
|
+
<EnumBadge value={optionValue} enum={enumOptions} />
|
|
132
|
+
{:else}
|
|
133
|
+
{optionValue}
|
|
134
|
+
{/if}
|
|
123
135
|
</Select.Item>
|
|
124
136
|
{/each}
|
|
125
137
|
</Select.Group>
|
|
126
138
|
</Select.Content>
|
|
127
139
|
</Select.Root>
|
|
128
140
|
{:else if field.type === "string"}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
|
|
139
|
-
class="
|
|
140
|
-
h-9 w-full bg-muted/30 pr-8
|
|
141
|
-
{destructive ? 'border-destructive bg-destructive/10' : ''}
|
|
142
|
-
"
|
|
143
|
-
>
|
|
144
|
-
{value ? value : "NULL"}
|
|
145
|
-
</Select.Trigger>
|
|
146
|
-
<Select.Content>
|
|
147
|
-
<Select.Group>
|
|
148
|
-
{#each field.validators.enum as option}
|
|
149
|
-
<Select.Item value={option} label={option} />
|
|
150
|
-
{/each}
|
|
151
|
-
</Select.Group>
|
|
152
|
-
</Select.Content>
|
|
153
|
-
</Select.Root>
|
|
154
|
-
{:else}
|
|
155
|
-
<Input
|
|
156
|
-
placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
|
|
157
|
-
type="text"
|
|
158
|
-
class="
|
|
159
|
-
bg-muted/30 text-xs
|
|
160
|
-
{destructive ? 'border-destructive bg-destructive/10' : ''}
|
|
161
|
-
"
|
|
162
|
-
bind:value
|
|
163
|
-
/>
|
|
164
|
-
{/if}
|
|
141
|
+
<Input
|
|
142
|
+
placeholder={ui?.placeholder ? ui.placeholder : "NULL"}
|
|
143
|
+
type="text"
|
|
144
|
+
class="
|
|
145
|
+
bg-muted/30 text-xs
|
|
146
|
+
{destructive ? 'border-destructive bg-destructive/10' : ''}
|
|
147
|
+
"
|
|
148
|
+
bind:value
|
|
149
|
+
/>
|
|
165
150
|
{:else if field.type === "text"}
|
|
166
151
|
<Textarea
|
|
167
152
|
placeholder={ui?.placeholder ? ui.placeholder : value === "" ? "EMPTY STRING" : "NULL"}
|
|
@@ -6,30 +6,27 @@
|
|
|
6
6
|
import Button from "./ui/button/button.svelte";
|
|
7
7
|
import { onMount } from "svelte";
|
|
8
8
|
import { getStudioContext } from "../context";
|
|
9
|
+
import { toast } from "svelte-sonner";
|
|
10
|
+
import Skeleton from "./ui/skeleton/skeleton.svelte";
|
|
9
11
|
|
|
10
12
|
const { lobb, ctx } = getStudioContext();
|
|
11
|
-
import { toast } from "svelte-sonner";
|
|
12
|
-
import Skeleton from "./ui/skeleton/skeleton.svelte";
|
|
13
13
|
|
|
14
14
|
interface Props {
|
|
15
15
|
collectionName: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
let {
|
|
19
|
-
collectionName,
|
|
20
|
-
}: Props = $props();
|
|
18
|
+
let { collectionName }: Props = $props();
|
|
21
19
|
|
|
22
20
|
let entry = $state({});
|
|
23
21
|
let loading = $state(true);
|
|
24
|
-
let singletonExists = $state(true);
|
|
25
22
|
const formFields = getCollectionFields(ctx, collectionName);
|
|
26
23
|
|
|
27
24
|
onMount(async () => {
|
|
28
|
-
const result = await lobb.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
const result = await fetch(`${lobb.lobbUrl}/api/collections/${collectionName}/singleton`, {
|
|
26
|
+
headers: lobb.getHeaders() as HeadersInit,
|
|
27
|
+
});
|
|
28
|
+
if (result.status !== 404) {
|
|
29
|
+
const json = await result.json();
|
|
33
30
|
entry = json.data;
|
|
34
31
|
}
|
|
35
32
|
loading = false;
|
|
@@ -43,29 +40,27 @@
|
|
|
43
40
|
}
|
|
44
41
|
</script>
|
|
45
42
|
|
|
46
|
-
<div>
|
|
47
|
-
<div class="flex justify-between items-center gap-2 p-2 border-b
|
|
43
|
+
<div class="flex flex-col h-full bg-background">
|
|
44
|
+
<div class="flex justify-between items-center gap-2 p-2 border-b h-10 shrink-0">
|
|
48
45
|
<div class="flex items-center gap-1">
|
|
49
46
|
<SidebarTrigger />
|
|
50
47
|
</div>
|
|
51
48
|
<div>
|
|
52
|
-
<Button
|
|
53
|
-
class="h-7 px-2 font-normal text-xs"
|
|
54
|
-
Icon={Save}
|
|
55
|
-
onclick={handleSave}
|
|
56
|
-
>
|
|
49
|
+
<Button class="h-7 px-2 font-normal text-xs" Icon={Save} onclick={handleSave}>
|
|
57
50
|
Save
|
|
58
51
|
</Button>
|
|
59
52
|
</div>
|
|
60
53
|
</div>
|
|
61
54
|
|
|
62
55
|
{#if loading}
|
|
63
|
-
<div class="flex flex-col gap-2 p-
|
|
56
|
+
<div class="flex flex-col gap-2 p-4 w-full">
|
|
64
57
|
<Skeleton class="h-8 w-full" />
|
|
65
58
|
<Skeleton class="h-8 w-[80%]" />
|
|
66
59
|
<Skeleton class="h-8 w-[60%]" />
|
|
67
60
|
</div>
|
|
68
61
|
{:else}
|
|
69
|
-
<
|
|
62
|
+
<div class="flex-1 overflow-y-auto max-w-xl">
|
|
63
|
+
<DetailViewForm bind:value={entry} fields={formFields} />
|
|
64
|
+
</div>
|
|
70
65
|
{/if}
|
|
71
66
|
</div>
|
|
@@ -93,7 +93,8 @@ export type ExtensionComponentKey =
|
|
|
93
93
|
| `detailView.fields.topRight.${string}.${string}`
|
|
94
94
|
| `detailView.fields.foreignKey.${string}`
|
|
95
95
|
| `listView.entry.children.${string}`
|
|
96
|
-
| `listView.entry.actions
|
|
96
|
+
| `listView.entry.actions`
|
|
97
|
+
| `listView.header.actions`;
|
|
97
98
|
|
|
98
99
|
export type ExtensionComponent =
|
|
99
100
|
| any
|