@cosmicdrift/kumiko-renderer 0.281.0 → 0.283.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.283.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.283.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.283.0",
|
|
20
|
+
"@cosmicdrift/kumiko-types": "0.283.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.283.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import type { ProjectionDetailScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
synthesizeProjectionDetailEntity,
|
|
5
|
+
synthesizeProjectionDetailScreen,
|
|
6
|
+
} from "../projection-detail-shim";
|
|
4
7
|
|
|
5
8
|
// Regression for the writeForm extension (fw editable-detail-screens): the
|
|
6
9
|
// hard readOnly:true enforcement in synthesizeProjectionDetailScreen must
|
|
@@ -60,3 +63,126 @@ describe("synthesizeProjectionDetailScreen", () => {
|
|
|
60
63
|
expect("description" in withoutDescription).toBe(false);
|
|
61
64
|
});
|
|
62
65
|
});
|
|
66
|
+
|
|
67
|
+
// Regression for fw#2982: a section that puts its fields in `groups`
|
|
68
|
+
// instead of `fields` used to vanish entirely — synthesizeProjectionDetailEntity
|
|
69
|
+
// never registered the field names, so computeEditViewModel threw "references
|
|
70
|
+
// unknown field" for every one of them, and synthesizeProjectionDetailScreen
|
|
71
|
+
// let a group field's own readOnly value pass through unforced.
|
|
72
|
+
describe("groups-sections (fw#2982)", () => {
|
|
73
|
+
const groupsOnlyScreen: ProjectionDetailScreenDefinition = {
|
|
74
|
+
id: "contact-detail",
|
|
75
|
+
type: "projectionDetail",
|
|
76
|
+
query: "contacts:query:contact:detail",
|
|
77
|
+
layout: {
|
|
78
|
+
sections: [
|
|
79
|
+
{
|
|
80
|
+
title: "Stammdaten",
|
|
81
|
+
fields: [],
|
|
82
|
+
groups: [
|
|
83
|
+
{
|
|
84
|
+
title: "Kontakt",
|
|
85
|
+
fields: [{ field: "contactKind", readOnly: false }, "email"],
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
test("synthesizeProjectionDetailEntity registers every field declared only through groups", () => {
|
|
94
|
+
const entity = synthesizeProjectionDetailEntity(groupsOnlyScreen.layout);
|
|
95
|
+
expect(entity.fields["contactKind"]).toEqual({ type: "text" });
|
|
96
|
+
expect(entity.fields["email"]).toEqual({ type: "text" });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("synthesizeProjectionDetailScreen forces readOnly:true on a group field even when the author set readOnly:false", () => {
|
|
100
|
+
const result = synthesizeProjectionDetailScreen(groupsOnlyScreen);
|
|
101
|
+
const [section] = result.layout.sections;
|
|
102
|
+
if (section === undefined || !("fields" in section) || "kind" in section) {
|
|
103
|
+
throw new Error("expected the section to stay a plain fields section");
|
|
104
|
+
}
|
|
105
|
+
if (!("groups" in section) || section.groups === undefined) {
|
|
106
|
+
throw new Error("expected the section to keep its groups");
|
|
107
|
+
}
|
|
108
|
+
expect(section.groups[0]?.fields).toEqual([
|
|
109
|
+
{ field: "contactKind", readOnly: true },
|
|
110
|
+
{ field: "email", readOnly: true },
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("a screen mixing a plain fields section and a groups section resolves both, for both functions", () => {
|
|
115
|
+
const mixedScreen: ProjectionDetailScreenDefinition = {
|
|
116
|
+
id: "deposit-detail",
|
|
117
|
+
type: "projectionDetail",
|
|
118
|
+
query: "deposits:query:deposit:detail",
|
|
119
|
+
layout: {
|
|
120
|
+
sections: [
|
|
121
|
+
{ title: "Basics", fields: [{ field: "amount", readOnly: false }] },
|
|
122
|
+
{
|
|
123
|
+
title: "Parties",
|
|
124
|
+
fields: [],
|
|
125
|
+
groups: [{ title: "Tenant", fields: ["tenantName"] }],
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const entity = synthesizeProjectionDetailEntity(mixedScreen.layout);
|
|
132
|
+
expect(entity.fields["amount"]).toEqual({ type: "text" });
|
|
133
|
+
expect(entity.fields["tenantName"]).toEqual({ type: "text" });
|
|
134
|
+
|
|
135
|
+
const result = synthesizeProjectionDetailScreen(mixedScreen);
|
|
136
|
+
const [fieldsSection, groupsSection] = result.layout.sections;
|
|
137
|
+
if (fieldsSection === undefined || !("fields" in fieldsSection) || "kind" in fieldsSection) {
|
|
138
|
+
throw new Error("expected the first section to stay a plain fields section");
|
|
139
|
+
}
|
|
140
|
+
expect(fieldsSection.fields[0]).toMatchObject({ field: "amount", readOnly: true });
|
|
141
|
+
|
|
142
|
+
if (
|
|
143
|
+
groupsSection === undefined ||
|
|
144
|
+
!("groups" in groupsSection) ||
|
|
145
|
+
groupsSection.groups === undefined
|
|
146
|
+
) {
|
|
147
|
+
throw new Error("expected the second section to keep its groups");
|
|
148
|
+
}
|
|
149
|
+
expect(groupsSection.groups[0]?.fields).toEqual([{ field: "tenantName", readOnly: true }]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// The boot-validator rejects fields+groups both non-empty on one section at
|
|
153
|
+
// boot time, so an author can never actually ship this shape — but the shim
|
|
154
|
+
// is not itself a validation pass, and this pins the union, not the
|
|
155
|
+
// either/or a naive `groups !== undefined ? grouped : fields` would take.
|
|
156
|
+
test("a single section carrying both fields and groups resolves both, for both functions", () => {
|
|
157
|
+
const bothScreen: ProjectionDetailScreenDefinition = {
|
|
158
|
+
id: "invoice-detail",
|
|
159
|
+
type: "projectionDetail",
|
|
160
|
+
query: "invoices:query:invoice:detail",
|
|
161
|
+
layout: {
|
|
162
|
+
sections: [
|
|
163
|
+
{
|
|
164
|
+
title: "Mixed",
|
|
165
|
+
fields: [{ field: "amount", readOnly: false }],
|
|
166
|
+
groups: [{ title: "Tenant", fields: ["tenantName"] }],
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const entity = synthesizeProjectionDetailEntity(bothScreen.layout);
|
|
173
|
+
expect(entity.fields["amount"]).toEqual({ type: "text" });
|
|
174
|
+
expect(entity.fields["tenantName"]).toEqual({ type: "text" });
|
|
175
|
+
|
|
176
|
+
const result = synthesizeProjectionDetailScreen(bothScreen);
|
|
177
|
+
const [section] = result.layout.sections;
|
|
178
|
+
if (section === undefined || !("fields" in section) || "kind" in section) {
|
|
179
|
+
throw new Error("expected the section to stay a plain fields section");
|
|
180
|
+
}
|
|
181
|
+
expect(section.fields[0]).toMatchObject({ field: "amount", readOnly: true });
|
|
182
|
+
|
|
183
|
+
if (!("groups" in section) || section.groups === undefined) {
|
|
184
|
+
throw new Error("expected the section to keep its groups");
|
|
185
|
+
}
|
|
186
|
+
expect(section.groups[0]?.fields).toEqual([{ field: "tenantName", readOnly: true }]);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
// Komponente.
|
|
22
22
|
|
|
23
23
|
import type {
|
|
24
|
+
EditFieldSpec,
|
|
25
|
+
EditFieldsSection,
|
|
24
26
|
EditLayout,
|
|
25
27
|
EntityDefinition,
|
|
26
28
|
EntityEditScreenDefinition,
|
|
@@ -32,6 +34,16 @@ import {
|
|
|
32
34
|
PROJECTION_DETAIL_ENTITY as PROJECTION_DETAIL_PSEUDO_ENTITY,
|
|
33
35
|
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
34
36
|
|
|
37
|
+
// The boot-validator treats `fields`/`groups` as mutually exclusive per
|
|
38
|
+
// section, but this reads both anyway: an extra name the current section
|
|
39
|
+
// shape can never carry is harmless, while a name computeEditViewModel does
|
|
40
|
+
// reference and this shim omitted throws downstream ("references unknown
|
|
41
|
+
// field ... on entity"). Union is strictly safer than picking one branch.
|
|
42
|
+
function allFieldSpecs(section: EditFieldsSection): readonly EditFieldSpec[] {
|
|
43
|
+
const grouped = section.groups?.flatMap((group) => group.fields) ?? [];
|
|
44
|
+
return [...section.fields, ...grouped];
|
|
45
|
+
}
|
|
46
|
+
|
|
35
47
|
/** Minimale EntityDefinition aus den Layout-Feldern: jedes Feld ein Text-
|
|
36
48
|
* Feld — computeEditViewModel liest nur `fields[<f>].type`, Text reicht für
|
|
37
49
|
* eine reine Anzeige (kein Select/Number-spezifisches Rendering nötig). */
|
|
@@ -40,7 +52,7 @@ export function synthesizeProjectionDetailEntity(layout: EditLayout): EntityDefi
|
|
|
40
52
|
for (const section of layout.sections) {
|
|
41
53
|
// relatedList and extension sections carry no `fields` to synthesize.
|
|
42
54
|
if (!isFieldsEditSection(section)) continue;
|
|
43
|
-
for (const spec of section
|
|
55
|
+
for (const spec of allFieldSpecs(section)) {
|
|
44
56
|
fields[normalizeEditField(spec).field] = { type: "text" };
|
|
45
57
|
}
|
|
46
58
|
}
|
|
@@ -52,12 +64,19 @@ export function synthesizeProjectionDetailEntity(layout: EditLayout): EntityDefi
|
|
|
52
64
|
export function synthesizeProjectionDetailScreen(
|
|
53
65
|
screen: ProjectionDetailScreenDefinition,
|
|
54
66
|
): EntityEditScreenDefinition {
|
|
67
|
+
const forceReadOnly = (spec: EditFieldSpec) => ({ ...normalizeEditField(spec), readOnly: true });
|
|
55
68
|
const sections = screen.layout.sections.map((section) => {
|
|
56
69
|
// relatedList and extension sections pass through unchanged.
|
|
57
70
|
if (!isFieldsEditSection(section)) return section;
|
|
58
71
|
return {
|
|
59
72
|
...section,
|
|
60
|
-
fields: section.fields.map(
|
|
73
|
+
fields: section.fields.map(forceReadOnly),
|
|
74
|
+
...(section.groups !== undefined && {
|
|
75
|
+
groups: section.groups.map((group) => ({
|
|
76
|
+
...group,
|
|
77
|
+
fields: group.fields.map(forceReadOnly),
|
|
78
|
+
})),
|
|
79
|
+
}),
|
|
61
80
|
};
|
|
62
81
|
});
|
|
63
82
|
return {
|
package/src/changes.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.283.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Fix projectionDetail detail screens dropping fields declared through groups",
|
|
6
|
+
"detail": "synthesizeProjectionDetailEntity/synthesizeProjectionDetailScreen only read section.fields, so a section that put its fields in groups instead lost them entirely, and any group field's own readOnly:false passed through unforced. Both helpers now read fields and groups together, and force readOnly:true on both."
|
|
7
|
+
}
|
|
8
|
+
]
|