@lobb-js/studio 0.36.0 → 0.37.1

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.
@@ -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 { emitEvent } from "../eventSystem";
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
- try {
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
- {headerIsSmall ? "" : "Refresh"}
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">
@@ -9,6 +9,7 @@ interface Props {
9
9
  onLink?: (record: any) => void;
10
10
  onCreate?: (changes: Changes) => void;
11
11
  showImport?: boolean;
12
+ loading?: boolean;
12
13
  left?: Snippet<[]>;
13
14
  }
14
15
  declare const Header: import("svelte").Component<Props, {}, "selectedRecords" | "params">;
@@ -223,8 +223,8 @@
223
223
  {:else}
224
224
  <ColumnIcon size="12.5" class="text-muted-foreground" />
225
225
  {/if}
226
- <div class="font-bold">{column.id}</div>
227
- <div class="text-muted-foreground text-[0.7rem]">
226
+ <div class="font-bold whitespace-nowrap">{column.id}</div>
227
+ <div class="text-muted-foreground text-[0.7rem] whitespace-nowrap">
228
228
  {column.subtext}
229
229
  </div>
230
230
  </button>
@@ -95,8 +95,8 @@
95
95
  showImport={false}
96
96
  showHeader={true}
97
97
  showFooter={false}
98
- showDelete={false}
99
- showEdit={false}
98
+ showDelete={true}
99
+ showEdit={true}
100
100
  tableProps={{ showLastColumnBorder: false, showLastRowBorder: true }}
101
101
  >
102
102
  {#snippet headerLeft()}
@@ -60,9 +60,7 @@
60
60
  relation.to.collection === parentRecord?.collectionName,
61
61
  )?.from.field;
62
62
  const createValues = {
63
- [refrenceFieldName]: {
64
- id: 0,
65
- },
63
+ [refrenceFieldName]: 0,
66
64
  };
67
65
  let selectedRecordsIds: string[] = $derived(
68
66
  entries.filter((entry) => entry.id).map((entry) => entry.id),
@@ -1,14 +1,10 @@
1
1
  import { House, Layers, Library, Workflow } from "lucide-svelte";
2
- import { emitEvent } from "../eventSystem";
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
- const res = await emitEvent({ lobb, ctx }, "auth.canAccess", {
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 { emitEvent } from "../../../eventSystem";
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
- // auth.canAccess checks resolve below.
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
- const res = await emitEvent(
36
- { lobb, ctx },
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
 
@@ -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>;
@@ -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) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lobb-js/studio",
3
3
  "license": "UNLICENSED",
4
- "version": "0.36.0",
4
+ "version": "0.37.1",
5
5
  "type": "module",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -28,7 +28,8 @@
28
28
  }
29
29
  },
30
30
  "scripts": {
31
- "dev": "vite 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": "vitest",
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.36.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 { emitEvent } from "../eventSystem";
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
- try {
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
- {headerIsSmall ? "" : "Refresh"}
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">
@@ -223,8 +223,8 @@
223
223
  {:else}
224
224
  <ColumnIcon size="12.5" class="text-muted-foreground" />
225
225
  {/if}
226
- <div class="font-bold">{column.id}</div>
227
- <div class="text-muted-foreground text-[0.7rem]">
226
+ <div class="font-bold whitespace-nowrap">{column.id}</div>
227
+ <div class="text-muted-foreground text-[0.7rem] whitespace-nowrap">
228
228
  {column.subtext}
229
229
  </div>
230
230
  </button>
@@ -95,8 +95,8 @@
95
95
  showImport={false}
96
96
  showHeader={true}
97
97
  showFooter={false}
98
- showDelete={false}
99
- showEdit={false}
98
+ showDelete={true}
99
+ showEdit={true}
100
100
  tableProps={{ showLastColumnBorder: false, showLastRowBorder: true }}
101
101
  >
102
102
  {#snippet headerLeft()}
@@ -60,9 +60,7 @@
60
60
  relation.to.collection === parentRecord?.collectionName,
61
61
  )?.from.field;
62
62
  const createValues = {
63
- [refrenceFieldName]: {
64
- id: 0,
65
- },
63
+ [refrenceFieldName]: 0,
66
64
  };
67
65
  let selectedRecordsIds: string[] = $derived(
68
66
  entries.filter((entry) => entry.id).map((entry) => entry.id),
@@ -1,5 +1,5 @@
1
1
  import { House, Layers, Library, Workflow } from "lucide-svelte";
2
- import { emitEvent } from "../eventSystem";
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
- const res = await emitEvent({ lobb, ctx }, "auth.canAccess", {
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 { emitEvent } from "../../../eventSystem";
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
- // auth.canAccess checks resolve below.
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
- const res = await emitEvent(
36
- { lobb, ctx },
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
 
@@ -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(
@@ -0,0 +1,7 @@
1
+ <script lang="ts">
2
+ import "../app.css";
3
+
4
+ let { children } = $props();
5
+ </script>
6
+
7
+ {@render children()}
@@ -0,0 +1 @@
1
+ export const ssr = false;
@@ -0,0 +1,5 @@
1
+ <script lang="ts">
2
+ import { StudioRoot } from "$lib/index.js";
3
+ </script>
4
+
5
+ <StudioRoot />
@@ -1,6 +0,0 @@
1
- <script lang="ts">
2
- import { Studio } from '$lib/index.js';
3
- import '../app.css';
4
- </script>
5
-
6
- <Studio />