@cosmicdrift/kumiko-framework 0.258.1 → 0.259.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 +3 -3
- package/src/changes.json +6 -0
- package/src/engine/__tests__/boot-validator-dashboard.test.ts +138 -1
- package/src/engine/boot-validator/query-refs.ts +11 -0
- package/src/engine/boot-validator/screens.ts +45 -2
- package/src/engine/index.ts +2 -0
- package/src/engine/types/index.ts +2 -0
- package/src/ui-types/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.259.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -198,7 +198,7 @@
|
|
|
198
198
|
"./package.json": "./package.json"
|
|
199
199
|
},
|
|
200
200
|
"dependencies": {
|
|
201
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
201
|
+
"@cosmicdrift/kumiko-types": "0.259.0",
|
|
202
202
|
"bullmq": "^5.76.7",
|
|
203
203
|
"bun-types": "^1.3.13",
|
|
204
204
|
"hono": "^4.13.1",
|
|
@@ -214,7 +214,7 @@
|
|
|
214
214
|
"zod": "^4.4.3"
|
|
215
215
|
},
|
|
216
216
|
"devDependencies": {
|
|
217
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
217
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.259.0",
|
|
218
218
|
"bun-types": "^1.3.13",
|
|
219
219
|
"pino-pretty": "^13.1.3"
|
|
220
220
|
},
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.259.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Dashboard screens compose other declarative screens via kind: \"screen\" panels (fw#2844, decision fw#2841).",
|
|
6
|
+
"detail": "`DashboardScreenPanel` (`@cosmicdrift/kumiko-types`) embeds a registered screen by same-feature short id or `<feature>:screen:<id>`; embeddable are projectionList, actionForm, secretMint, configEdit, secretsEdit — the boot validator rejects unresolvable targets and dashboard/entityEdit/projectionDetail/custom/entityList. `visibleWhen: { query, field, eq }` shows the panel only while the live query's flat result matches (query boot-checked). A user without access to the target gets no tile. `DashboardBodyProps` gains the required `featureName` (only relevant for a custom dashboard body implementation); `useEmbeddedScreen` is exported from `@cosmicdrift/kumiko-renderer`. Replaces \"framework screen plus own content\" custom screens with an app-feature-structure lint-ignore."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.242.0",
|
|
4
10
|
"type": "improvement",
|
|
@@ -3,7 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { requiredKeysFromScreen } from "../../i18n/required-surface-keys";
|
|
4
4
|
import { validateBoot } from "../boot-validator";
|
|
5
5
|
import { defineFeature } from "../define-feature";
|
|
6
|
-
import type { DashboardScreenDefinition } from "../types/screen";
|
|
6
|
+
import type { DashboardScreenDefinition, DashboardScreenPanel } from "../types/screen";
|
|
7
7
|
|
|
8
8
|
const STAT_PANEL = {
|
|
9
9
|
kind: "stat",
|
|
@@ -242,3 +242,140 @@ describe("validateBoot — dashboard screens", () => {
|
|
|
242
242
|
expect(keys).not.toContain("custom-panel");
|
|
243
243
|
});
|
|
244
244
|
});
|
|
245
|
+
|
|
246
|
+
const catalogFeature = defineFeature("catalog", (r) => {
|
|
247
|
+
r.queryHandler("items:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
|
|
248
|
+
access: { openToAll: true },
|
|
249
|
+
});
|
|
250
|
+
r.queryHandler("items:status", z.object({}), async () => ({ enabled: true }), {
|
|
251
|
+
access: { openToAll: true },
|
|
252
|
+
});
|
|
253
|
+
r.screen({
|
|
254
|
+
id: "items",
|
|
255
|
+
type: "projectionList",
|
|
256
|
+
query: "catalog:query:items:list",
|
|
257
|
+
columns: ["name"],
|
|
258
|
+
});
|
|
259
|
+
r.screen({
|
|
260
|
+
id: "items-overview",
|
|
261
|
+
type: "dashboard",
|
|
262
|
+
panels: [STAT_PANEL],
|
|
263
|
+
});
|
|
264
|
+
r.translations({
|
|
265
|
+
keys: {
|
|
266
|
+
"screen:items.title": { de: "Artikel", en: "Items" },
|
|
267
|
+
"screen:items-overview.title": { de: "Übersicht", en: "Overview" },
|
|
268
|
+
"demo:dashboard:panel:open-incidents": { de: "Offene Vorfälle", en: "Open incidents" },
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
function screenPanelFeature(panel: DashboardScreenPanel) {
|
|
274
|
+
return dashboardFeature([STAT_PANEL, panel]);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
describe("validateBoot — dashboard screen panels (fw#2841)", () => {
|
|
278
|
+
test("accepts a cross-feature projectionList target with a registered visibleWhen query", () => {
|
|
279
|
+
const feature = screenPanelFeature({
|
|
280
|
+
kind: "screen",
|
|
281
|
+
id: "items",
|
|
282
|
+
screen: "catalog:screen:items",
|
|
283
|
+
label: "demo:dashboard:panel:latest",
|
|
284
|
+
visibleWhen: { query: "catalog:query:items:status", field: "enabled", eq: true },
|
|
285
|
+
});
|
|
286
|
+
expect(() => validateBoot([feature, catalogFeature])).not.toThrow();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("accepts a same-feature short id", () => {
|
|
290
|
+
const feature = defineFeature("demo", (r) => {
|
|
291
|
+
r.queryHandler("items:list", z.object({}), async () => ({ rows: [], nextCursor: null }), {
|
|
292
|
+
access: { openToAll: true },
|
|
293
|
+
});
|
|
294
|
+
r.screen({
|
|
295
|
+
id: "items",
|
|
296
|
+
type: "projectionList",
|
|
297
|
+
query: "demo:query:items:list",
|
|
298
|
+
columns: ["name"],
|
|
299
|
+
});
|
|
300
|
+
r.screen({
|
|
301
|
+
id: "overview",
|
|
302
|
+
type: "dashboard",
|
|
303
|
+
panels: [{ kind: "screen", id: "items", screen: "items" }],
|
|
304
|
+
});
|
|
305
|
+
r.translations({
|
|
306
|
+
keys: {
|
|
307
|
+
"screen:items.title": { de: "Artikel", en: "Items" },
|
|
308
|
+
"screen:overview.title": { de: "Übersicht", en: "Overview" },
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
expect(() => validateBoot([feature])).not.toThrow();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("rejects a target that resolves to no registered screen", () => {
|
|
316
|
+
const feature = screenPanelFeature({
|
|
317
|
+
kind: "screen",
|
|
318
|
+
id: "items",
|
|
319
|
+
screen: "catalog:screen:ghost",
|
|
320
|
+
});
|
|
321
|
+
expect(() => validateBoot([feature, catalogFeature])).toThrow(
|
|
322
|
+
/screen-panel "items" screen "catalog:screen:ghost" does not resolve/,
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("rejects a short id that only exists in another feature", () => {
|
|
327
|
+
const feature = screenPanelFeature({ kind: "screen", id: "items", screen: "items" });
|
|
328
|
+
expect(() => validateBoot([feature, catalogFeature])).toThrow(/checked "demo:screen:items"/);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("rejects embedding a dashboard (no nesting)", () => {
|
|
332
|
+
const feature = screenPanelFeature({
|
|
333
|
+
kind: "screen",
|
|
334
|
+
id: "nested",
|
|
335
|
+
screen: "catalog:screen:items-overview",
|
|
336
|
+
});
|
|
337
|
+
expect(() => validateBoot([feature, catalogFeature])).toThrow(
|
|
338
|
+
/of type "dashboard", which can't be embedded/,
|
|
339
|
+
);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("rejects a visibleWhen query that is not registered", () => {
|
|
343
|
+
const feature = screenPanelFeature({
|
|
344
|
+
kind: "screen",
|
|
345
|
+
id: "items",
|
|
346
|
+
screen: "catalog:screen:items",
|
|
347
|
+
visibleWhen: { query: "catalog:query:items:ghost", field: "enabled", eq: true },
|
|
348
|
+
});
|
|
349
|
+
expect(() => validateBoot([feature, catalogFeature])).toThrow(
|
|
350
|
+
/screen-panel "items" visibleWhen query "catalog:query:items:ghost" is not a registered query-handler/,
|
|
351
|
+
);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("rejects a visibleWhen with an empty field", () => {
|
|
355
|
+
const feature = screenPanelFeature({
|
|
356
|
+
kind: "screen",
|
|
357
|
+
id: "items",
|
|
358
|
+
screen: "catalog:screen:items",
|
|
359
|
+
visibleWhen: { query: "catalog:query:items:status", field: "", eq: true },
|
|
360
|
+
});
|
|
361
|
+
expect(() => validateBoot([feature, catalogFeature])).toThrow(
|
|
362
|
+
/visibleWhen needs a non-empty query and field/,
|
|
363
|
+
);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("requiredKeysFromScreen sammelt ein gesetztes Screen-Panel-Label", () => {
|
|
367
|
+
const screen: DashboardScreenDefinition = {
|
|
368
|
+
id: "overview",
|
|
369
|
+
type: "dashboard",
|
|
370
|
+
panels: [
|
|
371
|
+
{
|
|
372
|
+
kind: "screen",
|
|
373
|
+
id: "items",
|
|
374
|
+
screen: "catalog:screen:items",
|
|
375
|
+
label: "demo:dashboard:panel:latest",
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
};
|
|
379
|
+
expect(requiredKeysFromScreen("demo", screen)).toContain("demo:dashboard:panel:latest");
|
|
380
|
+
});
|
|
381
|
+
});
|
|
@@ -56,6 +56,17 @@ function checkDashboardQueryRefs(
|
|
|
56
56
|
): void {
|
|
57
57
|
for (const panel of screen.panels) {
|
|
58
58
|
if (panel.kind === "custom") continue;
|
|
59
|
+
if (panel.kind === "screen") {
|
|
60
|
+
if (panel.visibleWhen !== undefined) {
|
|
61
|
+
checkQueryRef(
|
|
62
|
+
queryHandlers,
|
|
63
|
+
panel.visibleWhen.query,
|
|
64
|
+
() =>
|
|
65
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) screen-panel "${panel.id}" visibleWhen`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
59
70
|
if (panel.kind === "stat-group") {
|
|
60
71
|
for (const stat of panel.stats) {
|
|
61
72
|
checkQueryRef(
|
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
DashboardFilterDefinition,
|
|
24
24
|
DashboardPanelDefinition,
|
|
25
25
|
DashboardScreenDefinition,
|
|
26
|
+
DashboardScreenPanel,
|
|
26
27
|
DashboardStatGroupPanel,
|
|
27
28
|
EditFieldSpec,
|
|
28
29
|
EditLayout,
|
|
@@ -1234,7 +1235,7 @@ export function validateScreens(
|
|
|
1234
1235
|
}
|
|
1235
1236
|
|
|
1236
1237
|
if (screen.type === "dashboard") {
|
|
1237
|
-
validateDashboardScreen(feature.name, screenId, screen);
|
|
1238
|
+
validateDashboardScreen(feature.name, screenId, screen, featureMap);
|
|
1238
1239
|
continue;
|
|
1239
1240
|
}
|
|
1240
1241
|
|
|
@@ -1807,6 +1808,7 @@ function validateDashboardScreen(
|
|
|
1807
1808
|
featureName: string,
|
|
1808
1809
|
screenId: string,
|
|
1809
1810
|
screen: DashboardScreenDefinition,
|
|
1811
|
+
featureMap: ReadonlyMap<string, FeatureDefinition>,
|
|
1810
1812
|
): void {
|
|
1811
1813
|
if (screen.panels.length === 0) {
|
|
1812
1814
|
throw new Error(
|
|
@@ -1830,6 +1832,8 @@ function validateDashboardScreen(
|
|
|
1830
1832
|
validateDashboardStatGroupPanel(featureName, screenId, panel, addPanelId);
|
|
1831
1833
|
} else if (panel.kind === "custom") {
|
|
1832
1834
|
validateDashboardCustomPanel(featureName, screenId, panel);
|
|
1835
|
+
} else if (panel.kind === "screen") {
|
|
1836
|
+
validateDashboardScreenPanel(featureName, screenId, panel, featureMap);
|
|
1833
1837
|
} else {
|
|
1834
1838
|
validateDashboardQueryPanel(featureName, screenId, panel);
|
|
1835
1839
|
}
|
|
@@ -1840,6 +1844,42 @@ function validateDashboardScreen(
|
|
|
1840
1844
|
}
|
|
1841
1845
|
}
|
|
1842
1846
|
|
|
1847
|
+
const EMBEDDABLE_SCREEN_TYPES: ReadonlySet<ScreenDefinition["type"]> = new Set([
|
|
1848
|
+
"projectionList",
|
|
1849
|
+
"actionForm",
|
|
1850
|
+
"secretMint",
|
|
1851
|
+
"configEdit",
|
|
1852
|
+
"secretsEdit",
|
|
1853
|
+
]);
|
|
1854
|
+
|
|
1855
|
+
function validateDashboardScreenPanel(
|
|
1856
|
+
featureName: string,
|
|
1857
|
+
screenId: string,
|
|
1858
|
+
panel: DashboardScreenPanel,
|
|
1859
|
+
featureMap: ReadonlyMap<string, FeatureDefinition>,
|
|
1860
|
+
): void {
|
|
1861
|
+
const context = `[Feature ${featureName}] Screen "${screenId}" (dashboard) screen-panel "${panel.id}"`;
|
|
1862
|
+
const targetQn = resolveScreenTargetQn(featureName, panel.screen);
|
|
1863
|
+
const [targetFeatureName = "", , targetShortId = ""] = targetQn.split(":");
|
|
1864
|
+
const target = featureMap.get(targetFeatureName)?.screens[targetShortId];
|
|
1865
|
+
if (target === undefined) {
|
|
1866
|
+
throw new Error(
|
|
1867
|
+
`${context} screen "${panel.screen}" does not resolve to a registered screen (checked "${targetQn}"). ` +
|
|
1868
|
+
`Use a same-feature short id or a cross-feature QN "<feature>:screen:<id>".`,
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1871
|
+
if (!EMBEDDABLE_SCREEN_TYPES.has(target.type)) {
|
|
1872
|
+
throw new Error(
|
|
1873
|
+
`${context} embeds "${targetQn}" of type "${target.type}", which can't be embedded — allowed: ` +
|
|
1874
|
+
`${[...EMBEDDABLE_SCREEN_TYPES].join(", ")}. Use a custom panel for app components.`,
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
const visibleWhen = panel.visibleWhen;
|
|
1878
|
+
if (visibleWhen !== undefined && (visibleWhen.query === "" || visibleWhen.field === "")) {
|
|
1879
|
+
throw new Error(`${context} visibleWhen needs a non-empty query and field.`);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1843
1883
|
function validateDashboardStatGroupPanel(
|
|
1844
1884
|
featureName: string,
|
|
1845
1885
|
screenId: string,
|
|
@@ -1882,7 +1922,10 @@ function validateDashboardCustomPanel(
|
|
|
1882
1922
|
function validateDashboardQueryPanel(
|
|
1883
1923
|
featureName: string,
|
|
1884
1924
|
screenId: string,
|
|
1885
|
-
panel: Exclude<
|
|
1925
|
+
panel: Exclude<
|
|
1926
|
+
DashboardPanelDefinition,
|
|
1927
|
+
DashboardStatGroupPanel | DashboardCustomPanel | DashboardScreenPanel
|
|
1928
|
+
>,
|
|
1886
1929
|
): void {
|
|
1887
1930
|
if (!panel.query || typeof panel.query !== "string") {
|
|
1888
1931
|
throw new Error(
|
package/src/engine/index.ts
CHANGED
|
@@ -333,8 +333,10 @@ export type {
|
|
|
333
333
|
DashboardFilterDefinition,
|
|
334
334
|
DashboardListPanel,
|
|
335
335
|
DashboardPanelDefinition,
|
|
336
|
+
DashboardPanelVisibility,
|
|
336
337
|
DashboardProgressListPanel,
|
|
337
338
|
DashboardScreenDefinition,
|
|
339
|
+
DashboardScreenPanel,
|
|
338
340
|
DashboardStatGroupPanel,
|
|
339
341
|
DashboardStatPanel,
|
|
340
342
|
DateFieldDef,
|
|
@@ -270,8 +270,10 @@ export type {
|
|
|
270
270
|
DashboardFilterDefinition,
|
|
271
271
|
DashboardListPanel,
|
|
272
272
|
DashboardPanelDefinition,
|
|
273
|
+
DashboardPanelVisibility,
|
|
273
274
|
DashboardProgressListPanel,
|
|
274
275
|
DashboardScreenDefinition,
|
|
276
|
+
DashboardScreenPanel,
|
|
275
277
|
DashboardStatGroupPanel,
|
|
276
278
|
DashboardStatPanel,
|
|
277
279
|
EditExtensionSection,
|
package/src/ui-types/index.ts
CHANGED
|
@@ -66,8 +66,10 @@ export type {
|
|
|
66
66
|
DashboardFilterDefinition,
|
|
67
67
|
DashboardListPanel,
|
|
68
68
|
DashboardPanelDefinition,
|
|
69
|
+
DashboardPanelVisibility,
|
|
69
70
|
DashboardProgressListPanel,
|
|
70
71
|
DashboardScreenDefinition,
|
|
72
|
+
DashboardScreenPanel,
|
|
71
73
|
DashboardStatGroupPanel,
|
|
72
74
|
DashboardStatPanel,
|
|
73
75
|
EditExtensionSection,
|