@lobb-js/studio 0.35.0 → 0.37.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/canAccess.svelte +2 -12
- package/dist/components/dataTable/dataTable.svelte +4 -1
- package/dist/components/dataTable/header.svelte +10 -3
- package/dist/components/dataTable/header.svelte.d.ts +1 -0
- package/dist/components/detailView/create/createDetailViewChildren.svelte +2 -2
- package/dist/components/importButton.svelte +18 -2
- package/dist/components/mainNavShared.js +2 -6
- package/dist/components/routes/collections/collections.svelte +5 -10
- package/dist/components/ui/accordion/index.d.ts +1 -1
- package/dist/components/ui/command/command-dialog.svelte.d.ts +1 -1
- package/dist/components/ui/command/command-input.svelte.d.ts +1 -1
- package/dist/components/ui/command/command.svelte.d.ts +1 -1
- package/dist/components/ui/input/input.svelte.d.ts +1 -1
- package/dist/components/ui/range-calendar/range-calendar.svelte.d.ts +1 -1
- package/dist/components/ui/textarea/textarea.svelte.d.ts +1 -1
- package/dist/eventSystem.d.ts +1 -0
- package/dist/eventSystem.js +9 -0
- package/dist/extensions/extension.types.d.ts +6 -0
- package/dist/extensions/extensionUtils.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/store.types.d.ts +3 -9
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +8 -0
- package/package.json +10 -6
- package/src/lib/components/canAccess.svelte +2 -12
- package/src/lib/components/dataTable/dataTable.svelte +4 -1
- package/src/lib/components/dataTable/header.svelte +10 -3
- package/src/lib/components/detailView/create/createDetailViewChildren.svelte +2 -2
- package/src/lib/components/importButton.svelte +18 -2
- package/src/lib/components/mainNavShared.ts +2 -6
- package/src/lib/components/routes/collections/collections.svelte +5 -10
- package/src/lib/eventSystem.ts +9 -0
- package/src/lib/extensions/extension.types.ts +6 -0
- package/src/lib/extensions/extensionUtils.ts +1 -0
- package/src/lib/index.ts +2 -1
- package/src/lib/store.types.ts +6 -6
- package/src/lib/utils.ts +10 -0
- package/src/routes/+layout.svelte +7 -0
- package/src/routes/+layout.ts +1 -0
- package/src/routes/[...path]/+page.svelte +5 -0
- package/src/routes/+page.svelte +0 -6
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import type { Snippet } from "svelte";
|
|
15
15
|
import { onMount } from "svelte";
|
|
16
16
|
import { getStudioContext } from "../context";
|
|
17
|
-
import {
|
|
17
|
+
import { checkAuthAccess } from "../eventSystem";
|
|
18
18
|
|
|
19
19
|
interface Props {
|
|
20
20
|
collection: string;
|
|
@@ -31,17 +31,7 @@
|
|
|
31
31
|
let allowed = $state<boolean | null>(null);
|
|
32
32
|
|
|
33
33
|
onMount(async () => {
|
|
34
|
-
|
|
35
|
-
const result = await emitEvent({ lobb, ctx }, "auth.canAccess", {
|
|
36
|
-
collection,
|
|
37
|
-
action,
|
|
38
|
-
});
|
|
39
|
-
allowed = result === true;
|
|
40
|
-
} catch {
|
|
41
|
-
// No handler registered (auth extension not loaded), or the
|
|
42
|
-
// handler threw — fail closed.
|
|
43
|
-
allowed = false;
|
|
44
|
-
}
|
|
34
|
+
allowed = await checkAuthAccess({ lobb, ctx }, collection, action);
|
|
45
35
|
});
|
|
46
36
|
</script>
|
|
47
37
|
|
|
@@ -165,6 +165,7 @@
|
|
|
165
165
|
let totalCount = $state(0);
|
|
166
166
|
let serverData: TableProps["data"] = $state([]);
|
|
167
167
|
let loading = $state(true);
|
|
168
|
+
let hasLoaded = $state(false);
|
|
168
169
|
const columns: TableProps["columns"] = $state(
|
|
169
170
|
getCollectionColumns(ctx, collectionName),
|
|
170
171
|
);
|
|
@@ -197,6 +198,7 @@
|
|
|
197
198
|
totalCount = res.meta.totalCount;
|
|
198
199
|
onDataLoad?.(totalCount);
|
|
199
200
|
loading = false;
|
|
201
|
+
hasLoaded = true;
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
async function handleDelete(entryId: string) {
|
|
@@ -297,6 +299,7 @@
|
|
|
297
299
|
{collectionName}
|
|
298
300
|
bind:selectedRecords
|
|
299
301
|
{showImport}
|
|
302
|
+
{loading}
|
|
300
303
|
{parentContext}
|
|
301
304
|
onLink={isRecordingMode ? handleLink : undefined}
|
|
302
305
|
onCreate={isRecordingMode ? handleCreate : undefined}
|
|
@@ -310,7 +313,7 @@
|
|
|
310
313
|
<div class="relative flex-1 overflow-auto w-full">
|
|
311
314
|
{#key activeTabFilter}
|
|
312
315
|
<div class="h-full w-full" in:fade={{ duration: 120 }}>
|
|
313
|
-
{#if loading}
|
|
316
|
+
{#if loading && !hasLoaded}
|
|
314
317
|
<div class="flex flex-col gap-2 p-2 w-full">
|
|
315
318
|
<Skeleton class="h-8 w-full" />
|
|
316
319
|
<Skeleton class="h-8 w-[80%]" />
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { Changes } from "../detailView/utils";
|
|
4
4
|
import type { ParentContext } from "./dataTable.svelte";
|
|
5
5
|
import CanAccess from "../canAccess.svelte";
|
|
6
|
-
import { Download, ListRestart, Plus, Trash, Link } from "lucide-svelte";
|
|
6
|
+
import { Download, ListRestart, LoaderCircle, Plus, Trash, Link } from "lucide-svelte";
|
|
7
7
|
import LlmButton from "../LlmButton.svelte";
|
|
8
8
|
import FilterButton from "./filterButton.svelte";
|
|
9
9
|
import SortButton from "./sortButton.svelte";
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
onLink?: (record: any) => void;
|
|
27
27
|
onCreate?: (changes: Changes) => void;
|
|
28
28
|
showImport?: boolean;
|
|
29
|
+
loading?: boolean;
|
|
29
30
|
left?: Snippet<[]>;
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
onLink,
|
|
38
39
|
onCreate,
|
|
39
40
|
showImport = true,
|
|
41
|
+
loading = false,
|
|
40
42
|
left
|
|
41
43
|
}: Props = $props();
|
|
42
44
|
|
|
@@ -162,10 +164,15 @@
|
|
|
162
164
|
variant="ghost"
|
|
163
165
|
size="sm"
|
|
164
166
|
class="text-muted-foreground"
|
|
165
|
-
Icon={ListRestart}
|
|
166
167
|
onclick={() => (params = { ...params })}
|
|
167
168
|
>
|
|
168
|
-
{
|
|
169
|
+
{#if loading}
|
|
170
|
+
<LoaderCircle size={16} class="animate-spin" />
|
|
171
|
+
{headerIsSmall ? "" : "Loading"}
|
|
172
|
+
{:else}
|
|
173
|
+
<ListRestart size={16} />
|
|
174
|
+
{headerIsSmall ? "" : "Refresh"}
|
|
175
|
+
{/if}
|
|
169
176
|
</Button>
|
|
170
177
|
{#if showImport}
|
|
171
178
|
<CanAccess collection={collectionName} action="create">
|
|
@@ -95,8 +95,8 @@
|
|
|
95
95
|
showImport={false}
|
|
96
96
|
showHeader={true}
|
|
97
97
|
showFooter={false}
|
|
98
|
-
showDelete={
|
|
99
|
-
showEdit={
|
|
98
|
+
showDelete={true}
|
|
99
|
+
showEdit={true}
|
|
100
100
|
tableProps={{ showLastColumnBorder: false, showLastRowBorder: true }}
|
|
101
101
|
>
|
|
102
102
|
{#snippet headerLeft()}
|
|
@@ -86,7 +86,23 @@
|
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
function detectAndParse(content: string): any[] {
|
|
89
|
+
async function detectAndParse(content: string): Promise<any[]> {
|
|
90
|
+
// Give projects a chance to take over parsing for non-standard
|
|
91
|
+
// formats (preamble rows, multi-line cells, vendor exports, …) by
|
|
92
|
+
// registering a workflow on `studio.collections.import.parse`. The
|
|
93
|
+
// workflow receives the raw text and sets `handled: true` along
|
|
94
|
+
// with its own `rows`; if no workflow takes over, we fall through
|
|
95
|
+
// to the built-in JSON / simple-CSV detector.
|
|
96
|
+
const result = await emitEvent({ lobb, ctx }, "studio.collections.import.parse", {
|
|
97
|
+
collectionName,
|
|
98
|
+
content,
|
|
99
|
+
rows: null as any[] | null,
|
|
100
|
+
handled: false,
|
|
101
|
+
});
|
|
102
|
+
if (result?.handled && Array.isArray(result.rows)) {
|
|
103
|
+
return result.rows;
|
|
104
|
+
}
|
|
105
|
+
|
|
90
106
|
const trimmed = content.trim();
|
|
91
107
|
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
92
108
|
const parsed = JSON.parse(trimmed);
|
|
@@ -98,7 +114,7 @@
|
|
|
98
114
|
async function processContent(content: string) {
|
|
99
115
|
parseError = "";
|
|
100
116
|
try {
|
|
101
|
-
const rows = detectAndParse(content);
|
|
117
|
+
const rows = await detectAndParse(content);
|
|
102
118
|
if (rows.length === 0) throw new Error("No data rows found");
|
|
103
119
|
transformedRows = applyColumnMapping(rows);
|
|
104
120
|
step = "preview";
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { House, Layers, Library, Workflow } from "lucide-svelte";
|
|
2
|
-
import {
|
|
2
|
+
import { checkAuthAccess } from "../eventSystem";
|
|
3
3
|
import { getDashboardNavs } from "../extensions/extensionUtils";
|
|
4
4
|
async function isItemVisible(lobb, ctx, item) {
|
|
5
5
|
if (!item.represents)
|
|
6
6
|
return true;
|
|
7
|
-
|
|
8
|
-
collection: item.represents,
|
|
9
|
-
action: "read",
|
|
10
|
-
});
|
|
11
|
-
return res === true;
|
|
7
|
+
return checkAuthAccess({ lobb, ctx }, item.represents, "read");
|
|
12
8
|
}
|
|
13
9
|
export async function buildNavSections(lobb, ctx) {
|
|
14
10
|
const rawSections = [
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { SideBarData, SideBarNode } from "../../sidebar/sidebarElements.svelte";
|
|
3
3
|
import Sidebar from "../../sidebar/sidebar.svelte";
|
|
4
4
|
import { getStudioContext } from "../../../context";
|
|
5
|
-
import {
|
|
5
|
+
import { checkAuthAccess } from "../../../eventSystem";
|
|
6
6
|
import Collection from "./collection.svelte";
|
|
7
7
|
import { onMount } from "svelte";
|
|
8
8
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
let { collectionName } = $props();
|
|
19
19
|
|
|
20
20
|
// Start empty so unreadable collections never flash. Populated after the
|
|
21
|
-
//
|
|
21
|
+
// Populated after checkAuthAccess calls resolve below.
|
|
22
22
|
let collectionsList = $state<SideBarData>([]);
|
|
23
23
|
|
|
24
24
|
onMount(async () => {
|
|
@@ -31,14 +31,9 @@
|
|
|
31
31
|
);
|
|
32
32
|
const visibleNames = (
|
|
33
33
|
await Promise.all(
|
|
34
|
-
allNames.map(async (name) =>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"auth.canAccess",
|
|
38
|
-
{ collection: name, action: "read" },
|
|
39
|
-
);
|
|
40
|
-
return res === true ? name : null;
|
|
41
|
-
}),
|
|
34
|
+
allNames.map(async (name) =>
|
|
35
|
+
(await checkAuthAccess({ lobb, ctx }, name, "read")) ? name : null
|
|
36
|
+
),
|
|
42
37
|
)
|
|
43
38
|
).filter((n): n is string => n !== null);
|
|
44
39
|
|
|
@@ -2,5 +2,5 @@ import { Accordion as AccordionPrimitive } from "bits-ui";
|
|
|
2
2
|
import Content from "./accordion-content.svelte";
|
|
3
3
|
import Item from "./accordion-item.svelte";
|
|
4
4
|
import Trigger from "./accordion-trigger.svelte";
|
|
5
|
-
declare const Root: import("svelte").Component<AccordionPrimitive.RootProps, {}, "
|
|
5
|
+
declare const Root: import("svelte").Component<AccordionPrimitive.RootProps, {}, "value" | "ref">;
|
|
6
6
|
export { Root, Content, Item, Trigger, Root as Accordion, Content as AccordionContent, Item as AccordionItem, Trigger as AccordionTrigger, };
|
|
@@ -4,6 +4,6 @@ type $$ComponentProps = WithoutChildrenOrChild<DialogPrimitive.RootProps> & With
|
|
|
4
4
|
portalProps?: DialogPrimitive.PortalProps;
|
|
5
5
|
children: Snippet;
|
|
6
6
|
};
|
|
7
|
-
declare const CommandDialog: import("svelte").Component<$$ComponentProps, {}, "
|
|
7
|
+
declare const CommandDialog: import("svelte").Component<$$ComponentProps, {}, "value" | "ref" | "open">;
|
|
8
8
|
type CommandDialog = ReturnType<typeof CommandDialog>;
|
|
9
9
|
export default CommandDialog;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { Command as CommandPrimitive } from "bits-ui";
|
|
2
|
-
declare const CommandInput: import("svelte").Component<CommandPrimitive.InputProps, {}, "
|
|
2
|
+
declare const CommandInput: import("svelte").Component<CommandPrimitive.InputProps, {}, "value" | "ref">;
|
|
3
3
|
type CommandInput = ReturnType<typeof CommandInput>;
|
|
4
4
|
export default CommandInput;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { Command as CommandPrimitive } from "bits-ui";
|
|
2
|
-
declare const Command: import("svelte").Component<CommandPrimitive.RootProps, {}, "
|
|
2
|
+
declare const Command: import("svelte").Component<CommandPrimitive.RootProps, {}, "value" | "ref">;
|
|
3
3
|
type Command = ReturnType<typeof Command>;
|
|
4
4
|
export default Command;
|
|
@@ -8,6 +8,6 @@ type Props = WithElementRef<Omit<HTMLInputAttributes, "type"> & ({
|
|
|
8
8
|
type?: InputType;
|
|
9
9
|
files?: undefined;
|
|
10
10
|
})>;
|
|
11
|
-
declare const Input: import("svelte").Component<Props, {}, "
|
|
11
|
+
declare const Input: import("svelte").Component<Props, {}, "value" | "ref" | "files">;
|
|
12
12
|
type Input = ReturnType<typeof Input>;
|
|
13
13
|
export default Input;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RangeCalendar as RangeCalendarPrimitive } from "bits-ui";
|
|
2
2
|
import * as RangeCalendar from "./index.js";
|
|
3
|
-
declare const RangeCalendar: import("svelte").Component<Omit<Omit<RangeCalendarPrimitive.RootProps, "child">, "children">, {}, "
|
|
3
|
+
declare const RangeCalendar: import("svelte").Component<Omit<Omit<RangeCalendarPrimitive.RootProps, "child">, "children">, {}, "value" | "ref" | "placeholder">;
|
|
4
4
|
type RangeCalendar = ReturnType<typeof RangeCalendar>;
|
|
5
5
|
export default RangeCalendar;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { WithElementRef } from "bits-ui";
|
|
2
2
|
import type { HTMLTextareaAttributes } from "svelte/elements";
|
|
3
|
-
declare const Textarea: import("svelte").Component<Omit<WithElementRef<HTMLTextareaAttributes>, "children">, {}, "
|
|
3
|
+
declare const Textarea: import("svelte").Component<Omit<WithElementRef<HTMLTextareaAttributes>, "children">, {}, "value" | "ref">;
|
|
4
4
|
type Textarea = ReturnType<typeof Textarea>;
|
|
5
5
|
export default Textarea;
|
package/dist/eventSystem.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import type { StudioContext } from "./context";
|
|
2
|
+
export declare function checkAuthAccess(studioContext: StudioContext, collection: string, action: string): Promise<boolean>;
|
|
2
3
|
export declare function emitEvent(studioContext: StudioContext, eventName: string, input: any): Promise<any>;
|
package/dist/eventSystem.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { toast } from "svelte-sonner";
|
|
2
2
|
import { openCreateDetailView, openUpdateDetailView } from "./actions";
|
|
3
|
+
export async function checkAuthAccess(studioContext, collection, action) {
|
|
4
|
+
try {
|
|
5
|
+
const result = await emitEvent(studioContext, "auth.canAccess", { collection, action });
|
|
6
|
+
return result !== false;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
3
12
|
export async function emitEvent(studioContext, eventName, input) {
|
|
4
13
|
const { ctx } = studioContext;
|
|
5
14
|
const workflows = ctx.meta.studio_workflows.filter((workflow) => {
|
|
@@ -69,6 +69,12 @@ export interface ExtensionUtils {
|
|
|
69
69
|
openDataTableDrawer: (props: OpenDataTableDrawerProps) => void;
|
|
70
70
|
openDataTablePopup: (props: OpenDataTablePopupProps) => void;
|
|
71
71
|
emitEvent: (eventName: string, input: any) => Promise<any>;
|
|
72
|
+
/**
|
|
73
|
+
* Shorthand for the standalone `isHidden(ctx, id)` helper — returns
|
|
74
|
+
* true when the project has opted to hide the UI element registered
|
|
75
|
+
* under the given identifier in its `ui.hide` config.
|
|
76
|
+
*/
|
|
77
|
+
isHidden: (id: string) => boolean;
|
|
72
78
|
components: Components;
|
|
73
79
|
mediaQueries: typeof mediaQueries;
|
|
74
80
|
intlDate: typeof intlDate;
|
|
@@ -60,6 +60,7 @@ export function getExtensionUtils(lobb, ctx) {
|
|
|
60
60
|
openDataTableDrawer: (props) => openDataTableDrawer({ lobb, ctx }, props),
|
|
61
61
|
openDataTablePopup: (props) => openDataTablePopup({ lobb, ctx }, props),
|
|
62
62
|
emitEvent: (eventName, input) => emitEvent({ lobb, ctx }, eventName, input),
|
|
63
|
+
isHidden: (id) => ctx.meta?.ui?.hide?.[id] === true,
|
|
63
64
|
components: getComponents(),
|
|
64
65
|
mediaQueries: mediaQueries,
|
|
65
66
|
intlDate: intlDate,
|
package/dist/index.d.ts
CHANGED
|
@@ -26,3 +26,4 @@ export { default as DataTable } from "./components/dataTable/dataTable.svelte";
|
|
|
26
26
|
export { default as Drawer } from "./components/drawer.svelte";
|
|
27
27
|
export { default as SelectRecord } from "./components/selectRecord.svelte";
|
|
28
28
|
export { Switch } from "./components/ui/switch";
|
|
29
|
+
export { isHidden } from "./utils";
|
package/dist/index.js
CHANGED
|
@@ -25,3 +25,4 @@ export { default as DataTable } from "./components/dataTable/dataTable.svelte";
|
|
|
25
25
|
export { default as Drawer } from "./components/drawer.svelte";
|
|
26
26
|
export { default as SelectRecord } from "./components/selectRecord.svelte";
|
|
27
27
|
export { Switch } from "./components/ui/switch";
|
|
28
|
+
export { isHidden } from "./utils";
|
package/dist/store.types.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Extension } from "./extensions/extension.types";
|
|
2
|
+
import type { UIConfig, UITheme } from "@lobb-js/core";
|
|
3
|
+
export type { UITheme };
|
|
2
4
|
export interface CollectionTab {
|
|
3
5
|
id?: string;
|
|
4
6
|
label: string;
|
|
@@ -30,16 +32,9 @@ interface Collection {
|
|
|
30
32
|
};
|
|
31
33
|
}
|
|
32
34
|
type Collections = Record<string, Collection>;
|
|
33
|
-
export interface UITheme {
|
|
34
|
-
light?: Record<string, string>;
|
|
35
|
-
dark?: Record<string, string>;
|
|
36
|
-
}
|
|
37
35
|
interface Meta {
|
|
38
36
|
version: string;
|
|
39
|
-
ui?:
|
|
40
|
-
theme?: UITheme;
|
|
41
|
-
horizontalNav?: boolean;
|
|
42
|
-
};
|
|
37
|
+
ui?: UIConfig;
|
|
43
38
|
relations: Array<any>;
|
|
44
39
|
collections: Collections;
|
|
45
40
|
extensions: Record<string, any>;
|
|
@@ -54,4 +49,3 @@ export interface CTX {
|
|
|
54
49
|
meta: Meta;
|
|
55
50
|
currentUrl: URL;
|
|
56
51
|
}
|
|
57
|
-
export {};
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ClassValue } from "clsx";
|
|
2
2
|
import { MediaQuery } from 'svelte/reactivity';
|
|
3
|
+
import type { CTX } from "./store.types";
|
|
3
4
|
export declare function cn(...inputs: ClassValue[]): string;
|
|
4
5
|
export type WithoutChild<T> = T extends {
|
|
5
6
|
child?: any;
|
|
@@ -19,6 +20,7 @@ export declare const mediaQueries: {
|
|
|
19
20
|
'2xl': MediaQuery;
|
|
20
21
|
};
|
|
21
22
|
export declare function getFieldTypeLabel(type: string | undefined | null): string;
|
|
23
|
+
export declare function isHidden(ctx: CTX | null | undefined, id: string): boolean;
|
|
22
24
|
export declare function calculateDrawerWidth(): number;
|
|
23
25
|
export declare function getChangedProperties(oldObj: Record<string, any>, newObj: Record<string, any>): Record<string, any>;
|
|
24
26
|
export declare function parseFunction(functionString: string): any;
|
package/dist/utils.js
CHANGED
|
@@ -24,6 +24,14 @@ export function getFieldTypeLabel(type) {
|
|
|
24
24
|
return "";
|
|
25
25
|
return FIELD_TYPE_LABELS[type] ?? type;
|
|
26
26
|
}
|
|
27
|
+
// Settings-based visibility check. Returns true when the project has
|
|
28
|
+
// opted to hide the UI element registered under the given identifier
|
|
29
|
+
// in its `ui.hide` config (e.g. "reports.dashboardShareButton").
|
|
30
|
+
// Components that support hiding wrap their render with
|
|
31
|
+
// `{#if !isHidden(ctx, "...")}`.
|
|
32
|
+
export function isHidden(ctx, id) {
|
|
33
|
+
return ctx?.meta?.ui?.hide?.[id] === true;
|
|
34
|
+
}
|
|
27
35
|
export function calculateDrawerWidth() {
|
|
28
36
|
const backgroundDrawerButtons = document.querySelectorAll(".backgroundDrawerButton");
|
|
29
37
|
const drawersCount = Array.from(backgroundDrawerButtons).length;
|
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.37.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
|
-
"dev": "
|
|
31
|
+
"dev": "bun run --watch lobb.ts",
|
|
32
|
+
"dev:studio": "vite dev",
|
|
32
33
|
"build": "vite build && bun run package",
|
|
33
34
|
"package": "svelte-kit sync && svelte-package --input src/lib",
|
|
34
35
|
"preview": "vite preview",
|
|
@@ -36,14 +37,17 @@
|
|
|
36
37
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
37
38
|
"storybook": "storybook dev -p 6006",
|
|
38
39
|
"build-storybook": "storybook build",
|
|
39
|
-
"test": "
|
|
40
|
+
"test": "playwright test",
|
|
41
|
+
"test:ui": "playwright test --ui",
|
|
42
|
+
"test:unit": "vitest",
|
|
40
43
|
"test-storybook": "vitest --project=storybook",
|
|
41
44
|
"prepublishOnly": "./scripts/prepublish.sh",
|
|
42
45
|
"postpublish": "./scripts/postpublish.sh"
|
|
43
46
|
},
|
|
44
47
|
"devDependencies": {
|
|
45
|
-
"@lobb-js/core": "^0.
|
|
48
|
+
"@lobb-js/core": "^0.37.0",
|
|
46
49
|
"@chromatic-com/storybook": "^4.1.2",
|
|
50
|
+
"@playwright/test": "^1.60.0",
|
|
47
51
|
"@storybook/addon-a11y": "^10.0.1",
|
|
48
52
|
"@storybook/addon-docs": "^10.0.1",
|
|
49
53
|
"@storybook/addon-svelte-csf": "^5.0.10",
|
|
@@ -53,6 +57,7 @@
|
|
|
53
57
|
"@sveltejs/kit": "^2.60.1",
|
|
54
58
|
"@sveltejs/package": "^2.5.7",
|
|
55
59
|
"@tsconfig/svelte": "^5.0.6",
|
|
60
|
+
"@types/lodash-es": "^4.17.12",
|
|
56
61
|
"@types/mustache": "^4.2.6",
|
|
57
62
|
"@types/node": "^24.10.1",
|
|
58
63
|
"@types/qs": "^6.9.18",
|
|
@@ -69,8 +74,7 @@
|
|
|
69
74
|
"tw-animate-css": "^1.4.0",
|
|
70
75
|
"typescript": "~5.9.3",
|
|
71
76
|
"vite": "^8.0.13",
|
|
72
|
-
"vitest": "^4.0.5"
|
|
73
|
-
"@types/lodash-es": "^4.17.12"
|
|
77
|
+
"vitest": "^4.0.5"
|
|
74
78
|
},
|
|
75
79
|
"peerDependencies": {
|
|
76
80
|
"svelte": "^5.0.0",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import type { Snippet } from "svelte";
|
|
15
15
|
import { onMount } from "svelte";
|
|
16
16
|
import { getStudioContext } from "../context";
|
|
17
|
-
import {
|
|
17
|
+
import { checkAuthAccess } from "../eventSystem";
|
|
18
18
|
|
|
19
19
|
interface Props {
|
|
20
20
|
collection: string;
|
|
@@ -31,17 +31,7 @@
|
|
|
31
31
|
let allowed = $state<boolean | null>(null);
|
|
32
32
|
|
|
33
33
|
onMount(async () => {
|
|
34
|
-
|
|
35
|
-
const result = await emitEvent({ lobb, ctx }, "auth.canAccess", {
|
|
36
|
-
collection,
|
|
37
|
-
action,
|
|
38
|
-
});
|
|
39
|
-
allowed = result === true;
|
|
40
|
-
} catch {
|
|
41
|
-
// No handler registered (auth extension not loaded), or the
|
|
42
|
-
// handler threw — fail closed.
|
|
43
|
-
allowed = false;
|
|
44
|
-
}
|
|
34
|
+
allowed = await checkAuthAccess({ lobb, ctx }, collection, action);
|
|
45
35
|
});
|
|
46
36
|
</script>
|
|
47
37
|
|
|
@@ -165,6 +165,7 @@
|
|
|
165
165
|
let totalCount = $state(0);
|
|
166
166
|
let serverData: TableProps["data"] = $state([]);
|
|
167
167
|
let loading = $state(true);
|
|
168
|
+
let hasLoaded = $state(false);
|
|
168
169
|
const columns: TableProps["columns"] = $state(
|
|
169
170
|
getCollectionColumns(ctx, collectionName),
|
|
170
171
|
);
|
|
@@ -197,6 +198,7 @@
|
|
|
197
198
|
totalCount = res.meta.totalCount;
|
|
198
199
|
onDataLoad?.(totalCount);
|
|
199
200
|
loading = false;
|
|
201
|
+
hasLoaded = true;
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
async function handleDelete(entryId: string) {
|
|
@@ -297,6 +299,7 @@
|
|
|
297
299
|
{collectionName}
|
|
298
300
|
bind:selectedRecords
|
|
299
301
|
{showImport}
|
|
302
|
+
{loading}
|
|
300
303
|
{parentContext}
|
|
301
304
|
onLink={isRecordingMode ? handleLink : undefined}
|
|
302
305
|
onCreate={isRecordingMode ? handleCreate : undefined}
|
|
@@ -310,7 +313,7 @@
|
|
|
310
313
|
<div class="relative flex-1 overflow-auto w-full">
|
|
311
314
|
{#key activeTabFilter}
|
|
312
315
|
<div class="h-full w-full" in:fade={{ duration: 120 }}>
|
|
313
|
-
{#if loading}
|
|
316
|
+
{#if loading && !hasLoaded}
|
|
314
317
|
<div class="flex flex-col gap-2 p-2 w-full">
|
|
315
318
|
<Skeleton class="h-8 w-full" />
|
|
316
319
|
<Skeleton class="h-8 w-[80%]" />
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { Changes } from "../detailView/utils";
|
|
4
4
|
import type { ParentContext } from "./dataTable.svelte";
|
|
5
5
|
import CanAccess from "../canAccess.svelte";
|
|
6
|
-
import { Download, ListRestart, Plus, Trash, Link } from "lucide-svelte";
|
|
6
|
+
import { Download, ListRestart, LoaderCircle, Plus, Trash, Link } from "lucide-svelte";
|
|
7
7
|
import LlmButton from "../LlmButton.svelte";
|
|
8
8
|
import FilterButton from "./filterButton.svelte";
|
|
9
9
|
import SortButton from "./sortButton.svelte";
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
onLink?: (record: any) => void;
|
|
27
27
|
onCreate?: (changes: Changes) => void;
|
|
28
28
|
showImport?: boolean;
|
|
29
|
+
loading?: boolean;
|
|
29
30
|
left?: Snippet<[]>;
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
onLink,
|
|
38
39
|
onCreate,
|
|
39
40
|
showImport = true,
|
|
41
|
+
loading = false,
|
|
40
42
|
left
|
|
41
43
|
}: Props = $props();
|
|
42
44
|
|
|
@@ -162,10 +164,15 @@
|
|
|
162
164
|
variant="ghost"
|
|
163
165
|
size="sm"
|
|
164
166
|
class="text-muted-foreground"
|
|
165
|
-
Icon={ListRestart}
|
|
166
167
|
onclick={() => (params = { ...params })}
|
|
167
168
|
>
|
|
168
|
-
{
|
|
169
|
+
{#if loading}
|
|
170
|
+
<LoaderCircle size={16} class="animate-spin" />
|
|
171
|
+
{headerIsSmall ? "" : "Loading"}
|
|
172
|
+
{:else}
|
|
173
|
+
<ListRestart size={16} />
|
|
174
|
+
{headerIsSmall ? "" : "Refresh"}
|
|
175
|
+
{/if}
|
|
169
176
|
</Button>
|
|
170
177
|
{#if showImport}
|
|
171
178
|
<CanAccess collection={collectionName} action="create">
|
|
@@ -95,8 +95,8 @@
|
|
|
95
95
|
showImport={false}
|
|
96
96
|
showHeader={true}
|
|
97
97
|
showFooter={false}
|
|
98
|
-
showDelete={
|
|
99
|
-
showEdit={
|
|
98
|
+
showDelete={true}
|
|
99
|
+
showEdit={true}
|
|
100
100
|
tableProps={{ showLastColumnBorder: false, showLastRowBorder: true }}
|
|
101
101
|
>
|
|
102
102
|
{#snippet headerLeft()}
|
|
@@ -86,7 +86,23 @@
|
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
function detectAndParse(content: string): any[] {
|
|
89
|
+
async function detectAndParse(content: string): Promise<any[]> {
|
|
90
|
+
// Give projects a chance to take over parsing for non-standard
|
|
91
|
+
// formats (preamble rows, multi-line cells, vendor exports, …) by
|
|
92
|
+
// registering a workflow on `studio.collections.import.parse`. The
|
|
93
|
+
// workflow receives the raw text and sets `handled: true` along
|
|
94
|
+
// with its own `rows`; if no workflow takes over, we fall through
|
|
95
|
+
// to the built-in JSON / simple-CSV detector.
|
|
96
|
+
const result = await emitEvent({ lobb, ctx }, "studio.collections.import.parse", {
|
|
97
|
+
collectionName,
|
|
98
|
+
content,
|
|
99
|
+
rows: null as any[] | null,
|
|
100
|
+
handled: false,
|
|
101
|
+
});
|
|
102
|
+
if (result?.handled && Array.isArray(result.rows)) {
|
|
103
|
+
return result.rows;
|
|
104
|
+
}
|
|
105
|
+
|
|
90
106
|
const trimmed = content.trim();
|
|
91
107
|
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
92
108
|
const parsed = JSON.parse(trimmed);
|
|
@@ -98,7 +114,7 @@
|
|
|
98
114
|
async function processContent(content: string) {
|
|
99
115
|
parseError = "";
|
|
100
116
|
try {
|
|
101
|
-
const rows = detectAndParse(content);
|
|
117
|
+
const rows = await detectAndParse(content);
|
|
102
118
|
if (rows.length === 0) throw new Error("No data rows found");
|
|
103
119
|
transformedRows = applyColumnMapping(rows);
|
|
104
120
|
step = "preview";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { House, Layers, Library, Workflow } from "lucide-svelte";
|
|
2
|
-
import {
|
|
2
|
+
import { checkAuthAccess } from "../eventSystem";
|
|
3
3
|
import { getDashboardNavs } from "../extensions/extensionUtils";
|
|
4
4
|
|
|
5
5
|
export type NavItem = {
|
|
@@ -13,11 +13,7 @@ export type NavItem = {
|
|
|
13
13
|
|
|
14
14
|
async function isItemVisible(lobb: any, ctx: any, item: NavItem): Promise<boolean> {
|
|
15
15
|
if (!item.represents) return true;
|
|
16
|
-
|
|
17
|
-
collection: item.represents,
|
|
18
|
-
action: "read",
|
|
19
|
-
});
|
|
20
|
-
return res === true;
|
|
16
|
+
return checkAuthAccess({ lobb, ctx }, item.represents, "read");
|
|
21
17
|
}
|
|
22
18
|
|
|
23
19
|
export async function buildNavSections(lobb: any, ctx: any): Promise<NavItem[][]> {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { SideBarData, SideBarNode } from "../../sidebar/sidebarElements.svelte";
|
|
3
3
|
import Sidebar from "../../sidebar/sidebar.svelte";
|
|
4
4
|
import { getStudioContext } from "../../../context";
|
|
5
|
-
import {
|
|
5
|
+
import { checkAuthAccess } from "../../../eventSystem";
|
|
6
6
|
import Collection from "./collection.svelte";
|
|
7
7
|
import { onMount } from "svelte";
|
|
8
8
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
let { collectionName } = $props();
|
|
19
19
|
|
|
20
20
|
// Start empty so unreadable collections never flash. Populated after the
|
|
21
|
-
//
|
|
21
|
+
// Populated after checkAuthAccess calls resolve below.
|
|
22
22
|
let collectionsList = $state<SideBarData>([]);
|
|
23
23
|
|
|
24
24
|
onMount(async () => {
|
|
@@ -31,14 +31,9 @@
|
|
|
31
31
|
);
|
|
32
32
|
const visibleNames = (
|
|
33
33
|
await Promise.all(
|
|
34
|
-
allNames.map(async (name) =>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"auth.canAccess",
|
|
38
|
-
{ collection: name, action: "read" },
|
|
39
|
-
);
|
|
40
|
-
return res === true ? name : null;
|
|
41
|
-
}),
|
|
34
|
+
allNames.map(async (name) =>
|
|
35
|
+
(await checkAuthAccess({ lobb, ctx }, name, "read")) ? name : null
|
|
36
|
+
),
|
|
42
37
|
)
|
|
43
38
|
).filter((n): n is string => n !== null);
|
|
44
39
|
|
package/src/lib/eventSystem.ts
CHANGED
|
@@ -3,6 +3,15 @@ import type { StudioContext } from "./context";
|
|
|
3
3
|
import { openCreateDetailView, openUpdateDetailView } from "./actions";
|
|
4
4
|
import type { UpdateDetailViewProp } from "./components/detailView/update/updateDetailView.svelte";
|
|
5
5
|
|
|
6
|
+
export async function checkAuthAccess(studioContext: StudioContext, collection: string, action: string): Promise<boolean> {
|
|
7
|
+
try {
|
|
8
|
+
const result = await emitEvent(studioContext, "auth.canAccess", { collection, action });
|
|
9
|
+
return result !== false;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
export async function emitEvent(studioContext: StudioContext, eventName: string, input: any): Promise<any> {
|
|
7
16
|
const { ctx } = studioContext;
|
|
8
17
|
const workflows = ctx.meta.studio_workflows.filter(
|
|
@@ -73,6 +73,12 @@ export interface ExtensionUtils {
|
|
|
73
73
|
openDataTableDrawer: (props: OpenDataTableDrawerProps) => void;
|
|
74
74
|
openDataTablePopup: (props: OpenDataTablePopupProps) => void;
|
|
75
75
|
emitEvent: (eventName: string, input: any) => Promise<any>;
|
|
76
|
+
/**
|
|
77
|
+
* Shorthand for the standalone `isHidden(ctx, id)` helper — returns
|
|
78
|
+
* true when the project has opted to hide the UI element registered
|
|
79
|
+
* under the given identifier in its `ui.hide` config.
|
|
80
|
+
*/
|
|
81
|
+
isHidden: (id: string) => boolean;
|
|
76
82
|
components: Components;
|
|
77
83
|
mediaQueries: typeof mediaQueries;
|
|
78
84
|
intlDate: typeof intlDate;
|
|
@@ -70,6 +70,7 @@ export function getExtensionUtils(lobb: LobbClient, ctx: CTX): ExtensionUtils {
|
|
|
70
70
|
openDataTableDrawer: (props) => openDataTableDrawer({ lobb, ctx }, props),
|
|
71
71
|
openDataTablePopup: (props) => openDataTablePopup({ lobb, ctx }, props),
|
|
72
72
|
emitEvent: (eventName, input) => emitEvent({ lobb, ctx }, eventName, input),
|
|
73
|
+
isHidden: (id) => ctx.meta?.ui?.hide?.[id] === true,
|
|
73
74
|
components: getComponents(),
|
|
74
75
|
mediaQueries: mediaQueries,
|
|
75
76
|
intlDate: intlDate,
|
package/src/lib/index.ts
CHANGED
|
@@ -26,4 +26,5 @@ export { default as RangeCalendarButton } from "./components/rangeCalendarButton
|
|
|
26
26
|
export { default as DataTable } from "./components/dataTable/dataTable.svelte";
|
|
27
27
|
export { default as Drawer } from "./components/drawer.svelte";
|
|
28
28
|
export { default as SelectRecord } from "./components/selectRecord.svelte";
|
|
29
|
-
export { Switch } from "./components/ui/switch";
|
|
29
|
+
export { Switch } from "./components/ui/switch";
|
|
30
|
+
export { isHidden } from "./utils";
|
package/src/lib/store.types.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { Extension } from "./extensions/extension.types";
|
|
2
|
+
import type { UIConfig, UITheme } from "@lobb-js/core";
|
|
3
|
+
|
|
4
|
+
// Re-export so existing imports of UITheme from the studio package
|
|
5
|
+
// keep working without touching every consumer.
|
|
6
|
+
export type { UITheme };
|
|
2
7
|
|
|
3
8
|
export interface CollectionTab {
|
|
4
9
|
id?: string;
|
|
@@ -27,14 +32,9 @@ interface Collection {
|
|
|
27
32
|
}
|
|
28
33
|
type Collections = Record<string, Collection>;
|
|
29
34
|
|
|
30
|
-
export interface UITheme {
|
|
31
|
-
light?: Record<string, string>;
|
|
32
|
-
dark?: Record<string, string>;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
35
|
interface Meta {
|
|
36
36
|
version: string;
|
|
37
|
-
ui?:
|
|
37
|
+
ui?: UIConfig;
|
|
38
38
|
relations: Array<any>;
|
|
39
39
|
collections: Collections;
|
|
40
40
|
extensions: Record<string, any>;
|
package/src/lib/utils.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { twMerge } from "tailwind-merge";
|
|
|
3
3
|
import { isEqual } from "lodash-es";
|
|
4
4
|
|
|
5
5
|
import { MediaQuery } from 'svelte/reactivity';
|
|
6
|
+
import type { CTX } from "./store.types";
|
|
6
7
|
|
|
7
8
|
export function cn(...inputs: ClassValue[]) {
|
|
8
9
|
return twMerge(clsx(inputs));
|
|
@@ -36,6 +37,15 @@ export function getFieldTypeLabel(type: string | undefined | null): string {
|
|
|
36
37
|
return FIELD_TYPE_LABELS[type] ?? type;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
// Settings-based visibility check. Returns true when the project has
|
|
41
|
+
// opted to hide the UI element registered under the given identifier
|
|
42
|
+
// in its `ui.hide` config (e.g. "reports.dashboardShareButton").
|
|
43
|
+
// Components that support hiding wrap their render with
|
|
44
|
+
// `{#if !isHidden(ctx, "...")}`.
|
|
45
|
+
export function isHidden(ctx: CTX | null | undefined, id: string): boolean {
|
|
46
|
+
return ctx?.meta?.ui?.hide?.[id] === true;
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
|
|
40
50
|
export function calculateDrawerWidth() {
|
|
41
51
|
const backgroundDrawerButtons = document.querySelectorAll(".backgroundDrawerButton");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ssr = false;
|