@cosmicdrift/kumiko-renderer-web 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-renderer-web",
3
- "version": "0.258.1",
3
+ "version": "0.259.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.258.1",
20
- "@cosmicdrift/kumiko-headless": "0.258.1",
21
- "@cosmicdrift/kumiko-renderer": "0.258.1",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.259.0",
20
+ "@cosmicdrift/kumiko-headless": "0.259.0",
21
+ "@cosmicdrift/kumiko-renderer": "0.259.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",
@@ -66,7 +66,7 @@
66
66
  "@types/react-dom": "^19.2.3",
67
67
  "jsdom": "^29.1.1",
68
68
  "tailwindcss": "^4.3.0",
69
- "@cosmicdrift/kumiko-locale-de": "0.258.1"
69
+ "@cosmicdrift/kumiko-locale-de": "0.259.0"
70
70
  },
71
71
  "repository": {
72
72
  "type": "git",
@@ -3,11 +3,13 @@ import type { DashboardScreenDefinition } from "@cosmicdrift/kumiko-framework/ui
3
3
  import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
4
4
  import type { ExtensionSectionProps, FeatureSchema } from "@cosmicdrift/kumiko-renderer";
5
5
  import {
6
+ AppFeaturesProvider,
6
7
  DashboardBodyProvider,
7
8
  DispatcherProvider,
8
9
  ExtensionSectionsProvider,
9
10
  KumikoScreen,
10
11
  NavProvider,
12
+ UserRolesProvider,
11
13
  } from "@cosmicdrift/kumiko-renderer";
12
14
  import userEvent from "@testing-library/user-event";
13
15
  import type { ReactNode } from "react";
@@ -527,3 +529,151 @@ describe("KumikoScreen dashboard — neue Panel-Kinds", () => {
527
529
  expect(kpiCall?.payload).toEqual({ tenantId: "t-2" });
528
530
  });
529
531
  });
