@cosmicdrift/kumiko-renderer 0.220.0 → 0.220.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",
|
|
3
|
-
"version": "0.220.
|
|
3
|
+
"version": "0.220.1",
|
|
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.220.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.220.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.220.1",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.220.1",
|
|
20
20
|
"react": "^19.2.6",
|
|
21
21
|
"temporal-polyfill": "^0.3.2",
|
|
22
22
|
"zod": "^4.4.3"
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@testing-library/react": "^16.3.2",
|
|
26
26
|
"@types/react": "^19.2.14",
|
|
27
27
|
"jsdom": "^29.1.1",
|
|
28
|
-
"@cosmicdrift/kumiko-locale-de": "0.220.
|
|
28
|
+
"@cosmicdrift/kumiko-locale-de": "0.220.1"
|
|
29
29
|
},
|
|
30
30
|
"repository": {
|
|
31
31
|
"type": "git",
|
|
@@ -77,4 +77,27 @@ describe("mergeSearchParamsIntoInitial", () => {
|
|
|
77
77
|
const result = mergeSearchParamsIntoInitial(fields, { ownerId: "user-123" });
|
|
78
78
|
expect(result["ownerId"]).toBe("user-123");
|
|
79
79
|
});
|
|
80
|
+
|
|
81
|
+
test("multiSelect coerces comma-separated searchParam to string[]", () => {
|
|
82
|
+
const fields: Record<string, FieldDef> = { roles: { type: "multiSelect" } };
|
|
83
|
+
expect(mergeSearchParamsIntoInitial(fields, { roles: "TenantAdmin" })["roles"]).toEqual([
|
|
84
|
+
"TenantAdmin",
|
|
85
|
+
]);
|
|
86
|
+
expect(mergeSearchParamsIntoInitial(fields, { roles: "Admin,User" })["roles"]).toEqual([
|
|
87
|
+
"Admin",
|
|
88
|
+
"User",
|
|
89
|
+
]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("multiSelect coerces JSON-array searchParam to string[]", () => {
|
|
93
|
+
const fields: Record<string, FieldDef> = { roles: { type: "multiSelect" } };
|
|
94
|
+
expect(
|
|
95
|
+
mergeSearchParamsIntoInitial(fields, { roles: JSON.stringify(["Admin", "Editor"]) })["roles"],
|
|
96
|
+
).toEqual(["Admin", "Editor"]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("multiSelect defaults to [] when unset", () => {
|
|
100
|
+
const fields: Record<string, FieldDef> = { roles: { type: "multiSelect" } };
|
|
101
|
+
expect(mergeSearchParamsIntoInitial(fields, {})["roles"]).toEqual([]);
|
|
102
|
+
});
|
|
80
103
|
});
|
|
@@ -341,7 +341,13 @@ export function buildInitialValues(
|
|
|
341
341
|
continue;
|
|
342
342
|
}
|
|
343
343
|
out[name] =
|
|
344
|
-
shape.type === "boolean"
|
|
344
|
+
shape.type === "boolean"
|
|
345
|
+
? false
|
|
346
|
+
: shape.type === "number" || shape.type === "money"
|
|
347
|
+
? 0
|
|
348
|
+
: shape.type === "multiSelect"
|
|
349
|
+
? []
|
|
350
|
+
: "";
|
|
345
351
|
}
|
|
346
352
|
return out;
|
|
347
353
|
}
|
|
@@ -372,6 +378,26 @@ export function mergeSearchParamsIntoInitial(
|
|
|
372
378
|
: parsed;
|
|
373
379
|
} else if (shape.type === "boolean") {
|
|
374
380
|
merged[name] = raw === "true";
|
|
381
|
+
} else if (shape.type === "multiSelect") {
|
|
382
|
+
// Row-action navigate stringifies arrays via String(arr) → "a,b" (or
|
|
383
|
+
// JSON when the navigate helper JSON.stringifies). Accept both so
|
|
384
|
+
// member-roles-edit can prefill from ?roles=TenantAdmin.
|
|
385
|
+
if (raw.startsWith("[")) {
|
|
386
|
+
try {
|
|
387
|
+
const parsed: unknown = JSON.parse(raw);
|
|
388
|
+
merged[name] = Array.isArray(parsed) ? parsed.map((v) => String(v)) : defaults[name];
|
|
389
|
+
} catch {
|
|
390
|
+
merged[name] = defaults[name];
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
merged[name] =
|
|
394
|
+
raw === ""
|
|
395
|
+
? []
|
|
396
|
+
: raw
|
|
397
|
+
.split(",")
|
|
398
|
+
.map((s) => s.trim())
|
|
399
|
+
.filter(Boolean);
|
|
400
|
+
}
|
|
375
401
|
} else {
|
|
376
402
|
merged[name] = raw;
|
|
377
403
|
}
|
|
@@ -1343,7 +1369,8 @@ function EntityListBody({
|
|
|
1343
1369
|
// Row-Actions praktisch nicht erreichbar, Pfad differiert).
|
|
1344
1370
|
const stringified: Record<string, string | null> = {};
|
|
1345
1371
|
for (const [k, v] of Object.entries(params)) {
|
|
1346
|
-
stringified[k] =
|
|
1372
|
+
stringified[k] =
|
|
1373
|
+
v === null || v === undefined ? null : Array.isArray(v) ? JSON.stringify(v) : String(v);
|
|
1347
1374
|
}
|
|
1348
1375
|
nav.setSearchParams(stringified);
|
|
1349
1376
|
}
|
|
@@ -1705,7 +1732,8 @@ function ProjectionListBody({
|
|
|
1705
1732
|
if (params !== undefined) {
|
|
1706
1733
|
const stringified: Record<string, string | null> = {};
|
|
1707
1734
|
for (const [k, v] of Object.entries(params)) {
|
|
1708
|
-
stringified[k] =
|
|
1735
|
+
stringified[k] =
|
|
1736
|
+
v === null || v === undefined ? null : Array.isArray(v) ? JSON.stringify(v) : String(v);
|
|
1709
1737
|
}
|
|
1710
1738
|
nav.setSearchParams(stringified);
|
|
1711
1739
|
}
|
|
@@ -2005,7 +2033,12 @@ function ProjectionDetailBody({
|
|
|
2005
2033
|
if (params !== undefined) {
|
|
2006
2034
|
const stringified: Record<string, string | null> = {};
|
|
2007
2035
|
for (const [k, v] of Object.entries(params)) {
|
|
2008
|
-
stringified[k] =
|
|
2036
|
+
stringified[k] =
|
|
2037
|
+
v === null || v === undefined
|
|
2038
|
+
? null
|
|
2039
|
+
: Array.isArray(v)
|
|
2040
|
+
? JSON.stringify(v)
|
|
2041
|
+
: String(v);
|
|
2009
2042
|
}
|
|
2010
2043
|
nav.setSearchParams(stringified);
|
|
2011
2044
|
}
|
|
@@ -138,6 +138,7 @@ describe("RenderEditActionButton", () => {
|
|
|
138
138
|
|
|
139
139
|
fireEvent.click(rtlScreen.getByTestId("render-edit-action-boom"));
|
|
140
140
|
await waitFor(() => expect(errors).toContain("action exploded"));
|
|
141
|
+
// Cleared at the start of trigger, then set on failure.
|
|
141
142
|
expect(errors[0]).toBeNull();
|
|
142
143
|
});
|
|
143
144
|
|
package/src/hooks/use-form.ts
CHANGED
|
@@ -23,20 +23,10 @@ import { useOptionalDispatcher } from "../context/dispatcher-context";
|
|
|
23
23
|
// });
|
|
24
24
|
// and the hook wires the ambient DispatcherProvider's dispatcher in.
|
|
25
25
|
|
|
26
|
-
export type UseFormOptions<TValues extends FormValues, TCtx = unknown> =
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// `submit` here takes everything the ui-core SubmitConfig takes
|
|
31
|
-
// EXCEPT dispatcher — that comes from context. Passing an explicit
|
|
32
|
-
// dispatcher here (e.g. from a test) overrides the context one.
|
|
33
|
-
readonly submit?: Omit<
|
|
34
|
-
NonNullable<FormControllerOptions<TValues, TCtx>["submit"]>,
|
|
35
|
-
"dispatcher"
|
|
36
|
-
> & {
|
|
37
|
-
readonly dispatcher?: NonNullable<FormControllerOptions<TValues, TCtx>["submit"]>["dispatcher"];
|
|
38
|
-
};
|
|
39
|
-
};
|
|
26
|
+
export type UseFormOptions<TValues extends FormValues, TCtx = unknown> = FormControllerOptions<
|
|
27
|
+
TValues,
|
|
28
|
+
TCtx
|
|
29
|
+
>;
|
|
40
30
|
|
|
41
31
|
export type UseFormResult<TValues extends FormValues> = {
|
|
42
32
|
readonly controller: FormController<TValues>;
|
|
@@ -63,10 +53,7 @@ export function useForm<TValues extends FormValues, TCtx = unknown>(
|
|
|
63
53
|
// in-flight state and dirty edits on every parent re-render.
|
|
64
54
|
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional — controller is lifetime-scoped, not render-scoped
|
|
65
55
|
const controller = useMemo<FormController<TValues>>(() => {
|
|
66
|
-
// Default
|
|
67
|
-
// a fully-populated SubmitConfig. The input signature lets the
|
|
68
|
-
// caller omit `dispatcher`; the output shape createFormController
|
|
69
|
-
// expects requires it.
|
|
56
|
+
// Default submit.dispatcher to the ambient one when omitted.
|
|
70
57
|
type FullOptions = FormControllerOptions<TValues, TCtx>;
|
|
71
58
|
const rawSubmit = options.submit;
|
|
72
59
|
const submitConfig: FullOptions["submit"] = rawSubmit
|