@cosmicdrift/kumiko-headless 0.185.0 → 0.186.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-headless",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.186.1",
|
|
4
4
|
"description": "Headless UI logic for Kumiko — Dispatcher contract, Form-Controller, View-Model, Nav-Resolver. Plattform- und React-frei; jeder Renderer (renderer, renderer-web, renderer-native, …) komponiert darauf.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
39
|
+
"@cosmicdrift/kumiko-framework": "0.186.1",
|
|
40
40
|
"temporal-polyfill": "^0.3.2",
|
|
41
41
|
"zod": "^4.4.3"
|
|
42
42
|
},
|
|
@@ -147,6 +147,39 @@ describe("createFormController — submit()", () => {
|
|
|
147
147
|
expect(disp.writeSpy).toHaveBeenCalledWith("app:write:task:update", { title: "world" });
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
+
test("payloadMode: 'values' — strips an untouched optional field seeded with \"\"", async () => {
|
|
151
|
+
// `dueDate` was never touched by the user; buildInitialValues seeded it
|
|
152
|
+
// with "" for the controlled input. A `.optional()` server schema
|
|
153
|
+
// rejects "" but accepts a missing key, so the key must be dropped
|
|
154
|
+
// entirely — not sent as `dueDate: undefined`.
|
|
155
|
+
const disp = makeDispatcher();
|
|
156
|
+
const form = createFormController({
|
|
157
|
+
initial: { title: "hello", dueDate: "" },
|
|
158
|
+
submit: { dispatcher: disp, type: "app:write:task:create" },
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await form.submit();
|
|
162
|
+
|
|
163
|
+
const call = disp.writeSpy.mock.calls[0]?.[1] as Record<string, unknown>;
|
|
164
|
+
expect("dueDate" in call).toBe(false);
|
|
165
|
+
expect(call["title"]).toBe("hello");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("payloadMode: 'values' — keeps an explicitly-cleared field that started non-empty", async () => {
|
|
169
|
+
// `note` had a real value initially; the user cleared it via setField.
|
|
170
|
+
// That's a genuine change, not an untouched seed — it must survive.
|
|
171
|
+
const disp = makeDispatcher();
|
|
172
|
+
const form = createFormController({
|
|
173
|
+
initial: { note: "x" },
|
|
174
|
+
submit: { dispatcher: disp, type: "app:write:task:create" },
|
|
175
|
+
});
|
|
176
|
+
form.setField("note", "");
|
|
177
|
+
|
|
178
|
+
await form.submit();
|
|
179
|
+
|
|
180
|
+
expect(disp.writeSpy).toHaveBeenCalledWith("app:write:task:create", { note: "" });
|
|
181
|
+
});
|
|
182
|
+
|
|
150
183
|
test("stale-submit race: edits during the in-flight write stay dirty after success", async () => {
|
|
151
184
|
// User submits "hello", the network takes 50ms. During those 50ms the
|
|
152
185
|
// user types "world" into the same field. The server sees "hello"
|
|
@@ -74,6 +74,24 @@ function valuesDiff<TValues extends FormValues>(
|
|
|
74
74
|
return out;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
// buildInitialValues seeds untouched optional fields with "" for controlled
|
|
78
|
+
// inputs; a `.optional()` server schema accepts undefined but not "".
|
|
79
|
+
function stripUntouchedEmptyStrings<TValues extends FormValues>(
|
|
80
|
+
values: TValues,
|
|
81
|
+
initial: TValues,
|
|
82
|
+
): TValues {
|
|
83
|
+
const v = values as Record<string, unknown>; // @cast-boundary form-values
|
|
84
|
+
const ini = initial as Record<string, unknown>; // @cast-boundary form-values
|
|
85
|
+
let out: Record<string, unknown> | undefined;
|
|
86
|
+
for (const key of Object.keys(v)) {
|
|
87
|
+
if (v[key] === "" && Object.is(v[key], ini[key])) {
|
|
88
|
+
out ??= { ...v };
|
|
89
|
+
delete out[key];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return (out ?? v) as TValues;
|
|
93
|
+
}
|
|
94
|
+
|
|
77
95
|
// Shallow-freeze so accidental mutations on the snapshot (e.g. a test
|
|
78
96
|
// writing `snapshot.values.title = "x"`) throw in strict mode instead of
|
|
79
97
|
// silently diverging from the controller's internal state. Deep-freeze
|
|
@@ -304,7 +322,7 @@ export function createFormController<TValues extends FormValues, TCtx = unknown>
|
|
|
304
322
|
}
|
|
305
323
|
payload = submittedSnapshot.changes;
|
|
306
324
|
} else {
|
|
307
|
-
payload = submittedValues;
|
|
325
|
+
payload = stripUntouchedEmptyStrings(submittedValues, submittedSnapshot.initial);
|
|
308
326
|
}
|
|
309
327
|
|
|
310
328
|
const runWrite = async (): Promise<SubmitResult<TData>> => {
|