532
+
533
+ const sessionsSchema: FeatureSchema = {
534
+ featureName: "sessions",
535
+ entities: {},
536
+ screens: [
537
+ {
538
+ id: "my-sessions",
539
+ type: "projectionList",
540
+ query: "sessions:query:user-session:mine",
541
+ columns: [{ field: "userAgent", label: "sessions.mine.col.userAgent" }],
542
+ access: { openToAll: true },
543
+ },
544
+ {
545
+ id: "session-list",
546
+ type: "projectionList",
547
+ query: "sessions:query:user-session:list",
548
+ columns: [{ field: "userAgent", label: "sessions.list.col.userAgent" }],
549
+ access: { roles: ["SystemAdmin"] },
550
+ },
551
+ ],
552
+ };
553
+
554
+ const mfaSchema: FeatureSchema = {
555
+ featureName: "auth-mfa",
556
+ entities: {},
557
+ screens: [
558
+ {
559
+ id: "auth-mfa-enable",
560
+ type: "secretMint",
561
+ handler: "auth-mfa:write:enable-start",
562
+ fields: {},
563
+ layout: { sections: [] },
564
+ submitLabel: "mfa.enable.start",
565
+ reveal: { fields: [{ field: "recoveryCodes", label: "codes", display: "list" }] },
566
+ access: { openToAll: true },
567
+ },
568
+ {
569
+ id: "auth-mfa-disable",
570
+ type: "actionForm",
571
+ handler: "auth-mfa:write:disable",
572
+ fields: { code: { type: "text", required: true } },
573
+ layout: { sections: [{ fields: ["code"] }] },
574
+ submitLabel: "mfa.disable.submit",
575
+ access: { openToAll: true },
576
+ },
577
+ ],
578
+ };
579
+
580
+ const mfaStatus = { query: "auth-mfa:query:user-mfa:status", field: "enabled" } as const;
581
+
582
+ const accountSecuritySchema: FeatureSchema = {
583
+ featureName: "account-security",
584
+ entities: {},
585
+ screens: [
586
+ {
587
+ id: "account-security",
588
+ type: "dashboard",
589
+ panels: [
590
+ {
591
+ kind: "screen",
592
+ id: "mfa-enable",
593
+ screen: "auth-mfa:screen:auth-mfa-enable",
594
+ visibleWhen: { ...mfaStatus, eq: false },
595
+ },
596
+ {
597
+ kind: "screen",
598
+ id: "mfa-disable",
599
+ screen: "auth-mfa:screen:auth-mfa-disable",
600
+ visibleWhen: { ...mfaStatus, eq: true },
601
+ },
602
+ {
603
+ kind: "screen",
604
+ id: "sessions",
605
+ screen: "sessions:screen:my-sessions",
606
+ label: "account-security:panel:sessions",
607
+ },
608
+ { kind: "screen", id: "admin-sessions", screen: "sessions:screen:session-list" },
609
+ ],
610
+ },
611
+ ],
612
+ };
613
+
614
+ function renderAccountSecurity(mfaEnabled: boolean): void {
615
+ const dispatcher = createMockDispatcher({
616
+ query: (async (type: string) => {
617
+ if (type === mfaStatus.query) return { isSuccess: true, data: { enabled: mfaEnabled } };
618
+ if (type === "sessions:query:user-session:mine") {
619
+ return {
620
+ isSuccess: true,
621
+ data: { rows: [{ id: "s1", userAgent: "Firefox on Linux" }], nextCursor: null },
622
+ };
623
+ }
624
+ return {
625
+ isSuccess: true,
626
+ data: { rows: [{ id: "x", userAgent: "ADMIN ROW" }], nextCursor: null },
627
+ };
628
+ }) as unknown as Dispatcher["query"],
629
+ });
630
+ render(
631
+ <DispatcherProvider dispatcher={dispatcher}>
632
+ <AppFeaturesProvider features={[accountSecuritySchema, sessionsSchema, mfaSchema]}>
633
+ <UserRolesProvider roles={["User"]}>
634
+ <BrowserNav>
635
+ <DashboardBodyProvider value={WebDashboardBody}>
636
+ <KumikoScreen
637
+ schema={accountSecuritySchema}
638
+ qn="account-security:screen:account-security"
639
+ />
640
+ </DashboardBodyProvider>
641
+ </BrowserNav>
642
+ </UserRolesProvider>
643
+ </AppFeaturesProvider>
644
+ </DispatcherProvider>,
645
+ );
646
+ }
647
+
648
+ describe("KumikoScreen dashboard — screen-Panels (fw#2841)", () => {
649
+ afterEach(() => {
650
+ window.history.replaceState(null, "", "/");
651
+ });
652
+
653
+ test("bettet einen Cross-Feature-projectionList-Screen samt Label ein", async () => {
654
+ renderAccountSecurity(false);
655
+ await waitFor(() => expect(screen.getByText("Firefox on Linux")).toBeTruthy());
656
+ expect(screen.getByTestId("dashboard-panel-sessions")).toBeTruthy();
657
+ expect(screen.getByText("account-security:panel:sessions")).toBeTruthy();
658
+ });
659
+
660
+ test("visibleWhen zeigt nur das Panel, das zum Query-Wert passt", async () => {
661
+ renderAccountSecurity(false);
662
+ await waitFor(() => expect(screen.getByTestId("dashboard-panel-mfa-enable")).toBeTruthy());
663
+ expect(screen.queryByTestId("dashboard-panel-mfa-disable")).toBeNull();
664
+ });
665
+
666
+ test("visibleWhen mit umgekehrtem Status blendet das jeweils andere Panel ein", async () => {
667
+ renderAccountSecurity(true);
668
+ await waitFor(() => expect(screen.getByTestId("dashboard-panel-mfa-disable")).toBeTruthy());
669
+ expect(screen.queryByTestId("dashboard-panel-mfa-enable")).toBeNull();
670
+ });
671
+
672
+ test("ohne Zugriff auf den Ziel-Screen fällt die Kachel komplett weg (kein Access-Banner)", async () => {
673
+ renderAccountSecurity(false);
674
+ await waitFor(() => expect(screen.getByText("Firefox on Linux")).toBeTruthy());
675
+ expect(screen.queryByTestId("dashboard-panel-admin-sessions")).toBeNull();
676
+ expect(screen.queryByText("ADMIN ROW")).toBeNull();
677
+ expect(screen.queryByTestId("kumiko-screen-access-denied")).toBeNull();
678
+ });
679
+ });
@@ -23,6 +23,9 @@
23
23
  // progress-list → { rows: { id, label, value, fraction }[] }
24
24
  // custom → keine Query — eine über extensionSectionComponents
