@cosmicdrift/kumiko-renderer-web 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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.252.1",
|
|
4
4
|
"description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"./styles.css": "./src/styles.css"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
20
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
21
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
19
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.252.1",
|
|
20
|
+
"@cosmicdrift/kumiko-headless": "0.252.1",
|
|
21
|
+
"@cosmicdrift/kumiko-renderer": "0.252.1",
|
|
22
22
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
23
23
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
24
24
|
"@radix-ui/react-label": "^2.1.8",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"@types/react-dom": "^19.2.3",
|
|
65
65
|
"jsdom": "^29.1.1",
|
|
66
66
|
"tailwindcss": "^4.3.0",
|
|
67
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
67
|
+
"@cosmicdrift/kumiko-locale-de": "0.252.1"
|
|
68
68
|
},
|
|
69
69
|
"repository": {
|
|
70
70
|
"type": "git",
|
|
@@ -90,6 +90,76 @@ describe("KumikoScreen dashboard", () => {
|
|
|
90
90
|
});
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
+
// screen.description exists on DashboardScreenDefinition (types/src/
|
|
94
|
+
// screen.ts:713) but WebDashboardBody never read it — the panels grid
|
|
95
|
+
// rendered with no room for it at all.
|
|
96
|
+
describe("KumikoScreen dashboard — screen.description", () => {
|
|
97
|
+
test("a plain-text description renders above the panels", async () => {
|
|
98
|
+
const screenWithDescription: DashboardScreenDefinition = {
|
|
99
|
+
...dashboardScreen,
|
|
100
|
+
description: "Live health signals across every monitored service.",
|
|
101
|
+
};
|
|
102
|
+
const schemaWithDescription: FeatureSchema = {
|
|
103
|
+
featureName: "status",
|
|
104
|
+
entities: {},
|
|
105
|
+
screens: [screenWithDescription],
|
|
106
|
+
};
|
|
107
|
+
render(
|
|
108
|
+
<DispatcherProvider dispatcher={makeDispatcher()}>
|
|
109
|
+
<DashboardBodyProvider value={WebDashboardBody}>
|
|
110
|
+
<KumikoScreen schema={schemaWithDescription} qn="status:screen:overview" />
|
|
111
|
+
</DashboardBodyProvider>
|
|
112
|
+
</DispatcherProvider>,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
expect(screen.getByTestId("dashboard-overview-description").textContent).toBe(
|
|
116
|
+
"Live health signals across every monitored service.",
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("a description that is a known i18n key renders translated, not as the raw key", async () => {
|
|
121
|
+
const screenWithDescription: DashboardScreenDefinition = {
|
|
122
|
+
...dashboardScreen,
|
|
123
|
+
description: "status:dashboard:explainer",
|
|
124
|
+
};
|
|
125
|
+
const schemaWithDescription: FeatureSchema = {
|
|
126
|
+
featureName: "status",
|
|
127
|
+
entities: {},
|
|
128
|
+
screens: [screenWithDescription],
|
|
129
|
+
};
|
|
130
|
+
render(
|
|
131
|
+
<DispatcherProvider dispatcher={makeDispatcher()}>
|
|
132
|
+
<DashboardBodyProvider value={WebDashboardBody}>
|
|
133
|
+
<KumikoScreen
|
|
134
|
+
schema={schemaWithDescription}
|
|
135
|
+
qn="status:screen:overview"
|
|
136
|
+
translate={(key) =>
|
|
137
|
+
key === "status:dashboard:explainer" ? "Refreshes every 60 seconds." : key
|
|
138
|
+
}
|
|
139
|
+
/>
|
|
140
|
+
</DashboardBodyProvider>
|
|
141
|
+
</DispatcherProvider>,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const description = screen.getByTestId("dashboard-overview-description");
|
|
145
|
+
expect(description.textContent).toBe("Refreshes every 60 seconds.");
|
|
146
|
+
expect(description.textContent).not.toBe("status:dashboard:explainer");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("a screen without description renders no description block", async () => {
|
|
150
|
+
render(
|
|
151
|
+
<DispatcherProvider dispatcher={makeDispatcher()}>
|
|
152
|
+
<DashboardBodyProvider value={WebDashboardBody}>
|
|
153
|
+
<KumikoScreen schema={schema} qn="status:screen:overview" />
|
|
154
|
+
</DashboardBodyProvider>
|
|
155
|
+
</DispatcherProvider>,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
await waitFor(() => expect(screen.getByTestId("dashboard-overview")).toBeTruthy());
|
|
159
|
+
expect(screen.queryByTestId("dashboard-overview-description")).toBeNull();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
93
163
|
const richScreen: DashboardScreenDefinition = {
|
|
94
164
|
id: "rich",
|
|
95
165
|
type: "dashboard",
|
|
@@ -202,6 +202,45 @@ describe("RenderEdit", () => {
|
|
|
202
202
|
expect(fieldEl.querySelector("input")?.className).toContain("pl-8");
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
+
// Fall 2: tabs layout sets hideSectionTitles (kumiko-screen.tsx's
|
|
206
|
+
// ProjectionDetailBody, the only real caller) to blank out each section's
|
|
207
|
+
// OWN title — the active tab already names it. That gate used to also
|
|
208
|
+
// swallow the screen-level subtitle (screen.description), which has
|
|
209
|
+
// nothing to do with section titles and must survive tabs mode.
|
|
210
|
+
test("tabs layout (hideSectionTitles) keeps the section title hidden but still shows the screen's own subtitle", () => {
|
|
211
|
+
const entity = {
|
|
212
|
+
fields: { email: { type: "text", required: true } },
|
|
213
|
+
} as unknown as EntityDefinition;
|
|
214
|
+
const screenDef: EntityEditScreenDefinition = {
|
|
215
|
+
id: "orders:screen:order-detail",
|
|
216
|
+
type: "entityEdit",
|
|
217
|
+
entity: "order",
|
|
218
|
+
description: "Everything about this order in one place.",
|
|
219
|
+
layout: {
|
|
220
|
+
mode: "tabs",
|
|
221
|
+
sections: [{ id: "contact", title: "Contact", fields: [{ field: "email" }] }],
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
render(
|
|
225
|
+
<DispatcherProvider dispatcher={makeDispatcher()}>
|
|
226
|
+
<RenderEdit
|
|
227
|
+
screen={screenDef}
|
|
228
|
+
entity={entity}
|
|
229
|
+
featureName="orders"
|
|
230
|
+
initial={{ email: "" } as never}
|
|
231
|
+
writeCommand="order:create"
|
|
232
|
+
hideSectionTitles
|
|
233
|
+
/>
|
|
234
|
+
</DispatcherProvider>,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
expect(screen.queryByTestId("render-edit-form-title")).toBeNull();
|
|
238
|
+
expect(screen.queryByTestId("section-Contact-title")).toBeNull();
|
|
239
|
+
expect(screen.getByTestId("render-edit-form-subtitle").textContent).toBe(
|
|
240
|
+
"Everything about this order in one place.",
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
205
244
|
// End-to-end-Routing: ein `type:"locatedTimestamp"`-Entity-Feld muss durch
|
|
206
245
|
// computeEditViewModel → render-field → DefaultInput auf den Located-Picker
|
|
207
246
|
// laufen (Datum + Uhrzeit + Zone), NICHT auf den Klartext-Fallthrough. Vor
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// SecretsEditScreenDefinition.description exists (types/src/screen.ts:1205)
|
|
2
|
+
// to explain what the secrets on this screen are for, same doc-promise as
|
|
3
|
+
// every other description slot — but SecretsEditBody built its own <Form>
|
|
4
|
+
// without ever reading screen.description. Fixed by wiring it into the
|
|
5
|
+
// Form's established subtitle slot (same slot render-edit.tsx and
|
|
6
|
+
// write-form-section.tsx use), rendered through DefaultForm — real
|
|
7
|
+
// renderer-web primitives, not a stub, so this proves the actual DOM output.
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from "bun:test";
|
|
10
|
+
import type { SecretsEditScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
11
|
+
import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
|
|
12
|
+
import { DispatcherProvider } from "@cosmicdrift/kumiko-renderer";
|
|
13
|
+
import { SecretsEditBody } from "../../../renderer/src/app/secrets-edit-body";
|
|
14
|
+
import { createMockDispatcher, render, screen, waitFor } from "./test-utils";
|
|
15
|
+
|
|
16
|
+
function makeDispatcher(): Dispatcher {
|
|
17
|
+
return createMockDispatcher({
|
|
18
|
+
query: (async () => ({ isSuccess: true, data: [] })) as Dispatcher["query"],
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function baseScreen(): SecretsEditScreenDefinition {
|
|
23
|
+
return {
|
|
24
|
+
id: "integrations:screen:secrets",
|
|
25
|
+
type: "secretsEdit",
|
|
26
|
+
secretKeys: { apiKey: "integrations:secret:api-key" },
|
|
27
|
+
fieldLabels: { apiKey: "API key" },
|
|
28
|
+
sections: [{ fields: ["apiKey"] }],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("SecretsEditBody — screen.description as form subtitle", () => {
|
|
33
|
+
test("a plain-text description renders as the form's subtitle", async () => {
|
|
34
|
+
const screenDef: SecretsEditScreenDefinition = {
|
|
35
|
+
...baseScreen(),
|
|
36
|
+
description: "These credentials are shared across every integration on this workspace.",
|
|
37
|
+
};
|
|
38
|
+
render(
|
|
39
|
+
<DispatcherProvider dispatcher={makeDispatcher()}>
|
|
40
|
+
<SecretsEditBody screen={screenDef} />
|
|
41
|
+
</DispatcherProvider>,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
await waitFor(() =>
|
|
45
|
+
expect(screen.getByTestId("secrets-edit-form-subtitle").textContent).toBe(
|
|
46
|
+
"These credentials are shared across every integration on this workspace.",
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("a description that is a known i18n key renders translated, not as the raw key", async () => {
|
|
52
|
+
const screenDef: SecretsEditScreenDefinition = {
|
|
53
|
+
...baseScreen(),
|
|
54
|
+
description: "integrations:secrets.explainer",
|
|
55
|
+
};
|
|
56
|
+
render(
|
|
57
|
+
<DispatcherProvider dispatcher={makeDispatcher()}>
|
|
58
|
+
<SecretsEditBody
|
|
59
|
+
screen={screenDef}
|
|
60
|
+
translate={(key) =>
|
|
61
|
+
key === "integrations:secrets.explainer" ? "Rotate these keys every 90 days." : key
|
|
62
|
+
}
|
|
63
|
+
/>
|
|
64
|
+
</DispatcherProvider>,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
await waitFor(() => {
|
|
68
|
+
const subtitle = screen.getByTestId("secrets-edit-form-subtitle");
|
|
69
|
+
expect(subtitle.textContent).toBe("Rotate these keys every 90 days.");
|
|
70
|
+
expect(subtitle.textContent).not.toBe("integrations:secrets.explainer");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("a screen without description renders no subtitle at all", async () => {
|
|
75
|
+
render(
|
|
76
|
+
<DispatcherProvider dispatcher={makeDispatcher()}>
|
|
77
|
+
<SecretsEditBody screen={baseScreen()} />
|
|
78
|
+
</DispatcherProvider>,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
await waitFor(() => expect(screen.getByTestId("secrets-edit-form")).toBeTruthy());
|
|
82
|
+
expect(screen.queryByTestId("secrets-edit-form-subtitle")).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -437,9 +437,15 @@ function PanelBody({
|
|
|
437
437
|
export function WebDashboardBody({ screen, translate }: DashboardBodyProps): ReactNode {
|
|
438
438
|
const t = useTranslation();
|
|
439
439
|
const effectiveTranslate = translate ?? t;
|
|
440
|
+
const { Text } = usePrimitives();
|
|
440
441
|
const { params: filterParams, picker } = useFilterParams(screen);
|
|
441
442
|
return (
|
|
442
443
|
<PageSection className="flex flex-col gap-4" testId={`dashboard-${screen.id}`}>
|
|
444
|
+
{screen.description !== undefined && (
|
|
445
|
+
<Text variant="muted" testId={`dashboard-${screen.id}-description`}>
|
|
446
|
+
{effectiveTranslate(screen.description)}
|
|
447
|
+
</Text>
|
|
448
|
+
)}
|
|
443
449
|
{picker}
|
|
444
450
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
445
451
|
{screen.panels.map((panel) => {
|