@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.258.1",
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.258.1",
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.258.1",
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<DashboardPanelDefinition, DashboardStatGroupPanel | DashboardCustomPanel>,
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(
@@ -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,
@@ -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,