@cosmicdrift/kumiko-renderer 0.251.0 → 0.252.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 +4 -4
- package/src/app/__tests__/config-edit-shim.test.ts +17 -0
- package/src/app/config-edit-shim.ts +1 -0
- package/src/app/secrets-edit-body.tsx +3 -0
- package/src/components/__tests__/write-form-section.test.tsx +101 -4
- package/src/components/render-edit.tsx +1 -2
- package/src/components/write-form-section.tsx +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.252.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.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.252.1",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.252.1",
|
|
20
20
|
"react": "^19.2.6",
|
|
21
21
|
"temporal-polyfill": "^0.3.2",
|
|
22
22
|
"zod": "^4.4.3"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"@types/react-dom": "^19.2.3",
|
|
28
28
|
"jsdom": "^29.1.1",
|
|
29
29
|
"react-dom": "^19.2.6",
|
|
30
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
30
|
+
"@cosmicdrift/kumiko-locale-de": "0.252.1"
|
|
31
31
|
},
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|
|
@@ -37,4 +37,21 @@ describe("synthesizeConfigEditScreen", () => {
|
|
|
37
37
|
expect(result.fieldLabels).toEqual({ apiKey: "config.apiKey" });
|
|
38
38
|
expect(result.access).toEqual({ roles: ["Admin"] });
|
|
39
39
|
});
|
|
40
|
+
|
|
41
|
+
test("carries description through so RenderEdit can render it as the form subtitle", () => {
|
|
42
|
+
const withDescription = {
|
|
43
|
+
id: "settings",
|
|
44
|
+
layout: { sections: [] },
|
|
45
|
+
description: "These settings apply to every project in the workspace.",
|
|
46
|
+
} as unknown as ConfigEditScreenDefinition;
|
|
47
|
+
expect(synthesizeConfigEditScreen(withDescription).description).toBe(
|
|
48
|
+
"These settings apply to every project in the workspace.",
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const withoutDescription = {
|
|
52
|
+
id: "settings",
|
|
53
|
+
layout: { sections: [] },
|
|
54
|
+
} as unknown as ConfigEditScreenDefinition;
|
|
55
|
+
expect(synthesizeConfigEditScreen(withoutDescription)).not.toHaveProperty("description");
|
|
56
|
+
});
|
|
40
57
|
});
|
|
@@ -43,6 +43,7 @@ export function synthesizeConfigEditScreen(
|
|
|
43
43
|
type: "entityEdit",
|
|
44
44
|
entity: CONFIG_EDIT_PSEUDO_ENTITY,
|
|
45
45
|
layout: screen.layout,
|
|
46
|
+
...(screen.description !== undefined && { description: screen.description }),
|
|
46
47
|
...(screen.fieldLabels !== undefined && { fieldLabels: screen.fieldLabels }),
|
|
47
48
|
...(screen.access !== undefined && { access: screen.access }),
|
|
48
49
|
};
|
|
@@ -93,6 +93,9 @@ export function SecretsEditBody({ screen, translate }: SecretsEditBodyProps): Re
|
|
|
93
93
|
void handleSubmit();
|
|
94
94
|
}}
|
|
95
95
|
testId="secrets-edit-form"
|
|
96
|
+
{...(screen.description !== undefined && {
|
|
97
|
+
subtitle: effectiveTranslate(screen.description),
|
|
98
|
+
})}
|
|
96
99
|
actions={
|
|
97
100
|
<Button
|
|
98
101
|
type="submit"
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
EditWriteFormSection,
|
|
4
|
+
EntityDefinition,
|
|
5
|
+
EntityEditScreenDefinition,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
7
|
+
import {
|
|
8
|
+
computeEditViewModel,
|
|
9
|
+
type Dispatcher,
|
|
10
|
+
type EditWriteFormSectionViewModel,
|
|
11
|
+
} from "@cosmicdrift/kumiko-headless";
|
|
3
12
|
import { fireEvent, render, screen as rtlScreen, waitFor } from "@testing-library/react";
|
|
4
13
|
import type { ComponentType, ReactNode } from "react";
|
|
5
14
|
import { DispatcherProvider } from "../../context/dispatcher-context";
|
|
6
|
-
import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
|
|
15
|
+
import { createStaticLocaleResolver, LocaleProvider, type TranslationsByLocale } from "../../i18n";
|
|
7
16
|
import { kumikoDefaultTranslations } from "../../i18n-defaults";
|
|
8
17
|
import {
|
|
9
18
|
type BannerProps,
|
|
@@ -42,8 +51,14 @@ const testBanner: ComponentType<BannerProps> = ({ children, testId }) => (
|
|
|
42
51
|
|
|
43
52
|
// Mirrors DefaultSection's real actions slot closely enough to let tests
|
|
44
53
|
// assert the submit button lands in the footer, not the body (fw#2675).
|
|
45
|
-
|
|
54
|
+
// Also renders subtitle into the DOM (like DefaultSection's own subtitle
|
|
55
|
+
// slot) so description-passthrough tests prove real render output, not just
|
|
56
|
+
// a captured prop.
|
|
57
|
+
const testSection: ComponentType<SectionProps> = ({ testId, subtitle, children, actions }) => (
|
|
46
58
|
<div data-testid={testId}>
|
|
59
|
+
{subtitle !== undefined && (
|
|
60
|
+
<p data-testid={testId !== undefined ? `${testId}-subtitle` : undefined}>{subtitle}</p>
|
|
61
|
+
)}
|
|
47
62
|
<div data-testid={testId !== undefined ? `${testId}-body` : undefined}>{children}</div>
|
|
48
63
|
{actions !== undefined && (
|
|
49
64
|
<div data-testid={testId !== undefined ? `${testId}-actions` : undefined}>{actions}</div>
|
|
@@ -120,11 +135,16 @@ function renderWriteForm(
|
|
|
120
135
|
section: EditWriteFormSectionViewModel,
|
|
121
136
|
dispatcher: Dispatcher,
|
|
122
137
|
onSubmitted: () => void,
|
|
138
|
+
extraTranslations?: TranslationsByLocale,
|
|
123
139
|
) {
|
|
124
140
|
return render(
|
|
125
141
|
<LocaleProvider
|
|
126
142
|
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
127
|
-
fallbackBundles={
|
|
143
|
+
fallbackBundles={
|
|
144
|
+
extraTranslations !== undefined
|
|
145
|
+
? [extraTranslations, kumikoDefaultTranslations]
|
|
146
|
+
: [kumikoDefaultTranslations]
|
|
147
|
+
}
|
|
128
148
|
>
|
|
129
149
|
<DispatcherProvider dispatcher={dispatcher}>
|
|
130
150
|
<PrimitivesProvider value={testPrimitives()}>
|
|
@@ -223,3 +243,80 @@ describe("WriteFormSection", () => {
|
|
|
223
243
|
expect(writes[0]?.payload).toEqual({ orderId: "order-42", note: "hi" });
|
|
224
244
|
});
|
|
225
245
|
});
|
|
246
|
+
|
|
247
|
+
// section.description exists on EditWriteFormSection to make a self-service
|
|
248
|
+
// write form usable without training (same doc-promise as EditFieldsSection),
|
|
249
|
+
// but WriteFormSection never read it — the ViewModel translated it correctly,
|
|
250
|
+
// the renderer just dropped it on the floor. Built through the real
|
|
251
|
+
// computeEditViewModel pipeline (not a hand-built ViewModel) to prove the
|
|
252
|
+
// full Spec -> ViewModel -> Renderer path, matching render-edit-screen-
|
|
253
|
+
// description.test.tsx's approach for the sibling entityEdit-level bug.
|
|
254
|
+
describe("WriteFormSection — section.description as subtitle", () => {
|
|
255
|
+
function computeNoteSection(
|
|
256
|
+
descriptionSpec: string,
|
|
257
|
+
translate: (key: string) => string,
|
|
258
|
+
): EditWriteFormSectionViewModel {
|
|
259
|
+
const sectionSpec: EditWriteFormSection = {
|
|
260
|
+
kind: "writeForm",
|
|
261
|
+
title: "Add note",
|
|
262
|
+
description: descriptionSpec,
|
|
263
|
+
columns: 1,
|
|
264
|
+
handler: "orders:write:add-note",
|
|
265
|
+
fieldDefs: {
|
|
266
|
+
note: {
|
|
267
|
+
type: "text",
|
|
268
|
+
maxLength: 500,
|
|
269
|
+
required: true,
|
|
270
|
+
searchable: false,
|
|
271
|
+
sortable: false,
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
fields: ["note"],
|
|
275
|
+
};
|
|
276
|
+
const screen: EntityEditScreenDefinition = {
|
|
277
|
+
id: "order-detail",
|
|
278
|
+
type: "entityEdit",
|
|
279
|
+
entity: "order",
|
|
280
|
+
layout: { sections: [sectionSpec] },
|
|
281
|
+
};
|
|
282
|
+
const entity: EntityDefinition = { fields: {} };
|
|
283
|
+
const viewModel = computeEditViewModel({
|
|
284
|
+
screen,
|
|
285
|
+
entity,
|
|
286
|
+
values: {},
|
|
287
|
+
translate,
|
|
288
|
+
featureName: "orders",
|
|
289
|
+
});
|
|
290
|
+
const section = viewModel.sections[0];
|
|
291
|
+
if (section?.kind !== "writeForm") throw new Error("expected a writeForm section");
|
|
292
|
+
return section;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
test("a plain-text description renders as the section's subtitle", () => {
|
|
296
|
+
const { dispatcher } = stubDispatcher();
|
|
297
|
+
const section = computeNoteSection("Explain why you're adding this note.", (key) => key);
|
|
298
|
+
|
|
299
|
+
renderWriteForm(section, dispatcher, noop);
|
|
300
|
+
|
|
301
|
+
expect(rtlScreen.getByTestId("write-form-Add note-subtitle").textContent).toBe(
|
|
302
|
+
"Explain why you're adding this note.",
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("a description that is a known i18n key renders translated, not as the raw key", () => {
|
|
307
|
+
const { dispatcher } = stubDispatcher();
|
|
308
|
+
const translations: Record<string, string> = {
|
|
309
|
+
"orders:write-form.explainer": "Notes are visible to the whole team.",
|
|
310
|
+
};
|
|
311
|
+
const section = computeNoteSection(
|
|
312
|
+
"orders:write-form.explainer",
|
|
313
|
+
(key) => translations[key] ?? key,
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
renderWriteForm(section, dispatcher, noop);
|
|
317
|
+
|
|
318
|
+
const subtitle = rtlScreen.getByTestId("write-form-Add note-subtitle");
|
|
319
|
+
expect(subtitle.textContent).toBe("Notes are visible to the whole team.");
|
|
320
|
+
expect(subtitle.textContent).not.toBe("orders:write-form.explainer");
|
|
321
|
+
});
|
|
322
|
+
});
|
|
@@ -1079,8 +1079,7 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
|
|
|
1079
1079
|
<Form
|
|
1080
1080
|
onSubmit={() => void handleSubmit()}
|
|
1081
1081
|
{...(hideSectionTitles !== true && { title: formTitle })}
|
|
1082
|
-
{...(
|
|
1083
|
-
formSubtitle !== undefined && { subtitle: formSubtitle })}
|
|
1082
|
+
{...(formSubtitle !== undefined && { subtitle: formSubtitle })}
|
|
1084
1083
|
{...(hideActions !== true && hasFormActions && { actions: formActions })}
|
|
1085
1084
|
{...(hideActions !== true &&
|
|
1086
1085
|
hasSecondaryFormActions && { secondaryActions: secondaryFormActions })}
|
|
@@ -155,6 +155,7 @@ export function WriteFormSection({
|
|
|
155
155
|
return (
|
|
156
156
|
<Section
|
|
157
157
|
{...(!hideTitle && section.title !== undefined && { title: section.title })}
|
|
158
|
+
{...(section.description !== undefined && { subtitle: section.description })}
|
|
158
159
|
{...(section.icon !== undefined && { icon: section.icon })}
|
|
159
160
|
actions={submitButton}
|
|
160
161
|
testId={`write-form-${section.title ?? "section"}`}
|