@cosmicdrift/kumiko-renderer 0.325.1 → 0.325.2
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.325.
|
|
3
|
+
"version": "0.325.2",
|
|
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.325.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.325.
|
|
20
|
-
"@cosmicdrift/kumiko-types": "0.325.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.325.2",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.325.2",
|
|
20
|
+
"@cosmicdrift/kumiko-types": "0.325.2",
|
|
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.325.
|
|
31
|
+
"@cosmicdrift/kumiko-locale-de": "0.325.2"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.325.2",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Server validation issues without a rendered field keep the error banner",
|
|
6
|
+
"detail": "RenderEdit suppressed the banner as soon as the server returned any field issue, even one\nthat no rendered field can display (`version`, `id`, a root-level refine, a condition-hidden\nfield), so the save failed silently. The banner now stays whenever at least one issue has no\nvisible field; tabs/wizard layouts still jump to the step holding the first matched field."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.325.1",
|
|
4
10
|
"type": "fix",
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
import {
|
|
9
9
|
filterEditSections,
|
|
10
10
|
hasEditableSection,
|
|
11
|
+
hasIssueWithoutRenderedField,
|
|
11
12
|
resolveExtensionEntityId,
|
|
12
13
|
shouldNotifyCaller,
|
|
13
14
|
} from "../render-edit-logic";
|
|
@@ -248,3 +249,66 @@ describe("filterEditSections", () => {
|
|
|
248
249
|
expect(result).toEqual([extensionSection]);
|
|
249
250
|
});
|
|
250
251
|
});
|
|
252
|
+
|
|
253
|
+
describe("hasIssueWithoutRenderedField", () => {
|
|
254
|
+
test("issue path matches a rendered field → false", () => {
|
|
255
|
+
expect(hasIssueWithoutRenderedField(["title"], [namedFieldsSection("title", "body")])).toBe(
|
|
256
|
+
false,
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// Root-level refine issues (`version`/`id`/`changes`) never match a rendered
|
|
261
|
+
// field name — the banner is the only place that can show them.
|
|
262
|
+
test("root-level refine path (version) → true", () => {
|
|
263
|
+
expect(hasIssueWithoutRenderedField(["version"], [namedFieldsSection("title")])).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("(root) path → true", () => {
|
|
267
|
+
expect(hasIssueWithoutRenderedField(["(root)"], [namedFieldsSection("title")])).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("path matches a field hidden by its own condition → true", () => {
|
|
271
|
+
const section: EditSectionViewModel = {
|
|
272
|
+
kind: "fields",
|
|
273
|
+
columns: 1,
|
|
274
|
+
visible: true,
|
|
275
|
+
fields: [
|
|
276
|
+
{
|
|
277
|
+
field: "a",
|
|
278
|
+
label: "A",
|
|
279
|
+
type: "text",
|
|
280
|
+
value: "",
|
|
281
|
+
visible: true,
|
|
282
|
+
readOnly: false,
|
|
283
|
+
required: false,
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
field: "b",
|
|
287
|
+
label: "B",
|
|
288
|
+
type: "text",
|
|
289
|
+
value: "",
|
|
290
|
+
visible: false,
|
|
291
|
+
readOnly: false,
|
|
292
|
+
required: false,
|
|
293
|
+
},
|
|
294
|
+
],
|
|
295
|
+
};
|
|
296
|
+
expect(hasIssueWithoutRenderedField(["b"], [section])).toBe(true);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("dotted path (tasks.2.title) matches on its first segment → false", () => {
|
|
300
|
+
expect(hasIssueWithoutRenderedField(["tasks.2.title"], [namedFieldsSection("tasks")])).toBe(
|
|
301
|
+
false,
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("mixed: one matched, one unmatched → true", () => {
|
|
306
|
+
expect(hasIssueWithoutRenderedField(["title", "version"], [namedFieldsSection("title")])).toBe(
|
|
307
|
+
true,
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("empty issue list → true (single-section path always shows the banner then)", () => {
|
|
312
|
+
expect(hasIssueWithoutRenderedField([], [namedFieldsSection("title")])).toBe(true);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
@@ -41,6 +41,22 @@ export function shouldNotifyCaller(
|
|
|
41
41
|
return !(result.isSuccess && !extensionsPersisted);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// Matched on the first path segment (embedded-list rows are dotted, e.g. `tasks.2.title`).
|
|
45
|
+
// No issues at all counts as unmatched, so the caller keeps the banner.
|
|
46
|
+
export function hasIssueWithoutRenderedField(
|
|
47
|
+
issuePaths: readonly string[],
|
|
48
|
+
sections: readonly EditSectionViewModel[],
|
|
49
|
+
): boolean {
|
|
50
|
+
if (issuePaths.length === 0) return true;
|
|
51
|
+
const renderedFields = new Set(
|
|
52
|
+
sections
|
|
53
|
+
.filter((s) => s.kind === "fields" && (s.visible || s.fields.length === 0))
|
|
54
|
+
.flatMap((s) => (s.kind === "fields" ? s.fields.filter((f) => f.visible) : []))
|
|
55
|
+
.map((f) => f.field),
|
|
56
|
+
);
|
|
57
|
+
return issuePaths.some((path) => !renderedFields.has(path.split(".")[0] ?? path));
|
|
58
|
+
}
|
|
59
|
+
|
|
44
60
|
// Extension, relatedList and writeForm sections skip the `fields` filter
|
|
45
61
|
// (writeForm's fields belong to its own independent form, not the host's);
|
|
46
62
|
// a `fields` section left with zero fields after filtering is dropped, not
|
|
@@ -48,6 +48,7 @@ import { RelatedListSection } from "./related-list-section";
|
|
|
48
48
|
import {
|
|
49
49
|
filterEditSections,
|
|
50
50
|
hasEditableSection,
|
|
51
|
+
hasIssueWithoutRenderedField,
|
|
51
52
|
resolveExtensionEntityId,
|
|
52
53
|
shouldNotifyCaller,
|
|
53
54
|
} from "./render-edit-logic";
|
|
@@ -1029,29 +1030,21 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
|
|
|
1029
1030
|
}
|
|
1030
1031
|
} else {
|
|
1031
1032
|
const fieldIssues = result.error.details?.fields ?? [];
|
|
1033
|
+
const issuePaths = fieldIssues.map((i) => i.path);
|
|
1032
1034
|
// A server field error on a step the form isn't currently showing is
|
|
1033
1035
|
// otherwise invisible — jump to the first step that contains one of
|
|
1034
1036
|
// the errored fields. For tabs this is what keeps the submit from
|
|
1035
1037
|
// blocking in silence, the failure mode the boot-validator used to
|
|
1036
|
-
// prevent by rejecting the layout outright (fw#3134).
|
|
1037
|
-
//
|
|
1038
|
-
//
|
|
1039
|
-
// field itself already shows the error inline).
|
|
1038
|
+
// prevent by rejecting the layout outright (fw#3134). An issue no
|
|
1039
|
+
// rendered field can show (`version`, `id`, a root-level refine, a
|
|
1040
|
+
// hidden field) keeps the banner, jump or not.
|
|
1040
1041
|
if (isStepped) {
|
|
1041
|
-
const step = findFirstErroringStep(
|
|
1042
|
-
if (step !== undefined)
|
|
1043
|
-
setRawStep(step);
|
|
1044
|
-
setFormError(null);
|
|
1045
|
-
} else {
|
|
1046
|
-
// No rendered step contains any of the errored fields (a
|
|
1047
|
-
// root-level issue, or a field outside every step) — nothing
|
|
1048
|
-
// will show this error inline, so the banner must not be
|
|
1049
|
-
// suppressed.
|
|
1050
|
-
setFormError(result.error);
|
|
1051
|
-
}
|
|
1052
|
-
} else {
|
|
1053
|
-
setFormError(fieldIssues.length === 0 ? result.error : null);
|
|
1042
|
+
const step = findFirstErroringStep(issuePaths);
|
|
1043
|
+
if (step !== undefined) setRawStep(step);
|
|
1054
1044
|
}
|
|
1045
|
+
setFormError(
|
|
1046
|
+
hasIssueWithoutRenderedField(issuePaths, filteredSections) ? result.error : null,
|
|
1047
|
+
);
|
|
1055
1048
|
}
|
|
1056
1049
|
// skip: on entity-success-but-extension-failure the extension-error
|
|
1057
1050
|
// banner is showing — don't notify (the caller navigates away on success
|