@cosmicdrift/kumiko-renderer 0.309.0 → 0.310.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",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.310.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,9 +15,9 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
20
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.310.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.310.0",
|
|
20
|
+
"@cosmicdrift/kumiko-types": "0.310.0",
|
|
21
21
|
"react": "^19.2.6",
|
|
22
22
|
"temporal-polyfill": "^0.3.2",
|
|
23
23
|
"zod": "^4.4.3"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@types/react-dom": "^19.2.3",
|
|
29
29
|
"jsdom": "^29.1.1",
|
|
30
30
|
"react-dom": "^19.2.6",
|
|
31
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
31
|
+
"@cosmicdrift/kumiko-locale-de": "0.310.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { RowActionNavigate } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
|
+
import type { NavApi, NavTarget } from "../nav";
|
|
4
|
+
import { RETURN_TO_PARAM, type ReturnHost } from "../return-to";
|
|
5
|
+
import { buildRecordActions } from "../row-actions";
|
|
6
|
+
|
|
7
|
+
function recordingNav(): {
|
|
8
|
+
readonly nav: NavApi;
|
|
9
|
+
readonly navigateCalls: NavTarget[];
|
|
10
|
+
readonly setSearchParamsCalls: ReadonlyArray<Readonly<Record<string, string | null>>>;
|
|
11
|
+
} {
|
|
12
|
+
const navigateCalls: NavTarget[] = [];
|
|
13
|
+
const setSearchParamsCalls: Array<Readonly<Record<string, string | null>>> = [];
|
|
14
|
+
const nav: NavApi = {
|
|
15
|
+
route: undefined,
|
|
16
|
+
navigate: (target) => navigateCalls.push(target),
|
|
17
|
+
replace: () => {},
|
|
18
|
+
hrefFor: () => "",
|
|
19
|
+
searchParams: {},
|
|
20
|
+
setSearchParams: (updates) => setSearchParamsCalls.push(updates),
|
|
21
|
+
};
|
|
22
|
+
return { nav, navigateCalls, setSearchParamsCalls };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const host: ReturnHost = { screenId: "vehicle-list" };
|
|
26
|
+
const noOpenDrawer = (): void => {};
|
|
27
|
+
const noOnWriteSuccess = async (): Promise<void> => {};
|
|
28
|
+
|
|
29
|
+
describe("buildRecordActions — RowActionNavigate.tab", () => {
|
|
30
|
+
test("screen-target: tab lands in the same setSearchParams call as returnTo", () => {
|
|
31
|
+
const { nav, navigateCalls, setSearchParamsCalls } = recordingNav();
|
|
32
|
+
const action: RowActionNavigate = {
|
|
33
|
+
kind: "navigate",
|
|
34
|
+
id: "view-channels",
|
|
35
|
+
label: "app.actions.viewChannels",
|
|
36
|
+
screen: "vehicle-detail",
|
|
37
|
+
entityId: "id",
|
|
38
|
+
tab: "channels",
|
|
39
|
+
params: { pick: ["name"] },
|
|
40
|
+
};
|
|
41
|
+
const actions = buildRecordActions({
|
|
42
|
+
actions: [action],
|
|
43
|
+
record: { id: "abc", name: "Vehicle A" },
|
|
44
|
+
translate: (key) => key,
|
|
45
|
+
nav,
|
|
46
|
+
host,
|
|
47
|
+
dispatcher: undefined,
|
|
48
|
+
openDrawer: noOpenDrawer,
|
|
49
|
+
onWriteSuccess: noOnWriteSuccess,
|
|
50
|
+
});
|
|
51
|
+
actions?.[0]?.onPress();
|
|
52
|
+
|
|
53
|
+
expect(navigateCalls).toEqual([{ screenId: "vehicle-detail", entityId: "abc" }]);
|
|
54
|
+
expect(setSearchParamsCalls).toHaveLength(1);
|
|
55
|
+
expect(setSearchParamsCalls[0]).toMatchObject({
|
|
56
|
+
tab: "channels",
|
|
57
|
+
name: "Vehicle A",
|
|
58
|
+
[RETURN_TO_PARAM]: "vehicle-list",
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("entity-target: tab is included in the navigate search params", () => {
|
|
63
|
+
const { nav, setSearchParamsCalls } = recordingNav();
|
|
64
|
+
const action: RowActionNavigate = {
|
|
65
|
+
kind: "navigate",
|
|
66
|
+
id: "view-channels",
|
|
67
|
+
label: "app.actions.viewChannels",
|
|
68
|
+
entity: "vehicle",
|
|
69
|
+
entityId: "id",
|
|
70
|
+
tab: "channels",
|
|
71
|
+
};
|
|
72
|
+
const actions = buildRecordActions({
|
|
73
|
+
actions: [action],
|
|
74
|
+
record: { id: "abc" },
|
|
75
|
+
translate: (key) => key,
|
|
76
|
+
nav,
|
|
77
|
+
host,
|
|
78
|
+
dispatcher: undefined,
|
|
79
|
+
openDrawer: noOpenDrawer,
|
|
80
|
+
onWriteSuccess: noOnWriteSuccess,
|
|
81
|
+
});
|
|
82
|
+
actions?.[0]?.onPress();
|
|
83
|
+
|
|
84
|
+
expect(setSearchParamsCalls).toHaveLength(1);
|
|
85
|
+
expect(setSearchParamsCalls[0]).toEqual({ tab: "channels" });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("no tab set: search params carry no tab key", () => {
|
|
89
|
+
const { nav, setSearchParamsCalls } = recordingNav();
|
|
90
|
+
const action: RowActionNavigate = {
|
|
91
|
+
kind: "navigate",
|
|
92
|
+
id: "view-channels",
|
|
93
|
+
label: "app.actions.viewChannels",
|
|
94
|
+
screen: "vehicle-detail",
|
|
95
|
+
entityId: "id",
|
|
96
|
+
params: { pick: ["name"] },
|
|
97
|
+
};
|
|
98
|
+
const actions = buildRecordActions({
|
|
99
|
+
actions: [action],
|
|
100
|
+
record: { id: "abc", name: "Vehicle A" },
|
|
101
|
+
translate: (key) => key,
|
|
102
|
+
nav,
|
|
103
|
+
host,
|
|
104
|
+
dispatcher: undefined,
|
|
105
|
+
openDrawer: noOpenDrawer,
|
|
106
|
+
onWriteSuccess: noOnWriteSuccess,
|
|
107
|
+
});
|
|
108
|
+
actions?.[0]?.onPress();
|
|
109
|
+
|
|
110
|
+
expect(setSearchParamsCalls).toHaveLength(1);
|
|
111
|
+
expect(setSearchParamsCalls[0]).not.toHaveProperty("tab");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -103,9 +103,9 @@ import {
|
|
|
103
103
|
buildRecordActions,
|
|
104
104
|
evalRowExtractor,
|
|
105
105
|
isWriteHandlerRowAction,
|
|
106
|
+
navigateActionSearchParams,
|
|
106
107
|
refetchAfterWrite,
|
|
107
108
|
runProjectionRowNavigate,
|
|
108
|
-
stringifyNavParams,
|
|
109
109
|
} from "./row-actions";
|
|
110
110
|
import { findEditScreenFor, navigateTargetAllows, screenAccessAllows } from "./screen-access";
|
|
111
111
|
import { SecretMintBody } from "./secret-mint-body";
|
|
@@ -1952,10 +1952,9 @@ function EntityListBody({
|
|
|
1952
1952
|
const id = explicit ?? fallback ?? "";
|
|
1953
1953
|
if (id === "") return;
|
|
1954
1954
|
nav.navigate({ entity: action.entity, id });
|
|
1955
|
-
const params =
|
|
1956
|
-
action.params !== undefined ? evalRowExtractor(action.params, row.values) : undefined;
|
|
1955
|
+
const params = navigateActionSearchParams(action, row.values);
|
|
1957
1956
|
if (params !== undefined) {
|
|
1958
|
-
nav.setSearchParams(
|
|
1957
|
+
nav.setSearchParams(params);
|
|
1959
1958
|
}
|
|
1960
1959
|
} else if (action.screen !== undefined) {
|
|
1961
1960
|
// Default entityId für entityEdit-Targets: row["id"] wenn kein expliziter
|
|
@@ -1979,10 +1978,7 @@ function EntityListBody({
|
|
|
1979
1978
|
// kennt via URL nur Strings). Known-edge: zielt die Action auf den
|
|
1980
1979
|
// AKTUELLEN pathname, mergen die Params auf den alten ?-String (für
|
|
1981
1980
|
// Row-Actions praktisch nicht erreichbar, Pfad differiert).
|
|
1982
|
-
const params =
|
|
1983
|
-
action.params !== undefined
|
|
1984
|
-
? stringifyNavParams(evalRowExtractor(action.params, row.values))
|
|
1985
|
-
: undefined;
|
|
1981
|
+
const params = navigateActionSearchParams(action, row.values);
|
|
1986
1982
|
navigateWithReturnTo(nav, target, host, params);
|
|
1987
1983
|
}
|
|
1988
1984
|
},
|
|
@@ -2586,6 +2582,7 @@ function runMetricNavigate(
|
|
|
2586
2582
|
entity: navigate.entity,
|
|
2587
2583
|
...(navigate.entityId !== undefined && { entityId: navigate.entityId }),
|
|
2588
2584
|
...(navigate.params !== undefined && { params: navigate.params }),
|
|
2585
|
+
...(navigate.tab !== undefined && { tab: navigate.tab }),
|
|
2589
2586
|
}
|
|
2590
2587
|
: navigate.screen !== undefined
|
|
2591
2588
|
? {
|
|
@@ -2593,11 +2590,11 @@ function runMetricNavigate(
|
|
|
2593
2590
|
screen: navigate.screen,
|
|
2594
2591
|
...(navigate.entityId !== undefined && { entityId: navigate.entityId }),
|
|
2595
2592
|
...(navigate.params !== undefined && { params: navigate.params }),
|
|
2593
|
+
...(navigate.tab !== undefined && { tab: navigate.tab }),
|
|
2596
2594
|
}
|
|
2597
2595
|
: undefined;
|
|
2598
2596
|
if (action === undefined) return;
|
|
2599
2597
|
runProjectionRowNavigate(nav, action, { id: "", values: record }, host);
|
|
2600
|
-
if (navigate.tab !== undefined) nav.setSearchParams({ tab: navigate.tab });
|
|
2601
2598
|
}
|
|
2602
2599
|
|
|
2603
2600
|
// Absolute http(s) check for RecordHeaderSpec.subtitleHref — deliberately
|
package/src/app/row-actions.ts
CHANGED
|
@@ -71,6 +71,18 @@ export function stringifyNavParams(params: Record<string, unknown>): Record<stri
|
|
|
71
71
|
return out;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
export function navigateActionSearchParams(
|
|
75
|
+
action: RowActionNavigate,
|
|
76
|
+
values: Readonly<Record<string, unknown>>,
|
|
77
|
+
): Record<string, string | null> | undefined {
|
|
78
|
+
const params =
|
|
79
|
+
action.params !== undefined
|
|
80
|
+
? stringifyNavParams(evalRowExtractor(action.params, values))
|
|
81
|
+
: undefined;
|
|
82
|
+
if (action.tab === undefined) return params;
|
|
83
|
+
return { ...params, tab: action.tab };
|
|
84
|
+
}
|
|
85
|
+
|
|
74
86
|
export async function refetchAfterWrite(refetch: () => Promise<unknown>): Promise<void> {
|
|
75
87
|
await refetch().catch((err: unknown) => {
|
|
76
88
|
// biome-ignore lint/suspicious/noConsole: refetch must not poison the write-action error path
|
|
@@ -95,10 +107,9 @@ export function runProjectionRowNavigate(
|
|
|
95
107
|
// skip: no entityId column on this row — nothing to navigate to.
|
|
96
108
|
if (id === "") return;
|
|
97
109
|
nav.navigate({ entity: action.entity, id });
|
|
98
|
-
const params =
|
|
99
|
-
action.params !== undefined ? evalRowExtractor(action.params, row.values) : undefined;
|
|
110
|
+
const params = navigateActionSearchParams(action, row.values);
|
|
100
111
|
if (params !== undefined) {
|
|
101
|
-
nav.setSearchParams(
|
|
112
|
+
nav.setSearchParams(params);
|
|
102
113
|
}
|
|
103
114
|
} else if (action.screen !== undefined) {
|
|
104
115
|
const entityId =
|
|
@@ -107,10 +118,7 @@ export function runProjectionRowNavigate(
|
|
|
107
118
|
screenId: action.screen,
|
|
108
119
|
...(entityId !== undefined && entityId !== "" && { entityId }),
|
|
109
120
|
};
|
|
110
|
-
const params =
|
|
111
|
-
action.params !== undefined
|
|
112
|
-
? stringifyNavParams(evalRowExtractor(action.params, row.values))
|
|
113
|
-
: undefined;
|
|
121
|
+
const params = navigateActionSearchParams(action, row.values);
|
|
114
122
|
navigateWithReturnTo(nav, target, host, params);
|
|
115
123
|
}
|
|
116
124
|
// skip: neither entity nor screen set — the boot-validator rejects this
|
|
@@ -443,9 +451,8 @@ function buildNavigateRecordAction(
|
|
|
443
451
|
sameEntityScreenId,
|
|
444
452
|
} = options;
|
|
445
453
|
const runParams = (): void => {
|
|
446
|
-
const params =
|
|
447
|
-
|
|
448
|
-
if (params !== undefined) nav.setSearchParams(stringifyNavParams(params));
|
|
454
|
+
const params = navigateActionSearchParams(action, record);
|
|
455
|
+
if (params !== undefined) nav.setSearchParams(params);
|
|
449
456
|
};
|
|
450
457
|
if (action.entity !== undefined) {
|
|
451
458
|
const targetEntity = action.entity;
|
|
@@ -482,10 +489,7 @@ function buildNavigateRecordAction(
|
|
|
482
489
|
screenId: targetScreen,
|
|
483
490
|
...(navEntityId !== undefined && navEntityId !== "" && { entityId: navEntityId }),
|
|
484
491
|
};
|
|
485
|
-
const params =
|
|
486
|
-
action.params !== undefined
|
|
487
|
-
? stringifyNavParams(evalRowExtractor(action.params, record))
|
|
488
|
-
: undefined;
|
|
492
|
+
const params = navigateActionSearchParams(action, record);
|
|
489
493
|
navigateWithReturnTo(nav, target, host, params);
|
|
490
494
|
},
|
|
491
495
|
};
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.310.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Navigate actions merge `tab` into the search params next to returnTo",
|
|
6
|
+
"detail": "Row, record and section navigate actions set `tab` in the same\nsetSearchParams call as their `params` and `returnTo`, so the back\nnavigation survives. Metric navigate with screen/entity plus tab now also\nlands in that single call instead of a second setSearchParams after it."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.308.0",
|
|
4
10
|
"type": "improvement",
|