25
25
  // registrierte App-Komponente holt sich ihre Daten selbst.
26
+ // screen → no own query; embeds another declarative screen via
27
+ // KumikoScreen. visibleWhen reads a flat record (live); the
28
+ // tile is dropped when the user can't access the target.
26
29
  //
27
30
  // Screen-Filter (DashboardFilterDefinition): der gewählte Wert wird unter
28
31
  // `filter.id` in JEDE Panel-Query gemerged. useQuery refetcht automatisch
@@ -36,13 +39,17 @@ import type {
36
39
  DashboardPanelDefinition,
37
40
  DashboardProgressListPanel,
38
41
  DashboardScreenDefinition,
42
+ DashboardScreenPanel,
39
43
  DashboardStatGroupPanel,
40
44
  DashboardStatPanel,
41
45
  } from "@cosmicdrift/kumiko-framework/ui-types";
42
46
  import { normalizeListColumn } from "@cosmicdrift/kumiko-framework/ui-types";
47
+ import type { Translate } from "@cosmicdrift/kumiko-headless";
43
48
  import {
44
49
  type DashboardBodyProps,
45
50
  extensionSectionName,
51
+ KumikoScreen,
52
+ useEmbeddedScreen,
46
53
  useExtensionSectionComponent,
47
54
  useNav,
48
55
  usePrimitives,
@@ -403,7 +410,7 @@ function PanelBody({
403
410
  screenId,
404
411
  filterParams,
405
412
  }: {
406
- readonly panel: DashboardPanelDefinition;
413
+ readonly panel: Exclude<DashboardPanelDefinition, DashboardScreenPanel>;
407
414
  readonly label: string;
408
415
  readonly screenId: string;
409
416
  readonly filterParams: Readonly<Record<string, unknown>>;
@@ -434,7 +441,43 @@ function PanelBody({
434
441
  return <CustomPanelBody panel={panel} screenId={screenId} filterParams={filterParams} />;
435
442
  }
436
443
 
437
- export function WebDashboardBody({ screen, translate }: DashboardBodyProps): ReactNode {
444
+ function ScreenPanelTile({
445
+ panel,
446
+ featureName,
447
+ translate,
448
+ }: {
449
+ readonly panel: DashboardScreenPanel;
450
+ readonly featureName: string;
451
+ readonly translate: Translate;
452
+ }): ReactNode {
453
+ const visibleWhen = panel.visibleWhen;
454
+ const visibility = useQuery<Readonly<Record<string, unknown>>>(
455
+ visibleWhen?.query ?? "",
456
+ {},
457
+ { enabled: visibleWhen !== undefined, live: true },
458
+ );
459
+ const target = useEmbeddedScreen(featureName, panel.screen);
460
+ if (target === undefined) return null;
461
+ if (visibleWhen !== undefined && visibility.data?.[visibleWhen.field] !== visibleWhen.eq) {
462
+ return null;
463
+ }
464
+ const embedded = <KumikoScreen schema={target.schema} qn={target.qn} translate={translate} />;
465
+ return (
466
+ <div className={WIDE_PANEL} data-testid={`dashboard-panel-${panel.id}`}>
467
+ {panel.label !== undefined ? (
468
+ <SectionCard title={translate(panel.label)}>{embedded}</SectionCard>
469
+ ) : (
470
+ embedded
471
+ )}
472
+ </div>
473
+ );
474
+ }
475
+
476
+ export function WebDashboardBody({
477
+ featureName,
478
+ screen,
479
+ translate,
480
+ }: DashboardBodyProps): ReactNode {
438
481
  const t = useTranslation();
439
482
  const effectiveTranslate = translate ?? t;
440
483
  const { Text } = usePrimitives();
@@ -449,6 +492,16 @@ export function WebDashboardBody({ screen, translate }: DashboardBodyProps): Rea
449
492
  {picker}
450
493
  <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
451
494
  {screen.panels.map((panel) => {
495
+ if (panel.kind === "screen") {
496
+ return (
497
+ <ScreenPanelTile
498
+ key={panel.id}
499
+ panel={panel}
500
+ featureName={featureName}
501
+ translate={effectiveTranslate}
502
+ />
503
+ );
504
+ }
452
505
  const label = panel.kind === "custom" ? "" : effectiveTranslate(panel.label);
453
506
  const span = panelSpanClassName(panel);
454
507
  return (