@cosmicdrift/kumiko-renderer-web 0.133.0 → 0.134.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 +4 -4
- package/src/__tests__/dashboard-screen.test.tsx +207 -1
- package/src/__tests__/workspace-shell.test.tsx +5 -37
- package/src/app/dashboard-body.tsx +273 -30
- package/src/index.ts +4 -0
- package/src/widgets/feed-list.tsx +35 -0
- package/src/widgets/index.ts +2 -0
- package/src/widgets/progress-list.tsx +38 -0
- package/src/ui/card.tsx +0 -93
- package/src/ui/collapsible.tsx +0 -34
- package/src/ui/dropdown-menu.tsx +0 -258
- package/src/ui/select.tsx +0 -191
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.134.0",
|
|
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.134.0",
|
|
20
|
+
"@cosmicdrift/kumiko-headless": "0.134.0",
|
|
21
|
+
"@cosmicdrift/kumiko-renderer": "0.134.0",
|
|
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",
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import type { DashboardScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
3
|
import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
|
|
4
|
-
import type { FeatureSchema } from "@cosmicdrift/kumiko-renderer";
|
|
4
|
+
import type { ExtensionSectionProps, FeatureSchema } from "@cosmicdrift/kumiko-renderer";
|
|
5
5
|
import {
|
|
6
6
|
DashboardBodyProvider,
|
|
7
7
|
DispatcherProvider,
|
|
8
|
+
ExtensionSectionsProvider,
|
|
8
9
|
KumikoScreen,
|
|
9
10
|
} from "@cosmicdrift/kumiko-renderer";
|
|
11
|
+
import userEvent from "@testing-library/user-event";
|
|
12
|
+
import type { ReactNode } from "react";
|
|
10
13
|
import { WebDashboardBody } from "../app/dashboard-body";
|
|
11
14
|
import { createMockDispatcher, render, screen, waitFor } from "./test-utils";
|
|
12
15
|
|
|
@@ -84,3 +87,206 @@ describe("KumikoScreen dashboard", () => {
|
|
|
84
87
|
await waitFor(() => expect(screen.getByText("API-Ausfall")).toBeTruthy());
|
|
85
88
|
});
|
|
86
89
|
});
|
|
90
|
+
|
|
91
|
+
const richScreen: DashboardScreenDefinition = {
|
|
92
|
+
id: "rich",
|
|
93
|
+
type: "dashboard",
|
|
94
|
+
filter: {
|
|
95
|
+
id: "region",
|
|
96
|
+
label: "widgets:dashboard:filter-region",
|
|
97
|
+
kind: "select",
|
|
98
|
+
options: [
|
|
99
|
+
{ value: "eu", label: "widgets:dashboard:filter-region-eu" },
|
|
100
|
+
{ value: "us", label: "widgets:dashboard:filter-region-us" },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
panels: [
|
|
104
|
+
{
|
|
105
|
+
kind: "stat",
|
|
106
|
+
id: "kpi",
|
|
107
|
+
label: "widgets:dashboard:kpi",
|
|
108
|
+
query: "widgets:query:metrics:kpi",
|
|
109
|
+
valueField: "value",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
kind: "stat-group",
|
|
113
|
+
id: "net-worth",
|
|
114
|
+
label: "widgets:dashboard:net-worth",
|
|
115
|
+
stats: [
|
|
116
|
+
{
|
|
117
|
+
kind: "stat",
|
|
118
|
+
id: "assets",
|
|
119
|
+
label: "widgets:dashboard:assets",
|
|
120
|
+
query: "widgets:query:metrics:assets",
|
|
121
|
+
valueField: "value",
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
kind: "feed",
|
|
127
|
+
id: "upcoming",
|
|
128
|
+
label: "widgets:dashboard:upcoming",
|
|
129
|
+
query: "widgets:query:metrics:upcoming",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
kind: "progress-list",
|
|
133
|
+
id: "goal-progress",
|
|
134
|
+
label: "widgets:dashboard:goal-progress",
|
|
135
|
+
query: "widgets:query:metrics:goal-progress",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
kind: "custom",
|
|
139
|
+
id: "custom-panel",
|
|
140
|
+
component: { react: { __component: "rich-dashboard-custom" } },
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const richSchema: FeatureSchema = {
|
|
146
|
+
featureName: "widgets",
|
|
147
|
+
entities: {},
|
|
148
|
+
screens: [richScreen],
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
describe("KumikoScreen dashboard — neue Panel-Kinds", () => {
|
|
152
|
+
test("stat-group rendert Sektions-Titel + genestete Stat-Panels", async () => {
|
|
153
|
+
const dispatcher = createMockDispatcher({
|
|
154
|
+
query: (async (type: string) => {
|
|
155
|
+
if (type === "widgets:query:metrics:assets") {
|
|
156
|
+
return { isSuccess: true, data: { value: "120.000 €" } };
|
|
157
|
+
}
|
|
158
|
+
return { isSuccess: true, data: {} };
|
|
159
|
+
}) as unknown as Dispatcher["query"],
|
|
160
|
+
});
|
|
161
|
+
render(
|
|
162
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
163
|
+
<DashboardBodyProvider value={WebDashboardBody}>
|
|
164
|
+
<KumikoScreen schema={richSchema} qn="widgets:screen:rich" />
|
|
165
|
+
</DashboardBodyProvider>
|
|
166
|
+
</DispatcherProvider>,
|
|
167
|
+
);
|
|
168
|
+
expect(screen.getByText("widgets:dashboard:net-worth")).toBeTruthy();
|
|
169
|
+
await waitFor(() => expect(screen.getByText("120.000 €")).toBeTruthy());
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("feed-Panel rendert primary/trailing-Zeilen", async () => {
|
|
173
|
+
const dispatcher = createMockDispatcher({
|
|
174
|
+
query: (async (type: string) => {
|
|
175
|
+
if (type === "widgets:query:metrics:upcoming") {
|
|
176
|
+
return {
|
|
177
|
+
isSuccess: true,
|
|
178
|
+
data: { rows: [{ primary: "Zinsanpassung", trailing: "Aug 2026" }] },
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return { isSuccess: true, data: {} };
|
|
182
|
+
}) as unknown as Dispatcher["query"],
|
|
183
|
+
});
|
|
184
|
+
render(
|
|
185
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
186
|
+
<DashboardBodyProvider value={WebDashboardBody}>
|
|
187
|
+
<KumikoScreen schema={richSchema} qn="widgets:screen:rich" />
|
|
188
|
+
</DashboardBodyProvider>
|
|
189
|
+
</DispatcherProvider>,
|
|
190
|
+
);
|
|
191
|
+
await waitFor(() => expect(screen.getByText("Zinsanpassung")).toBeTruthy());
|
|
192
|
+
expect(screen.getByText("Aug 2026")).toBeTruthy();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("progress-list-Panel rendert Label/Wert + Fortschrittsbalken", async () => {
|
|
196
|
+
const dispatcher = createMockDispatcher({
|
|
197
|
+
query: (async (type: string) => {
|
|
198
|
+
if (type === "widgets:query:metrics:goal-progress") {
|
|
199
|
+
return {
|
|
200
|
+
isSuccess: true,
|
|
201
|
+
data: { rows: [{ label: "Baudarlehen", value: "42.000 € offen", fraction: 0.71 }] },
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return { isSuccess: true, data: {} };
|
|
205
|
+
}) as unknown as Dispatcher["query"],
|
|
206
|
+
});
|
|
207
|
+
render(
|
|
208
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
209
|
+
<DashboardBodyProvider value={WebDashboardBody}>
|
|
210
|
+
<KumikoScreen schema={richSchema} qn="widgets:screen:rich" />
|
|
211
|
+
</DashboardBodyProvider>
|
|
212
|
+
</DispatcherProvider>,
|
|
213
|
+
);
|
|
214
|
+
await waitFor(() => expect(screen.getByText("Baudarlehen")).toBeTruthy());
|
|
215
|
+
const bar = screen.getByRole("progressbar");
|
|
216
|
+
expect(bar.getAttribute("aria-valuenow")).toBe("71");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("custom-Panel: registrierte Komponente rendert mit screenId + filterParams", async () => {
|
|
220
|
+
function CustomEcho({ screenId, filterParams }: ExtensionSectionProps): ReactNode {
|
|
221
|
+
return (
|
|
222
|
+
<div data-testid="custom-echo">
|
|
223
|
+
{screenId}:{String(filterParams?.["region"] ?? "none")}
|
|
224
|
+
</div>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
const dispatcher = createMockDispatcher();
|
|
228
|
+
render(
|
|
229
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
230
|
+
<ExtensionSectionsProvider value={{ "rich-dashboard-custom": CustomEcho }}>
|
|
231
|
+
<DashboardBodyProvider value={WebDashboardBody}>
|
|
232
|
+
<KumikoScreen schema={richSchema} qn="widgets:screen:rich" />
|
|
233
|
+
</DashboardBodyProvider>
|
|
234
|
+
</ExtensionSectionsProvider>
|
|
235
|
+
</DispatcherProvider>,
|
|
236
|
+
);
|
|
237
|
+
await waitFor(() => expect(screen.getByTestId("custom-echo")).toBeTruthy());
|
|
238
|
+
expect(screen.getByTestId("custom-echo").textContent).toBe("rich:none");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("custom-Panel: unregistrierte Komponente rendert nichts, wirft nicht", async () => {
|
|
242
|
+
const dispatcher = createMockDispatcher();
|
|
243
|
+
render(
|
|
244
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
245
|
+
<DashboardBodyProvider value={WebDashboardBody}>
|
|
246
|
+
<KumikoScreen schema={richSchema} qn="widgets:screen:rich" />
|
|
247
|
+
</DashboardBodyProvider>
|
|
248
|
+
</DispatcherProvider>,
|
|
249
|
+
);
|
|
250
|
+
await waitFor(() => expect(screen.getByTestId("dashboard-panel-kpi")).toBeTruthy());
|
|
251
|
+
expect(screen.queryByTestId("custom-echo")).toBeNull();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("Filter-Wechsel refetcht Stat- UND Feed-Panel mit neuem Payload", async () => {
|
|
255
|
+
const calls: { readonly type: string; readonly payload: unknown }[] = [];
|
|
256
|
+
const dispatcher = createMockDispatcher({
|
|
257
|
+
query: (async (type: string, payload: unknown) => {
|
|
258
|
+
calls.push({ type, payload });
|
|
259
|
+
const region = (payload as { readonly region?: string } | undefined)?.region;
|
|
260
|
+
if (type === "widgets:query:metrics:kpi") {
|
|
261
|
+
return { isSuccess: true, data: { value: region === "us" ? "38.120 $" : "92.753 €" } };
|
|
262
|
+
}
|
|
263
|
+
if (type === "widgets:query:metrics:upcoming") {
|
|
264
|
+
return {
|
|
265
|
+
isSuccess: true,
|
|
266
|
+
data: {
|
|
267
|
+
rows: [{ primary: region === "us" ? "US-Event" : "EU-Event" }],
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return { isSuccess: true, data: {} };
|
|
272
|
+
}) as unknown as Dispatcher["query"],
|
|
273
|
+
});
|
|
274
|
+
const user = userEvent.setup();
|
|
275
|
+
render(
|
|
276
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
277
|
+
<DashboardBodyProvider value={WebDashboardBody}>
|
|
278
|
+
<KumikoScreen schema={richSchema} qn="widgets:screen:rich" />
|
|
279
|
+
</DashboardBodyProvider>
|
|
280
|
+
</DispatcherProvider>,
|
|
281
|
+
);
|
|
282
|
+
await waitFor(() => expect(screen.getByText("92.753 €")).toBeTruthy());
|
|
283
|
+
await waitFor(() => expect(screen.getByText("EU-Event")).toBeTruthy());
|
|
284
|
+
|
|
285
|
+
await user.click(screen.getByTestId("combobox-dashboard-filter-region"));
|
|
286
|
+
const usOption = await screen.findByText("widgets:dashboard:filter-region-us");
|
|
287
|
+
await user.click(usOption);
|
|
288
|
+
|
|
289
|
+
await waitFor(() => expect(screen.getByText("38.120 $")).toBeTruthy());
|
|
290
|
+
await waitFor(() => expect(screen.getByText("US-Event")).toBeTruthy());
|
|
291
|
+
});
|
|
292
|
+
});
|
|
@@ -57,7 +57,6 @@ function ws(
|
|
|
57
57
|
openToAll?: boolean;
|
|
58
58
|
isDefault?: boolean;
|
|
59
59
|
navMembers?: readonly string[];
|
|
60
|
-
navigation?: "nav" | "tree";
|
|
61
60
|
} = {},
|
|
62
61
|
): WorkspaceSchema {
|
|
63
62
|
const access = options.openToAll
|
|
@@ -72,7 +71,6 @@ function ws(
|
|
|
72
71
|
...(options.order !== undefined && { order: options.order }),
|
|
73
72
|
...(access !== undefined && { access }),
|
|
74
73
|
...(options.isDefault === true && { default: true }),
|
|
75
|
-
...(options.navigation !== undefined && { navigation: options.navigation }),
|
|
76
74
|
},
|
|
77
75
|
navMembers: options.navMembers ?? [],
|
|
78
76
|
};
|
|
@@ -774,43 +772,13 @@ describe("WorkspaceShell — AppSchema (multi-feature)", () => {
|
|
|
774
772
|
});
|
|
775
773
|
|
|
776
774
|
// ---------------------------------------------------------------------------
|
|
777
|
-
// EINE Nav
|
|
778
|
-
//
|
|
779
|
-
//
|
|
780
|
-
// WorkspaceDefinition). Im Content-Bereich ersetzt ein aktives target
|
|
781
|
-
// (aus den nav.searchParams) den RoutedScreen durch den EditorPanel.
|
|
775
|
+
// EINE Nav. WorkspaceShell rendert IMMER NavTree. Im Content-Bereich
|
|
776
|
+
// ersetzt ein aktives target (aus den nav.searchParams) den RoutedScreen
|
|
777
|
+
// durch den EditorPanel.
|
|
782
778
|
// ---------------------------------------------------------------------------
|
|
783
779
|
|
|
784
|
-
describe("WorkspaceShell — EINE Nav
|
|
785
|
-
test(
|
|
786
|
-
const schema = {
|
|
787
|
-
featureName: "demo",
|
|
788
|
-
entities: {},
|
|
789
|
-
screens: [],
|
|
790
|
-
navs: [{ id: "list", label: "List" }],
|
|
791
|
-
workspaces: [
|
|
792
|
-
ws("visual", {
|
|
793
|
-
label: "Visual",
|
|
794
|
-
openToAll: true,
|
|
795
|
-
isDefault: true,
|
|
796
|
-
navigation: "tree",
|
|
797
|
-
navMembers: ["demo:nav:list"],
|
|
798
|
-
}),
|
|
799
|
-
],
|
|
800
|
-
} as const;
|
|
801
|
-
|
|
802
|
-
renderShell(
|
|
803
|
-
<WorkspaceShell brand={<div>Brand</div>} schema={schema} user={{ id: "u1", roles: [] }}>
|
|
804
|
-
<div>content</div>
|
|
805
|
-
</WorkspaceShell>,
|
|
806
|
-
);
|
|
807
|
-
// NavTree rendert das nav-Item — auch bei navigation:"tree" (No-op).
|
|
808
|
-
expect(screen.getByText("List")).toBeTruthy();
|
|
809
|
-
// Der alte VisualTree-Stub existiert nicht mehr.
|
|
810
|
-
expect(screen.queryByLabelText("Visual Tree (no providers)")).toBeNull();
|
|
811
|
-
});
|
|
812
|
-
|
|
813
|
-
test("workspace ohne navigation-Property rendert NavTree (Backwards-Compat)", () => {
|
|
780
|
+
describe("WorkspaceShell — EINE Nav", () => {
|
|
781
|
+
test("workspace rendert NavTree", () => {
|
|
814
782
|
const schema = {
|
|
815
783
|
featureName: "demo",
|
|
816
784
|
entities: {},
|