@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.
|
|
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.
|
|
20
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
21
|
-
"@cosmicdrift/kumiko-renderer": "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.
|
|
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
|
-
<
|
|
1652
|
-
<
|
|
1653
|
-
schema={
|
|
1654
|
-
|
|
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
|
-
//
|
|
1665
|
-
//
|
|
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({
|