@cosmicdrift/kumiko-renderer 0.205.0 → 0.206.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 +3 -3
- package/src/app/__tests__/nav.test.ts +28 -4
- package/src/app/nav.tsx +33 -10
- package/src/index.ts +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.206.0",
|
|
4
4
|
"description": "Platform-agnostic React renderer for Kumiko screens. Contains the shared logic — primitives-contract, hooks, KumikoScreen, navigation & SSE abstractions — that any platform-specific renderer (web, native) composes. No DOM, no EventSource, no react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.206.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.206.0",
|
|
20
20
|
"react": "^19.2.6",
|
|
21
21
|
"temporal-polyfill": "^0.3.2",
|
|
22
22
|
"zod": "^4.4.3"
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { describe, expect, test } from "bun:test";
|
|
7
7
|
import type { ScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
8
8
|
import type { FeatureSchema } from "../feature-schema";
|
|
9
|
-
import { formatPath, parsePath, resolveTarget } from "../nav";
|
|
9
|
+
import { formatPath, hasDetailScreen, parsePath, resolveTarget } from "../nav";
|
|
10
10
|
|
|
11
11
|
describe("parsePath — ohne Workspaces", () => {
|
|
12
12
|
test("/<screenId>", () => {
|
|
@@ -126,10 +126,14 @@ describe("resolveTarget", () => {
|
|
|
126
126
|
expect(resolveTarget([], target)).toBe(target);
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
+
// Short screenId, not the qualified "property:screen:lease-detail" —
|
|
130
|
+
// ScreenTarget.screenId round-trips through formatPath/parsePath as a
|
|
131
|
+
// short id; RoutedScreen re-qualifies it itself (fw#2164 fix, previously
|
|
132
|
+
// double-qualified and silently fell back to the app's default screen).
|
|
129
133
|
test("ObjectTarget resolves via the matching custom detailFor screen", () => {
|
|
130
134
|
const schema = schemaWith("property", [leaseDetailScreen]);
|
|
131
135
|
expect(resolveTarget([schema], { entity: "lease", id: "l-1" })).toEqual({
|
|
132
|
-
screenId: "
|
|
136
|
+
screenId: "lease-detail",
|
|
133
137
|
entityId: "l-1",
|
|
134
138
|
});
|
|
135
139
|
});
|
|
@@ -146,7 +150,7 @@ describe("resolveTarget", () => {
|
|
|
146
150
|
leaseDetailScreen,
|
|
147
151
|
]);
|
|
148
152
|
expect(resolveTarget([schema], { entity: "lease", id: "l-1" })).toEqual({
|
|
149
|
-
screenId: "
|
|
153
|
+
screenId: "lease-detail",
|
|
150
154
|
entityId: "l-1",
|
|
151
155
|
});
|
|
152
156
|
});
|
|
@@ -156,7 +160,7 @@ describe("resolveTarget", () => {
|
|
|
156
160
|
const resolved = resolveTarget([schema], { entity: "lease", id: "l-1", workspaceId: "admin" });
|
|
157
161
|
expect(resolved).toEqual({
|
|
158
162
|
workspaceId: "admin",
|
|
159
|
-
screenId: "
|
|
163
|
+
screenId: "lease-detail",
|
|
160
164
|
entityId: "l-1",
|
|
161
165
|
});
|
|
162
166
|
});
|
|
@@ -172,3 +176,23 @@ describe("resolveTarget", () => {
|
|
|
172
176
|
expect(() => resolveTarget([schema], { entity: "boat", id: "b-1" })).toThrow(/"boat"/);
|
|
173
177
|
});
|
|
174
178
|
});
|
|
179
|
+
|
|
180
|
+
// hasDetailScreen: non-throwing existence check create-app's row-click
|
|
181
|
+
// default (fw#2164) uses to pick between a detailFor screen and its other
|
|
182
|
+
// fallbacks — resolveTarget's throw isn't usable there since not resolving
|
|
183
|
+
// is the expected, non-error case for most entities.
|
|
184
|
+
describe("hasDetailScreen", () => {
|
|
185
|
+
test("true when a screen declares detailFor for the entity", () => {
|
|
186
|
+
const schema = schemaWith("property", [leaseDetailScreen]);
|
|
187
|
+
expect(hasDetailScreen([schema], "lease")).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("false when no screen declares detailFor for the entity", () => {
|
|
191
|
+
const schema = schemaWith("property", [leaseListScreen, leaseEditScreen]);
|
|
192
|
+
expect(hasDetailScreen([schema], "lease")).toBe(false);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("false for an empty feature list", () => {
|
|
196
|
+
expect(hasDetailScreen([], "lease")).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
});
|
package/src/app/nav.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createContext, type ReactNode, useContext } from "react";
|
|
2
2
|
import type { FeatureSchema } from "./feature-schema";
|
|
3
|
-
import { qualifyScreenId } from "./qualify-screen-id";
|
|
4
3
|
|
|
5
4
|
// Navigation-Contract, plattform-neutral. Types + Context + Hook leben
|
|
6
5
|
// hier; die konkrete Implementation (window.history im Web,
|
|
@@ -118,21 +117,45 @@ export function formatPath(target: ScreenTarget): string {
|
|
|
118
117
|
return `/${segments.join("/")}`;
|
|
119
118
|
}
|
|
120
119
|
|
|
120
|
+
// Shared entity→detailFor-screen lookup — resolveTarget's ObjectTarget
|
|
121
|
+
// branch and hasDetailScreen (create-app's row-click default, fw#2164) both
|
|
122
|
+
// need "is there a detail screen for this entity", one to resolve it, the
|
|
123
|
+
// other to just check before falling back to a different default.
|
|
124
|
+
function findDetailForScreen(
|
|
125
|
+
features: readonly FeatureSchema[],
|
|
126
|
+
entity: string,
|
|
127
|
+
): { readonly featureName: string; readonly screenId: string } | undefined {
|
|
128
|
+
for (const feature of features) {
|
|
129
|
+
for (const screen of feature.screens) {
|
|
130
|
+
if (screen.detailFor === entity)
|
|
131
|
+
return { featureName: feature.featureName, screenId: screen.id };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Non-throwing existence check for findDetailForScreen — lets a caller pick
|
|
138
|
+
* a different default (fw#2164's row-click fallback) instead of resolving. */
|
|
139
|
+
export function hasDetailScreen(features: readonly FeatureSchema[], entity: string): boolean {
|
|
140
|
+
return findDetailForScreen(features, entity) !== undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
121
143
|
// Resolves a NavTarget to the ScreenTarget form navigate/replace/hrefFor
|
|
122
144
|
// operate on. Pure — no context, no I/O — so callers can use it outside
|
|
123
145
|
// React too (see resolve-at-NavApi-build alternative in renderer-web).
|
|
124
146
|
export function resolveTarget(features: readonly FeatureSchema[], target: NavTarget): ScreenTarget {
|
|
125
147
|
if ("screenId" in target) return target;
|
|
126
148
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
149
|
+
const found = findDetailForScreen(features, target.entity);
|
|
150
|
+
if (found !== undefined) {
|
|
151
|
+
// Short form (globally unique per validateScreenShortIdCollisions) —
|
|
152
|
+
// ScreenTarget.screenId round-trips through formatPath/parsePath as a
|
|
153
|
+
// short id; qualifying it here double-qualifies at RoutedScreen.
|
|
154
|
+
return {
|
|
155
|
+
...(target.workspaceId !== undefined && { workspaceId: target.workspaceId }),
|
|
156
|
+
screenId: found.screenId,
|
|
157
|
+
entityId: target.id,
|
|
158
|
+
};
|
|
136
159
|
}
|
|
137
160
|
|
|
138
161
|
throw new Error(
|
package/src/index.ts
CHANGED
|
@@ -72,7 +72,14 @@ export type {
|
|
|
72
72
|
ObjectTarget,
|
|
73
73
|
ScreenTarget,
|
|
74
74
|
} from "./app/nav";
|
|
75
|
-
export {
|
|
75
|
+
export {
|
|
76
|
+
formatPath,
|
|
77
|
+
hasDetailScreen,
|
|
78
|
+
NavProvider,
|
|
79
|
+
parsePath,
|
|
80
|
+
resolveTarget,
|
|
81
|
+
useNav,
|
|
82
|
+
} from "./app/nav";
|
|
76
83
|
export { lastSegment } from "./app/qn";
|
|
77
84
|
export type { VariableChipsProps } from "./app/variable-chips";
|
|
78
85
|
export { VariableChips } from "./app/variable-chips";
|