@cosmicdrift/kumiko-renderer-web 0.223.0 → 0.224.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.223.0",
3
+ "version": "0.224.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.223.0",
20
- "@cosmicdrift/kumiko-headless": "0.223.0",
21
- "@cosmicdrift/kumiko-renderer": "0.223.0",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.224.1",
20
+ "@cosmicdrift/kumiko-headless": "0.224.1",
21
+ "@cosmicdrift/kumiko-renderer": "0.224.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.223.0"
67
+ "@cosmicdrift/kumiko-locale-de": "0.224.1"
68
68
  },
69
69
  "repository": {
70
70
  "type": "git",
@@ -8,6 +8,7 @@ import type {
8
8
  import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
9
9
  import type { FeatureSchema, NavApi, NavTarget } from "@cosmicdrift/kumiko-renderer";
10
10
  import {
11
+ AppFeaturesProvider,
11
12
  DispatcherProvider,
12
13
  ExtensionSectionsProvider,
13
14
  KumikoScreen,
@@ -1618,7 +1619,7 @@ describe("KumikoScreen", () => {
1618
1619
  expect(Object.hasOwn(navigateCalls[0] ?? {}, "entityId")).toBe(false);
1619
1620
  });
1620
1621
 
1621
- test("actionForm mit Cross-Feature-QN als redirect: navigiert per Short-Id (#1946)", async () => {
1622
+ test("actionForm mit Cross-Feature-QN als redirect auf entityList: navigiert per Short-Id ohne entityId (#1946, #2485)", async () => {
1622
1623
  const navigateCalls: NavTarget[] = [];
1623
1624
  const dispatcher = makeDispatcher({
1624
1625
  write: (async () => ({
@@ -1644,16 +1645,27 @@ describe("KumikoScreen", () => {
1644
1645
  layout: { sections: [{ title: "x", fields: ["title"] }] },
1645
1646
  redirect: "statements:screen:statement-upload-list",
1646
1647
  };
1648
+ const statementListScreen: EntityListScreenDefinition = {
1649
+ id: "statement-upload-list",
1650
+ type: "entityList",
1651
+ entity: "statement",
1652
+ columns: ["title"],
1653
+ };
1654
+ const statementsSchema: FeatureSchema = {
1655
+ featureName: "statements",
1656
+ entities: { statement: taskEntity },
1657
+ screens: [statementListScreen],
1658
+ };
1659
+ const tasksSchema: FeatureSchema = { ...schema, screens: [actionScreen, listScreen] };
1647
1660
 
1648
1661
  const { NavProvider } = await import("@cosmicdrift/kumiko-renderer");
1649
1662
  render(
1650
1663
  <NavProvider value={memoryNav}>
1651
- <DispatcherProvider dispatcher={dispatcher}>
1652
- <KumikoScreen
1653
- schema={{ ...schema, screens: [actionScreen, listScreen] }}
1654
- qn="tasks:screen:quick-add"
1655
- />
1656
- </DispatcherProvider>
1664
+ <AppFeaturesProvider features={[tasksSchema, statementsSchema]}>
1665
+ <DispatcherProvider dispatcher={dispatcher}>
1666
+ <KumikoScreen schema={tasksSchema} qn="tasks:screen:quick-add" />
1667
+ </DispatcherProvider>
1668
+ </AppFeaturesProvider>
1657
1669
  </NavProvider>,
1658
1670
  );
1659
1671
 
@@ -1661,12 +1673,156 @@ describe("KumikoScreen", () => {
1661
1673
  fireEvent.change(titleInput, { target: { value: "go" } });
1662
1674
  fireEvent.click(screen.getByTestId("render-edit-submit"));
1663
1675
  await waitFor(() => expect(navigateCalls.length).toBe(1));
1664
- // Cross-feature QN is not in this feature's schema.screens — keep prior
1665
- // behavior without entityId (#2419).
1676
+ // #2485: the cross-feature target now resolves (over all mounted
1677
+ // features) it just happens to be an entityList, which never carries
1678
+ // an id, same as a same-feature entityList redirect above.
1666
1679
  expect(navigateCalls[0]).toEqual({ screenId: "statement-upload-list" });
1667
1680
  expect(Object.hasOwn(navigateCalls[0] ?? {}, "entityId")).toBe(false);
1668
1681
  });
1669
1682
 
1683
+ test("actionForm mit Cross-Feature-QN als redirect auf entityEdit: hängt entityId an (#2485)", async () => {
1684
+ const navigateCalls: NavTarget[] = [];
1685
+ const dispatcher = makeDispatcher({
1686
+ write: (async () => ({
1687
+ isSuccess: true,
1688
+ data: { id: "x" },
1689
+ })) as unknown as Dispatcher["write"],
1690
+ });
1691
+ const memoryNav = {
1692
+ route: { screenId: "vehicle-start" },
1693
+ navigate: (target: NavTarget) => {
1694
+ if ("screenId" in target) navigateCalls.push(target);
1695
+ },
1696
+ replace: () => undefined,
1697
+ hrefFor: (t: NavTarget) => ("screenId" in t ? `/${t.screenId}` : ""),
1698
+ searchParams: {},
1699
+ setSearchParams: () => undefined,
1700
+ };
1701
+ const decodeScreen: ActionFormScreenDefinition = {
1702
+ id: "vehicle-start",
1703
+ type: "actionForm",
1704
+ handler: "vehicle-vin-decode:write:vehicle:decode",
1705
+ fields: { vin: { type: "text", required: true } },
1706
+ layout: { sections: [{ title: "x", fields: ["vin"] }] },
1707
+ redirect: "vehicles:screen:vehicle-wizard",
1708
+ };
1709
+ const decodeSchema: FeatureSchema = {
1710
+ featureName: "vehicle-vin-decode",
1711
+ entities: { task: taskEntity },
1712
+ screens: [decodeScreen],
1713
+ };
1714
+ const wizardScreen: EntityEditScreenDefinition = {
1715
+ id: "vehicle-wizard",
1716
+ type: "entityEdit",
1717
+ entity: "vehicle",
1718
+ layout: { sections: [{ title: "x", fields: ["title"] }] },
1719
+ };
1720
+ const vehiclesSchema: FeatureSchema = {
1721
+ featureName: "vehicles",
1722
+ entities: { vehicle: taskEntity },
1723
+ screens: [wizardScreen],
1724
+ };
1725
+
1726
+ const { NavProvider } = await import("@cosmicdrift/kumiko-renderer");
1727
+ render(
1728
+ <NavProvider value={memoryNav}>
1729
+ <AppFeaturesProvider features={[decodeSchema, vehiclesSchema]}>
1730
+ <DispatcherProvider dispatcher={dispatcher}>
1731
+ <KumikoScreen schema={decodeSchema} qn="vehicle-vin-decode:screen:vehicle-start" />
1732
+ </DispatcherProvider>
1733
+ </AppFeaturesProvider>
1734
+ </NavProvider>,
1735
+ );
1736
+
1737
+ const vinInput = screen.getByTestId("field-vin").querySelector("input") as HTMLInputElement;
1738
+ fireEvent.change(vinInput, { target: { value: "1HGCM82633A004352" } });
1739
+ fireEvent.click(screen.getByTestId("render-edit-submit"));
1740
+ await waitFor(() => expect(navigateCalls.length).toBe(1));
1741
+ // #2485: redirect crosses a feature boundary (vehicle-vin-decode →
1742
+ // vehicles) — the target is only found by searching all mounted
1743
+ // features, and it must still carry the created id since it's an
1744
+ // entityEdit screen.
1745
+ expect(navigateCalls[0]).toEqual({ screenId: "vehicle-wizard", entityId: "x" });
1746
+ });
1747
+
1748
+ test("actionForm mit Cross-Feature-QN als redirect: löst den Screen aus der im QN benannten Feature auf, nicht aus dem ersten Id-Treffer (#2485)", async () => {
1749
+ const navigateCalls: NavTarget[] = [];
1750
+ const dispatcher = makeDispatcher({
1751
+ write: (async () => ({
1752
+ isSuccess: true,
1753
+ data: { id: "x" },
1754
+ })) as unknown as Dispatcher["write"],
1755
+ });
1756
+ const memoryNav = {
1757
+ route: { screenId: "vehicle-start" },
1758
+ navigate: (target: NavTarget) => {
1759
+ if ("screenId" in target) navigateCalls.push(target);
1760
+ },
1761
+ replace: () => undefined,
1762
+ hrefFor: (t: NavTarget) => ("screenId" in t ? `/${t.screenId}` : ""),
1763
+ searchParams: {},
1764
+ setSearchParams: () => undefined,
1765
+ };
1766
+ const decodeScreen: ActionFormScreenDefinition = {
1767
+ id: "vehicle-start",
1768
+ type: "actionForm",
1769
+ handler: "vehicle-vin-decode:write:vehicle:decode",
1770
+ fields: { vin: { type: "text", required: true } },
1771
+ layout: { sections: [{ title: "x", fields: ["vin"] }] },
1772
+ redirect: "vehicles:screen:wizard",
1773
+ };
1774
+ const decodeSchema: FeatureSchema = {
1775
+ featureName: "vehicle-vin-decode",
1776
+ entities: { task: taskEntity },
1777
+ screens: [decodeScreen],
1778
+ };
1779
+ // Same short id ("wizard") as the real target, but a DIFFERENT feature
1780
+ // and a DIFFERENT type (entityList never carries an id) — mounted
1781
+ // BEFORE "vehicles" so a naive first-match-by-short-id across all
1782
+ // features would pick this one and silently drop the entityId.
1783
+ const decoyWizardScreen: EntityListScreenDefinition = {
1784
+ id: "wizard",
1785
+ type: "entityList",
1786
+ entity: "task",
1787
+ columns: ["title"],
1788
+ };
1789
+ const decoySchema: FeatureSchema = {
1790
+ featureName: "decoy",
1791
+ entities: { task: taskEntity },
1792
+ screens: [decoyWizardScreen],
1793
+ };
1794
+ const wizardScreen: EntityEditScreenDefinition = {
1795
+ id: "wizard",
1796
+ type: "entityEdit",
1797
+ entity: "vehicle",
1798
+ layout: { sections: [{ title: "x", fields: ["title"] }] },
1799
+ };
1800
+ const vehiclesSchema: FeatureSchema = {
1801
+ featureName: "vehicles",
1802
+ entities: { vehicle: taskEntity },
1803
+ screens: [wizardScreen],
1804
+ };
1805
+
1806
+ const { NavProvider } = await import("@cosmicdrift/kumiko-renderer");
1807
+ render(
1808
+ <NavProvider value={memoryNav}>
1809
+ <AppFeaturesProvider features={[decodeSchema, decoySchema, vehiclesSchema]}>
1810
+ <DispatcherProvider dispatcher={dispatcher}>
1811
+ <KumikoScreen schema={decodeSchema} qn="vehicle-vin-decode:screen:vehicle-start" />
1812
+ </DispatcherProvider>
1813
+ </AppFeaturesProvider>
1814
+ </NavProvider>,
1815
+ );
1816
+
1817
+ const vinInput = screen.getByTestId("field-vin").querySelector("input") as HTMLInputElement;
1818
+ fireEvent.change(vinInput, { target: { value: "1HGCM82633A004352" } });
1819
+ fireEvent.click(screen.getByTestId("render-edit-submit"));
1820
+ await waitFor(() => expect(navigateCalls.length).toBe(1));
1821
+ // The QN names "vehicles" explicitly — the decoy feature's same-id
1822
+ // screen (mounted first) must not win just because it comes first.
1823
+ expect(navigateCalls[0]).toEqual({ screenId: "wizard", entityId: "x" });
1824
+ });
1825
+
1670
1826
  test("actionForm mit redirect auf entityEdit: hängt entityId an (#2419)", async () => {
1671
1827
  const navigateCalls: NavTarget[] = [];
1672
1828
  const dispatcher = makeDispatcher({