@cosmicdrift/kumiko-renderer-web 0.157.0 → 0.157.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer-web",
3
- "version": "0.157.0",
3
+ "version": "0.157.1",
4
4
  "description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -16,9 +16,9 @@
16
16
  "./styles.css": "./src/styles.css"
17
17
  },
18
18
  "dependencies": {
19
- "@cosmicdrift/kumiko-dispatcher-live": "0.157.0",
20
- "@cosmicdrift/kumiko-headless": "0.157.0",
21
- "@cosmicdrift/kumiko-renderer": "0.157.0",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.157.1",
20
+ "@cosmicdrift/kumiko-headless": "0.157.1",
21
+ "@cosmicdrift/kumiko-renderer": "0.157.1",
22
22
  "@radix-ui/react-dialog": "^1.1.15",
23
23
  "@radix-ui/react-dropdown-menu": "^2.1.16",
24
24
  "@radix-ui/react-label": "^2.1.8",
@@ -7,9 +7,11 @@
7
7
  // pinst dass DefaultAppShell user nun akzeptiert UND durchreicht.
8
8
 
9
9
  import { describe, expect, test } from "bun:test";
10
+ import type { EntityListScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
10
11
  import type { FeatureSchema } from "@cosmicdrift/kumiko-renderer";
12
+ import { DispatcherProvider, KumikoScreen } from "@cosmicdrift/kumiko-renderer";
11
13
  import { DefaultAppShell } from "../layout/default-app-shell";
12
- import { render, screen } from "./test-utils";
14
+ import { createMockDispatcher, render, screen } from "./test-utils";
13
15
 
14
16
  function makeSchema(): FeatureSchema {
15
17
  return {
@@ -84,3 +86,47 @@ describe("DefaultAppShell headerActions-Slot (Topbar rechts)", () => {
84
86
  expect(document.querySelector("[data-kumiko-layout='header-actions']")).toBeNull();
85
87
  });
86
88
  });
89
+
90
+ // DefaultAppShell threads `user.roles` into a UserRolesProvider around
91
+ // children (#1203) — KumikoScreen reads that to gate role-restricted
92
+ // screens at render time, not just in nav.
93
+ describe("DefaultAppShell wires user.roles into children for screen-level access (#1203)", () => {
94
+ const restrictedScreen: EntityListScreenDefinition = {
95
+ id: "restricted",
96
+ type: "entityList",
97
+ entity: "x",
98
+ columns: [],
99
+ access: { roles: ["SystemAdmin"] },
100
+ };
101
+ const restrictedSchema: FeatureSchema = {
102
+ featureName: "showcase",
103
+ entities: {},
104
+ screens: [restrictedScreen],
105
+ } as FeatureSchema;
106
+
107
+ test("no user prop → role-gated screen child shows access-denied", () => {
108
+ render(
109
+ <DispatcherProvider dispatcher={createMockDispatcher()}>
110
+ <DefaultAppShell brand={<span>Brand</span>} schema={restrictedSchema}>
111
+ <KumikoScreen schema={restrictedSchema} qn="showcase:screen:restricted" />
112
+ </DefaultAppShell>
113
+ </DispatcherProvider>,
114
+ );
115
+ expect(screen.getByTestId("kumiko-screen-access-denied")).toBeTruthy();
116
+ });
117
+
118
+ test("user prop with matching role → role-gated screen child renders", () => {
119
+ render(
120
+ <DispatcherProvider dispatcher={createMockDispatcher()}>
121
+ <DefaultAppShell
122
+ brand={<span>Brand</span>}
123
+ schema={restrictedSchema}
124
+ user={{ id: "u1", roles: ["SystemAdmin"] }}
125
+ >
126
+ <KumikoScreen schema={restrictedSchema} qn="showcase:screen:restricted" />
127
+ </DefaultAppShell>
128
+ </DispatcherProvider>,
129
+ );
130
+ expect(screen.queryByTestId("kumiko-screen-access-denied")).toBeNull();
131
+ });
132
+ });
@@ -13,6 +13,7 @@ import {
13
13
  KumikoScreen,
14
14
  kumikoDefaultTranslations,
15
15
  NavProvider,
16
+ UserRolesProvider,
16
17
  } from "@cosmicdrift/kumiko-renderer";
17
18
  import userEvent from "@testing-library/user-event";
18
19
  import { createMockDispatcher, fireEvent, render, screen, waitFor } from "./test-utils";
@@ -41,10 +42,18 @@ const listScreen: EntityListScreenDefinition = {
41
42
  columns: ["title", "count", "done"],
42
43
  };
43
44
 
45
+ const restrictedListScreen: EntityListScreenDefinition = {
46
+ id: "task-list-restricted",
47
+ type: "entityList",
48
+ entity: "task",
49
+ columns: ["title"],
50
+ access: { roles: ["Admin"] },
51
+ };
52
+
44
53
  const schema: FeatureSchema = {
45
54
  featureName: "tasks",
46
55
  entities: { task: taskEntity },
47
- screens: [editScreen, listScreen],
56
+ screens: [editScreen, listScreen, restrictedListScreen],
48
57
  };
49
58
 
50
59
  function makeDispatcher(overrides: Partial<Dispatcher> = {}): Dispatcher {
@@ -67,6 +76,39 @@ describe("KumikoScreen", () => {
67
76
  expect(screen.getByTestId("kumiko-screen-not-found")).toBeTruthy();
68
77
  });
69
78
 
79
+ test("role-gated screen, no UserRolesProvider mounted → access-denied placeholder", () => {
80
+ render(
81
+ <DispatcherProvider dispatcher={makeDispatcher()}>
82
+ <KumikoScreen schema={schema} qn="tasks:screen:task-list-restricted" />
83
+ </DispatcherProvider>,
84
+ );
85
+ expect(screen.getByTestId("kumiko-screen-access-denied")).toBeTruthy();
86
+ });
87
+
88
+ test("role-gated screen, user without matching role → access-denied placeholder", () => {
89
+ render(
90
+ <DispatcherProvider dispatcher={makeDispatcher()}>
91
+ <UserRolesProvider roles={["Viewer"]}>
92
+ <KumikoScreen schema={schema} qn="tasks:screen:task-list-restricted" />
93
+ </UserRolesProvider>
94
+ </DispatcherProvider>,
95
+ );
96
+ expect(screen.getByTestId("kumiko-screen-access-denied")).toBeTruthy();
97
+ });
98
+
99
+ test("role-gated screen, user with matching role → renders normally", async () => {
100
+ render(
101
+ <DispatcherProvider dispatcher={makeDispatcher()}>
102
+ <UserRolesProvider roles={["Admin"]}>
103
+ <KumikoScreen schema={schema} qn="tasks:screen:task-list-restricted" />
104
+ </UserRolesProvider>
105
+ </DispatcherProvider>,
106
+ );
107
+ await waitFor(() => expect(screen.queryByTestId("kumiko-screen-loading")).toBeNull());
108
+ expect(screen.queryByTestId("kumiko-screen-access-denied")).toBeNull();
109
+ expect(screen.getByTestId("render-list-table-toolbar")).toBeTruthy();
110
+ });
111
+
70
112
  test("entityEdit → renders RenderEdit form for the screen's entity", () => {
71
113
  render(
72
114
  <DispatcherProvider dispatcher={makeDispatcher()}>
@@ -1,7 +1,10 @@
1
1
  import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+ import type { EntityListScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
3
  import type { WorkspaceSchema } from "@cosmicdrift/kumiko-renderer";
3
4
  import {
4
5
  createStaticLocaleResolver,
6
+ DispatcherProvider,
7
+ KumikoScreen,
5
8
  LocaleProvider,
6
9
  NavProvider,
7
10
  PrimitivesProvider,
@@ -17,7 +20,7 @@ import {
17
20
  } from "../layout/workspace-shell";
18
21
  import { WorkspaceSwitcher } from "../layout/workspace-switcher";
19
22
  import { defaultPrimitives } from "../primitives";
20
- import { fireEvent, render, screen } from "./test-utils";
23
+ import { createMockDispatcher, fireEvent, render, screen } from "./test-utils";
21
24
 
22
25
  // jsdom shares window.history across tests in the same file. Reset to /
23
26
  // before each render so URL-driven workspace state from one test doesn't
@@ -871,3 +874,49 @@ describe("WorkspaceShell — EINE Nav", () => {
871
874
  expect(systemItems.every((el) => !el.closest("li")?.textContent?.includes("3"))).toBe(true);
872
875
  });
873
876
  });
877
+
878
+ // WorkspaceShell threads `user.roles` into a UserRolesProvider around
879
+ // children (#1203) — KumikoScreen reads that to gate role-restricted
880
+ // screens at render time, not just via nav/workspace-switcher filtering.
881
+ describe("WorkspaceShell wires user.roles into children for screen-level access (#1203)", () => {
882
+ const restrictedScreen: EntityListScreenDefinition = {
883
+ id: "restricted",
884
+ type: "entityList",
885
+ entity: "x",
886
+ columns: [],
887
+ access: { roles: ["admin"] },
888
+ };
889
+ const schema = {
890
+ featureName: "bmc",
891
+ entities: {},
892
+ screens: [restrictedScreen],
893
+ navs: [],
894
+ workspaces: [],
895
+ } as const;
896
+
897
+ test("no user prop → role-gated screen child shows access-denied", () => {
898
+ renderShell(
899
+ <DispatcherProvider dispatcher={createMockDispatcher()}>
900
+ <WorkspaceShell brand={<div>Brand</div>} schema={schema}>
901
+ <KumikoScreen schema={schema} qn="bmc:screen:restricted" />
902
+ </WorkspaceShell>
903
+ </DispatcherProvider>,
904
+ );
905
+ expect(screen.getByTestId("kumiko-screen-access-denied")).toBeTruthy();
906
+ });
907
+
908
+ test("user prop with matching role → role-gated screen child renders", () => {
909
+ renderShell(
910
+ <DispatcherProvider dispatcher={createMockDispatcher()}>
911
+ <WorkspaceShell
912
+ brand={<div>Brand</div>}
913
+ schema={schema}
914
+ user={{ id: "u1", roles: ["admin"] }}
915
+ >
916
+ <KumikoScreen schema={schema} qn="bmc:screen:restricted" />
917
+ </WorkspaceShell>
918
+ </DispatcherProvider>,
919
+ );
920
+ expect(screen.queryByTestId("kumiko-screen-access-denied")).toBeNull();
921
+ });
922
+ });
package/src/index.ts CHANGED
@@ -51,6 +51,7 @@ export type {
51
51
  UseFormResult,
52
52
  UseQueryOptions,
53
53
  UseQueryResult,
54
+ UserRolesProviderProps,
54
55
  WorkspaceSchema,
55
56
  } from "@cosmicdrift/kumiko-renderer";
56
57
  export {
@@ -69,6 +70,7 @@ export {
69
70
  RenderField,
70
71
  RenderList,
71
72
  TokensProvider,
73
+ UserRolesProvider,
72
74
  useDispatcher,
73
75
  useDispatcherStatus,
74
76
  useForm,
@@ -82,6 +84,7 @@ export {
82
84
  useTokenController,
83
85
  useTokens,
84
86
  useTranslation,
87
+ useUserRoles,
85
88
  } from "@cosmicdrift/kumiko-renderer";
86
89
  // --- Web-platform specifics ---
87
90
  export { createBrowserLocaleResolver } from "./app/browser-locale";
@@ -14,7 +14,11 @@
14
14
  // Apps die feingranulare Kontrolle wollen, gehen direkt auf die ui/-Teile.
15
15
  // Wer ein Topbar mit Workspace-Switcher braucht, nutzt <WorkspaceShell>.
16
16
 
17
- import type { AppSchema, FeatureSchema } from "@cosmicdrift/kumiko-renderer";
17
+ import {
18
+ type AppSchema,
19
+ type FeatureSchema,
20
+ UserRolesProvider,
21
+ } from "@cosmicdrift/kumiko-renderer";
18
22
  import type { ReactNode } from "react";
19
23
  import {
20
24
  Sidebar,
@@ -112,7 +116,7 @@ export function DefaultAppShell({
112
116
  {...(headerActions !== undefined && { headerActions })}
113
117
  />
114
118
  <main className={fill === true ? "min-h-0 flex-1 overflow-auto" : "flex-1 overflow-auto"}>
115
- {children}
119
+ <UserRolesProvider roles={user?.roles}>{children}</UserRolesProvider>
116
120
  </main>
117
121
  </SidebarInset>
118
122
  </SidebarProvider>
@@ -31,7 +31,7 @@
31
31
 
32
32
  import type { AccessRule } from "@cosmicdrift/kumiko-framework/ui-types";
33
33
  import type { AppSchema, FeatureSchema, WorkspaceSchema } from "@cosmicdrift/kumiko-renderer";
34
- import { qualifyNavId, toAppSchema, useNav } from "@cosmicdrift/kumiko-renderer";
34
+ import { qualifyNavId, toAppSchema, UserRolesProvider, useNav } from "@cosmicdrift/kumiko-renderer";
35
35
  import { type ReactNode, useCallback, useLayoutEffect, useMemo } from "react";
36
36
  import { useResolvers } from "../app/resolvers-context";
37
37
  import {
@@ -239,7 +239,11 @@ export function WorkspaceShell({
239
239
  {...(topbarActions !== undefined && { headerActions: topbarActions })}
240
240
  />
241
241
  <main className="flex-1 overflow-auto">
242
- {activeTarget !== undefined ? <EditorPanel resolvers={resolvers} /> : children}
242
+ {activeTarget !== undefined ? (
243
+ <EditorPanel resolvers={resolvers} />
244
+ ) : (
245
+ <UserRolesProvider roles={user?.roles}>{children}</UserRolesProvider>
246
+ )}
243
247
  </main>
244
248
  </SidebarInset>
245
249
  </SidebarProvider>