@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.
@@ -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
- {#if loading}
167
- <div class="flex flex-col gap-2 p-2 w-full">
168
- <Skeleton class="h-8 w-full" />
169
- <Skeleton class="h-8 w-[80%]" />
170
- <Skeleton class="h-8 w-[60%]" />
171
- </div>
172
- {:else}
173
- <Table
174
- {data}
175
- {columns}
176
- showCollapsible={doesCollectionHasChildren}
177
- selectByColumn="id"
178
- showLastRowBorder={true}
179
- showLastColumnBorder={true}
180
- bind:sort={params.sort}
181
- bind:selectedRecords
182
- {unifiedBgColor}
183
- bind:tableWidth={dataTableWidth}
184
- {...tableProps}
185
- rowActions={hasRowActions ? rowActionsSnippet : undefined}>
186
- {#snippet tools(entry)}
187
- <UpdateDetailViewButton
188
- {collectionName}
189
- recordId={entry.id}
190
- variant="ghost"
191
- class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
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={icons[
214
- workflowTool.icon as keyof typeof icons
215
- ]}
216
- onclick={workflowTool.onclick}
217
- ></Button>
218
- {/each}
219
- {/await}
220
- {/snippet}
221
- {#snippet overrideCell(value, column, entry)}
222
- <FieldCell
223
- {collectionName}
224
- fieldName={column.id}
225
- {value}
226
- {entry}
227
- tableParams={params}
228
- />
229
- {/snippet}
230
- {#snippet collapsible(entry)}
231
- <ChildRecords
232
- {collectionName}
233
- recordId={entry.id}
234
- width={dataTableWidth > dataTableContainerWidth
235
- ? dataTableContainerWidth
236
- : dataTableWidth}
237
- unifiedBgColor={unifiedBgColor ?? "bg-background"}
238
- />
239
- {/snippet}
240
- </Table>
241
- {/if}
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 levelClasses: Record<string, string> = {
12
- success: "bg-green-100 text-green-700 border-green-200",
13
- warning: "bg-yellow-100 text-yellow-700 border-yellow-300",
14
- danger: "bg-red-100 text-red-700 border-red-200",
15
- info: "bg-blue-100 text-blue-700 border-blue-200",
16
- neutral: "bg-gray-100 text-gray-600 border-gray-200",
17
- muted: "bg-gray-50 text-gray-400 border-gray-100",
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 === "string" ? { value: e, level: "neutral" as const } : 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="px-2 py-0.5 rounded-full text-xs font-medium border {levelClasses[enumOption.level]}">
31
- {value}
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.type === "string" && field.enum}
97
- {@const rawEnum = field.enum as (string | EnumOption)[]}
98
- {@const enumOptions = rawEnum.map((e): EnumOption => typeof e === "string" ? { value: e, level: "neutral" } : e)}
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
- onValueChange={(newValue) => {
102
- value = newValue;
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 enumOptions as option}
121
- <Select.Item value={option.value} label={option.value}>
122
- <EnumBadge value={option.value} enum={enumOptions} />
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
- <!-- if the string has a validator of type enum -->
130
- {#if field.validators && field.validators.enum}
131
- <Select.Root
132
- type="single"
133
- onValueChange={(newValue) => {
134
- value = newValue;
135
- }}
136
- >
137
- <Select.Trigger
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.readSingleton(collectionName);
29
- const json = await result.json();
30
- if (result.status === 404) {
31
- singletonExists = false;
32
- } else {
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 bg-background h-10">
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-2 w-full">
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
- <DetailViewForm bind:value={entry} fields={formFields} />
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.13.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.19.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
- {#if loading}
167
- <div class="flex flex-col gap-2 p-2 w-full">
168
- <Skeleton class="h-8 w-full" />
169
- <Skeleton class="h-8 w-[80%]" />
170
- <Skeleton class="h-8 w-[60%]" />
171
- </div>
172
- {:else}
173
- <Table
174
- {data}
175
- {columns}
176
- showCollapsible={doesCollectionHasChildren}
177
- selectByColumn="id"
178
- showLastRowBorder={true}
179
- showLastColumnBorder={true}
180
- bind:sort={params.sort}
181
- bind:selectedRecords
182
- {unifiedBgColor}
183
- bind:tableWidth={dataTableWidth}
184
- {...tableProps}
185
- rowActions={hasRowActions ? rowActionsSnippet : undefined}>
186
- {#snippet tools(entry)}
187
- <UpdateDetailViewButton
188
- {collectionName}
189
- recordId={entry.id}
190
- variant="ghost"
191
- class="h-5 w-5 px-0 py-0 text-muted-foreground hover:bg-transparent"
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={icons[
214
- workflowTool.icon as keyof typeof icons
215
- ]}
216
- onclick={workflowTool.onclick}
217
- ></Button>
218
- {/each}
219
- {/await}
220
- {/snippet}
221
- {#snippet overrideCell(value, column, entry)}
222
- <FieldCell
223
- {collectionName}
224
- fieldName={column.id}
225
- {value}
226
- {entry}
227
- tableParams={params}
228
- />
229
- {/snippet}
230
- {#snippet collapsible(entry)}
231
- <ChildRecords
232
- {collectionName}
233
- recordId={entry.id}
234
- width={dataTableWidth > dataTableContainerWidth
235
- ? dataTableContainerWidth
236
- : dataTableWidth}
237
- unifiedBgColor={unifiedBgColor ?? "bg-background"}
238
- />
239
- {/snippet}
240
- </Table>
241
- {/if}
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 levelClasses: Record<string, string> = {
12
- success: "bg-green-100 text-green-700 border-green-200",
13
- warning: "bg-yellow-100 text-yellow-700 border-yellow-300",
14
- danger: "bg-red-100 text-red-700 border-red-200",
15
- info: "bg-blue-100 text-blue-700 border-blue-200",
16
- neutral: "bg-gray-100 text-gray-600 border-gray-200",
17
- muted: "bg-gray-50 text-gray-400 border-gray-100",
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 === "string" ? { value: e, level: "neutral" as const } : 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="px-2 py-0.5 rounded-full text-xs font-medium border {levelClasses[enumOption.level]}">
31
- {value}
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.type === "string" && field.enum}
97
- {@const rawEnum = field.enum as (string | EnumOption)[]}
98
- {@const enumOptions = rawEnum.map((e): EnumOption => typeof e === "string" ? { value: e, level: "neutral" } : e)}
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
- onValueChange={(newValue) => {
102
- value = newValue;
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 enumOptions as option}
121
- <Select.Item value={option.value} label={option.value}>
122
- <EnumBadge value={option.value} enum={enumOptions} />
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
- <!-- if the string has a validator of type enum -->
130
- {#if field.validators && field.validators.enum}
131
- <Select.Root
132
- type="single"
133
- onValueChange={(newValue) => {
134
- value = newValue;
135
- }}
136
- >
137
- <Select.Trigger
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.readSingleton(collectionName);
29
- const json = await result.json();
30
- if (result.status === 404) {
31
- singletonExists = false;
32
- } else {
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 bg-background h-10">
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-2 w-full">
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
- <DetailViewForm bind:value={entry} fields={formFields} />
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