@cosmicdrift/kumiko-renderer-web 0.154.1 → 0.155.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.155.0",
|
|
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.
|
|
20
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
21
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
19
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.155.0",
|
|
20
|
+
"@cosmicdrift/kumiko-headless": "0.155.0",
|
|
21
|
+
"@cosmicdrift/kumiko-renderer": "0.155.0",
|
|
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",
|
|
@@ -9,13 +9,23 @@ import type {
|
|
|
9
9
|
AppSchema,
|
|
10
10
|
FeatureSchema,
|
|
11
11
|
NavDefinition,
|
|
12
|
+
ScreenDefinition,
|
|
12
13
|
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
14
|
+
import { resolveNavigation } from "@cosmicdrift/kumiko-headless";
|
|
13
15
|
import { buildNavRegistrySlice, buildNavRegistrySliceForApp } from "../nav-tree";
|
|
14
16
|
|
|
15
17
|
function feature(navs: readonly NavDefinition[], featureName = "tasks"): FeatureSchema {
|
|
16
18
|
return { featureName, entities: {}, screens: [], navs };
|
|
17
19
|
}
|
|
18
20
|
|
|
21
|
+
function featureWithScreens(
|
|
22
|
+
navs: readonly NavDefinition[],
|
|
23
|
+
screens: readonly ScreenDefinition[],
|
|
24
|
+
featureName = "tasks",
|
|
25
|
+
): FeatureSchema {
|
|
26
|
+
return { featureName, entities: {}, screens, navs };
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
describe("buildNavRegistrySlice", () => {
|
|
20
30
|
test("qualifiziert Nav-IDs zu feature:nav:id", () => {
|
|
21
31
|
const slice = buildNavRegistrySlice(feature([{ id: "main", label: "Main" }]));
|
|
@@ -70,4 +80,80 @@ describe("buildNavRegistrySliceForApp", () => {
|
|
|
70
80
|
const slice = buildNavRegistrySliceForApp(app);
|
|
71
81
|
expect(slice.topLevel.map((n) => n.id)).toEqual(["shop:nav:catalog", "admin:nav:users"]);
|
|
72
82
|
});
|
|
83
|
+
|
|
84
|
+
test("nav ohne eigene access erbt access vom referenzierten Screen (#1099)", () => {
|
|
85
|
+
const app: AppSchema = {
|
|
86
|
+
features: [
|
|
87
|
+
featureWithScreens(
|
|
88
|
+
[{ id: "members", label: "Members", screen: "members" }],
|
|
89
|
+
[
|
|
90
|
+
{
|
|
91
|
+
id: "members",
|
|
92
|
+
type: "custom",
|
|
93
|
+
renderer: { react: { __component: "Members" } },
|
|
94
|
+
access: { roles: ["Admin"] },
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
),
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
const slice = buildNavRegistrySliceForApp(app);
|
|
101
|
+
expect(slice.topLevel[0]?.access).toEqual({ roles: ["Admin"] });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("nav mit eigener access ignoriert die Screen-Access (expliziter Override gewinnt)", () => {
|
|
105
|
+
const app: AppSchema = {
|
|
106
|
+
features: [
|
|
107
|
+
featureWithScreens(
|
|
108
|
+
[
|
|
109
|
+
{
|
|
110
|
+
id: "members",
|
|
111
|
+
label: "Members",
|
|
112
|
+
screen: "members",
|
|
113
|
+
access: { roles: ["Admin", "Editor"] },
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
[
|
|
117
|
+
{
|
|
118
|
+
id: "members",
|
|
119
|
+
type: "custom",
|
|
120
|
+
renderer: { react: { __component: "Members" } },
|
|
121
|
+
access: { roles: ["Admin"] },
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
),
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
const slice = buildNavRegistrySliceForApp(app);
|
|
128
|
+
expect(slice.topLevel[0]?.access).toEqual({ roles: ["Admin", "Editor"] });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("nav ohne screen bleibt ohne access (nichts zum Erben da)", () => {
|
|
132
|
+
const app: AppSchema = {
|
|
133
|
+
features: [feature([{ id: "group", label: "Group" }])],
|
|
134
|
+
};
|
|
135
|
+
const slice = buildNavRegistrySliceForApp(app);
|
|
136
|
+
expect(slice.topLevel[0]?.access).toBeUndefined();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("end-to-end: resolveNavigation versteckt den Eintrag fuer eine Rolle, die den Ziel-Screen nicht sehen darf", () => {
|
|
140
|
+
const app: AppSchema = {
|
|
141
|
+
features: [
|
|
142
|
+
featureWithScreens(
|
|
143
|
+
[{ id: "members", label: "Members", screen: "members" }],
|
|
144
|
+
[
|
|
145
|
+
{
|
|
146
|
+
id: "members",
|
|
147
|
+
type: "custom",
|
|
148
|
+
renderer: { react: { __component: "Members" } },
|
|
149
|
+
access: { roles: ["Admin"] },
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
),
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
const source = buildNavRegistrySliceForApp(app);
|
|
156
|
+
const tree = resolveNavigation({ source, user: { id: "u1", roles: ["Editor"] } });
|
|
157
|
+
expect(tree).toEqual([]);
|
|
158
|
+
});
|
|
73
159
|
});
|
package/src/layout/nav-tree.tsx
CHANGED
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
// auf/zu. State lebt lokal im NavTree (useState); Default expanded
|
|
12
12
|
// für alles, Caller kann später localStorage-Persistenz drüberlegen.
|
|
13
13
|
|
|
14
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
AccessRule,
|
|
16
|
+
TargetRef,
|
|
17
|
+
TreeAction,
|
|
18
|
+
TreeNode,
|
|
19
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
15
20
|
import type { NavDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
16
21
|
import type { NavNode, NavRegistrySlice } from "@cosmicdrift/kumiko-headless";
|
|
17
22
|
import { resolveNavigation } from "@cosmicdrift/kumiko-headless";
|
|
@@ -816,14 +821,24 @@ export function buildNavRegistrySliceForApp(
|
|
|
816
821
|
app: AppSchema,
|
|
817
822
|
allowedNavQns?: ReadonlySet<string>,
|
|
818
823
|
): NavRegistrySlice {
|
|
824
|
+
const screenAccessByQn = buildScreenAccessMap(app);
|
|
819
825
|
const qualified: NavDefinition[] = [];
|
|
820
826
|
for (const feature of app.features) {
|
|
821
827
|
for (const n of feature.navs ?? []) {
|
|
828
|
+
const screen =
|
|
829
|
+
n.screen !== undefined ? qualifyScreenId(feature.featureName, n.screen) : undefined;
|
|
830
|
+
// Nav entries without their own `access` inherit the referenced
|
|
831
|
+
// screen's access rule — otherwise a broader workspace/parent access
|
|
832
|
+
// lets a role see a nav entry whose target screen 403s them (#1099).
|
|
833
|
+
// An explicit `n.access` always wins; inheritance only fills the gap.
|
|
834
|
+
const inheritedAccess =
|
|
835
|
+
n.access ?? (screen !== undefined ? screenAccessByQn.get(screen) : undefined);
|
|
822
836
|
qualified.push({
|
|
823
837
|
...n,
|
|
824
838
|
id: qualifyNavId(feature.featureName, n.id),
|
|
825
839
|
...(n.parent !== undefined && { parent: qualifyNavId(feature.featureName, n.parent) }),
|
|
826
|
-
...(
|
|
840
|
+
...(screen !== undefined && { screen }),
|
|
841
|
+
...(inheritedAccess !== undefined && { access: inheritedAccess }),
|
|
827
842
|
});
|
|
828
843
|
}
|
|
829
844
|
}
|
|
@@ -863,6 +878,16 @@ function qualifyScreenId(feature: string, id: string): string {
|
|
|
863
878
|
return id.includes(":screen:") ? id : `${feature}:screen:${id}`;
|
|
864
879
|
}
|
|
865
880
|
|
|
881
|
+
function buildScreenAccessMap(app: AppSchema): Map<string, AccessRule | undefined> {
|
|
882
|
+
const map = new Map<string, AccessRule | undefined>();
|
|
883
|
+
for (const feature of app.features) {
|
|
884
|
+
for (const s of feature.screens) {
|
|
885
|
+
map.set(qualifyScreenId(feature.featureName, s.id), s.access);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return map;
|
|
889
|
+
}
|
|
890
|
+
|
|
866
891
|
// `lastSegment` lebt jetzt in @cosmicdrift/kumiko-renderer (./app/qn) — eine
|
|
867
892
|
// Quelle, beide Pakete teilen sie.
|
|
868
893
|
export { lastSegment };
|