@cosmicdrift/kumiko-renderer-web 0.286.0 → 0.287.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.287.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.287.0",
|
|
20
|
+
"@cosmicdrift/kumiko-headless": "0.287.0",
|
|
21
|
+
"@cosmicdrift/kumiko-renderer": "0.287.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",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"@types/react-dom": "^19.2.3",
|
|
67
67
|
"jsdom": "^29.1.1",
|
|
68
68
|
"tailwindcss": "^4.3.0",
|
|
69
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
69
|
+
"@cosmicdrift/kumiko-locale-de": "0.287.0"
|
|
70
70
|
},
|
|
71
71
|
"repository": {
|
|
72
72
|
"type": "git",
|
package/src/index.ts
CHANGED
|
@@ -112,6 +112,8 @@ export type { DefaultAppShellProps } from "./layout/default-app-shell";
|
|
|
112
112
|
export { DefaultAppShell } from "./layout/default-app-shell";
|
|
113
113
|
export type { EditorPanelProps, ResolverComponent } from "./layout/editor-panel";
|
|
114
114
|
export { EditorPanel } from "./layout/editor-panel";
|
|
115
|
+
export type { NavReparentOverride } from "./layout/filter-app-schema-navs";
|
|
116
|
+
export { filterAppSchemaNavsByAllowlist } from "./layout/filter-app-schema-navs";
|
|
115
117
|
export type { LanguageSwitcherProps, LocaleOption } from "./layout/language-switcher";
|
|
116
118
|
export { LanguageSwitcher } from "./layout/language-switcher";
|
|
117
119
|
export type { NavTreeProps } from "./layout/nav-tree";
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { AppSchema, FeatureSchema } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
+
import { filterAppSchemaNavsByAllowlist } from "../filter-app-schema-navs";
|
|
4
|
+
|
|
5
|
+
function feature(overrides: Partial<FeatureSchema> & { featureName: string }): FeatureSchema {
|
|
6
|
+
return {
|
|
7
|
+
entities: {},
|
|
8
|
+
screens: [],
|
|
9
|
+
...overrides,
|
|
10
|
+
} as FeatureSchema;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe("filterAppSchemaNavsByAllowlist", () => {
|
|
14
|
+
test("keeps only navs whose qualified QN is in the allowlist", () => {
|
|
15
|
+
const schema: AppSchema = {
|
|
16
|
+
features: [
|
|
17
|
+
feature({
|
|
18
|
+
featureName: "vehicles",
|
|
19
|
+
navs: [{ id: "vehicle-list", label: "x", screen: "vehicles:screen:vehicle-list" }],
|
|
20
|
+
}),
|
|
21
|
+
feature({
|
|
22
|
+
featureName: "app-shell",
|
|
23
|
+
navs: [{ id: "vehicle-list", label: "x", screen: "vehicles:screen:vehicle-list" }],
|
|
24
|
+
}),
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const filtered = filterAppSchemaNavsByAllowlist(
|
|
29
|
+
schema,
|
|
30
|
+
new Set(["app-shell:nav:vehicle-list"]),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(filtered.features.find((f) => f.featureName === "vehicles")?.navs).toEqual([]);
|
|
34
|
+
expect(filtered.features.find((f) => f.featureName === "app-shell")?.navs).toHaveLength(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("passes through an already-qualified id unchanged when allowed", () => {
|
|
38
|
+
const schema: AppSchema = {
|
|
39
|
+
features: [
|
|
40
|
+
feature({
|
|
41
|
+
featureName: "config",
|
|
42
|
+
navs: [{ id: "config:nav:audience-tenant", label: "x" }],
|
|
43
|
+
}),
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const filtered = filterAppSchemaNavsByAllowlist(
|
|
48
|
+
schema,
|
|
49
|
+
new Set(["config:nav:audience-tenant"]),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(filtered.features[0]?.navs).toHaveLength(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("a feature without navs passes through unchanged", () => {
|
|
56
|
+
const schema: AppSchema = {
|
|
57
|
+
features: [feature({ featureName: "no-nav-feature" })],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const filtered = filterAppSchemaNavsByAllowlist(schema, new Set());
|
|
61
|
+
|
|
62
|
+
expect(filtered.features[0]?.navs).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("an empty allowlist yields empty navs arrays, not a broken schema", () => {
|
|
66
|
+
const schema: AppSchema = {
|
|
67
|
+
features: [feature({ featureName: "vehicles", navs: [{ id: "vehicle-list", label: "x" }] })],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const filtered = filterAppSchemaNavsByAllowlist(schema, new Set());
|
|
71
|
+
|
|
72
|
+
expect(filtered.features[0]?.navs).toEqual([]);
|
|
73
|
+
expect(filtered.features).toHaveLength(1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("reparents an adopted foreign nav onto the given section, leaving other fields intact", () => {
|
|
77
|
+
const schema: AppSchema = {
|
|
78
|
+
features: [
|
|
79
|
+
feature({
|
|
80
|
+
featureName: "config",
|
|
81
|
+
navs: [
|
|
82
|
+
{ id: "audience-tenant", label: "config.settings.tenant", order: 0 },
|
|
83
|
+
{
|
|
84
|
+
id: "dealers-tenant",
|
|
85
|
+
label: "dealers.settings",
|
|
86
|
+
icon: "building",
|
|
87
|
+
parent: "audience-tenant",
|
|
88
|
+
screen: "config:screen:dealers-tenant",
|
|
89
|
+
order: 1,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
}),
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const filtered = filterAppSchemaNavsByAllowlist(
|
|
97
|
+
schema,
|
|
98
|
+
new Set(["config:nav:dealers-tenant"]),
|
|
99
|
+
new Map([["config:nav:dealers-tenant", { parent: "app-shell:nav:tenant", order: 10 }]]),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(filtered.features[0]?.navs).toEqual([
|
|
103
|
+
{
|
|
104
|
+
id: "dealers-tenant",
|
|
105
|
+
label: "dealers.settings",
|
|
106
|
+
icon: "building",
|
|
107
|
+
parent: "app-shell:nav:tenant",
|
|
108
|
+
screen: "config:screen:dealers-tenant",
|
|
109
|
+
order: 10,
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AppSchema, FeatureSchema } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
import { qualifyNavId } from "./nav-tree";
|
|
3
|
+
|
|
4
|
+
export type NavReparentOverride = { readonly parent: string; readonly order: number };
|
|
5
|
+
|
|
6
|
+
// Reduces every feature's `navs` to the app's sidebar allowlist, leaving
|
|
7
|
+
// screens/entities/handlers/translations and every other schema field
|
|
8
|
+
// untouched. Pure and defensive: a feature with no `navs` passes through
|
|
9
|
+
// unchanged, and a feature whose every nav gets stripped ends up with an
|
|
10
|
+
// empty `navs` array, never `undefined` turned into something broken.
|
|
11
|
+
//
|
|
12
|
+
// `reparentOverrides` (keyed by the nav's qualified QN) lets a foreign,
|
|
13
|
+
// otherwise-unmodifiable nav (e.g. a settings-hub-synthesized entry that
|
|
14
|
+
// can't be hand-re-registered under a new parent at boot-validation time)
|
|
15
|
+
// be "adopted" into a different section by rewriting its parent/order as
|
|
16
|
+
// it survives the allowlist filter. Without an override, a surviving nav's
|
|
17
|
+
// parent/order pass through unchanged.
|
|
18
|
+
export function filterAppSchemaNavsByAllowlist(
|
|
19
|
+
schema: AppSchema,
|
|
20
|
+
allowedNavQns: ReadonlySet<string>,
|
|
21
|
+
reparentOverrides: ReadonlyMap<string, NavReparentOverride> = new Map(),
|
|
22
|
+
): AppSchema {
|
|
23
|
+
return {
|
|
24
|
+
...schema,
|
|
25
|
+
features: schema.features.map((feature): FeatureSchema => {
|
|
26
|
+
if (feature.navs === undefined) return feature;
|
|
27
|
+
return {
|
|
28
|
+
...feature,
|
|
29
|
+
navs: feature.navs.flatMap((nav) => {
|
|
30
|
+
const qn = qualifyNavId(feature.featureName, nav.id);
|
|
31
|
+
if (!allowedNavQns.has(qn)) return [];
|
|
32
|
+
const override = reparentOverrides.get(qn);
|
|
33
|
+
return override === undefined
|
|
34
|
+
? [nav]
|
|
35
|
+
: [{ ...nav, parent: override.parent, order: override.order }];
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
}
|
package/src/layout/nav-tree.tsx
CHANGED
|
@@ -970,7 +970,10 @@ export function buildNavRegistrySliceForApp(
|
|
|
970
970
|
};
|
|
971
971
|
}
|
|
972
972
|
|
|
973
|
-
|
|
973
|
+
// Exported (not part of the package's public index.ts surface) so other
|
|
974
|
+
// modules under layout/ — e.g. filter-app-schema-navs.ts — qualify nav ids
|
|
975
|
+
// the same way instead of keeping a second copy in sync.
|
|
976
|
+
export function qualifyNavId(feature: string, id: string): string {
|
|
974
977
|
return id.includes(":nav:") ? id : `${feature}:nav:${id}`;
|
|
975
978
|
}
|
|
976
979
|
|