@cosmicdrift/kumiko-renderer-web 0.242.0 → 0.243.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.242.0",
3
+ "version": "0.243.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.242.0",
20
- "@cosmicdrift/kumiko-headless": "0.242.0",
21
- "@cosmicdrift/kumiko-renderer": "0.242.0",
19
+ "@cosmicdrift/kumiko-dispatcher-live": "0.243.1",
20
+ "@cosmicdrift/kumiko-headless": "0.243.1",
21
+ "@cosmicdrift/kumiko-renderer": "0.243.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.242.0"
67
+ "@cosmicdrift/kumiko-locale-de": "0.243.1"
68
68
  },
69
69
  "repository": {
70
70
  "type": "git",
@@ -1889,6 +1889,114 @@ describe("KumikoScreen", () => {
1889
1889
  expect(navigateCalls[0]).toEqual({ screenId: "task-edit", entityId: "x" });
1890
1890
  });
1891
1891
 
1892
+ // #2670: a handler that writes a child record reports the child's id, but
1893
+ // the redirect target is the parent's detail screen — `idFrom` names the
1894
+ // payload field the navigation id comes from, so the handler keeps
1895
+ // reporting what it actually wrote.
1896
+ test("actionForm mit redirect-Objekt + idFrom: navigiert mit der Eltern-ID statt data.id (#2670)", async () => {
1897
+ const navigateCalls: NavTarget[] = [];
1898
+ const dispatcher = makeDispatcher({
1899
+ write: (async () => ({
1900
+ isSuccess: true,
1901
+ data: { id: "child", leaseId: "parent" },
1902
+ })) as unknown as Dispatcher["write"],
1903
+ });
1904
+ const memoryNav = {
1905
+ route: { screenId: "quick-add" },
1906
+ navigate: (target: NavTarget) => {
1907
+ if ("screenId" in target) navigateCalls.push(target);
1908
+ },
1909
+ replace: () => undefined,
1910
+ hrefFor: (t: NavTarget) => ("screenId" in t ? `/${t.screenId}` : ""),
1911
+ searchParams: {},
1912
+ setSearchParams: () => undefined,
1913
+ };
1914
+ const editScreen: EntityEditScreenDefinition = {
1915
+ id: "task-edit",
1916
+ type: "entityEdit",
1917
+ entity: "task",
1918
+ layout: { sections: [{ title: "x", fields: ["title"] }] },
1919
+ };
1920
+ const actionScreen: ActionFormScreenDefinition = {
1921
+ id: "quick-add",
1922
+ type: "actionForm",
1923
+ handler: "tasks:write:task:quick-add",
1924
+ fields: { title: { type: "text", required: true } },
1925
+ layout: { sections: [{ title: "x", fields: ["title"] }] },
1926
+ redirect: { screen: "task-edit", idFrom: "leaseId" },
1927
+ };
1928
+
1929
+ const { NavProvider } = await import("@cosmicdrift/kumiko-renderer");
1930
+ render(
1931
+ <NavProvider value={memoryNav}>
1932
+ <DispatcherProvider dispatcher={dispatcher}>
1933
+ <KumikoScreen
1934
+ schema={{ ...schema, screens: [actionScreen, editScreen, listScreen] }}
1935
+ qn="tasks:screen:quick-add"
1936
+ />
1937
+ </DispatcherProvider>
1938
+ </NavProvider>,
1939
+ );
1940
+
1941
+ const titleInput = screen.getByTestId("field-title").querySelector("input") as HTMLInputElement;
1942
+ fireEvent.change(titleInput, { target: { value: "go" } });
1943
+ await clickSubmitOnceEnabled();
1944
+ await waitFor(() => expect(navigateCalls.length).toBe(1));
1945
+ expect(navigateCalls[0]).toEqual({ screenId: "task-edit", entityId: "parent" });
1946
+ });
1947
+
1948
+ test("actionForm mit blankem String-redirect: navigiert weiter mit data.id, nicht mit einem anderen Payload-Feld (#2670)", async () => {
1949
+ const navigateCalls: NavTarget[] = [];
1950
+ const dispatcher = makeDispatcher({
1951
+ write: (async () => ({
1952
+ isSuccess: true,
1953
+ data: { id: "child", leaseId: "parent" },
1954
+ })) as unknown as Dispatcher["write"],
1955
+ });
1956
+ const memoryNav = {
1957
+ route: { screenId: "quick-add" },
1958
+ navigate: (target: NavTarget) => {
1959
+ if ("screenId" in target) navigateCalls.push(target);
1960
+ },
1961
+ replace: () => undefined,
1962
+ hrefFor: (t: NavTarget) => ("screenId" in t ? `/${t.screenId}` : ""),
1963
+ searchParams: {},
1964
+ setSearchParams: () => undefined,
1965
+ };
1966
+ const editScreen: EntityEditScreenDefinition = {
1967
+ id: "task-edit",
1968
+ type: "entityEdit",
1969
+ entity: "task",
1970
+ layout: { sections: [{ title: "x", fields: ["title"] }] },
1971
+ };
1972
+ const actionScreen: ActionFormScreenDefinition = {
1973
+ id: "quick-add",
1974
+ type: "actionForm",
1975
+ handler: "tasks:write:task:quick-add",
1976
+ fields: { title: { type: "text", required: true } },
1977
+ layout: { sections: [{ title: "x", fields: ["title"] }] },
1978
+ redirect: "task-edit",
1979
+ };
1980
+
1981
+ const { NavProvider } = await import("@cosmicdrift/kumiko-renderer");
1982
+ render(
1983
+ <NavProvider value={memoryNav}>
1984
+ <DispatcherProvider dispatcher={dispatcher}>
1985
+ <KumikoScreen
1986
+ schema={{ ...schema, screens: [actionScreen, editScreen, listScreen] }}
1987
+ qn="tasks:screen:quick-add"
1988
+ />
1989
+ </DispatcherProvider>
1990
+ </NavProvider>,
1991
+ );
1992
+
1993
+ const titleInput = screen.getByTestId("field-title").querySelector("input") as HTMLInputElement;
1994
+ fireEvent.change(titleInput, { target: { value: "go" } });
1995
+ await clickSubmitOnceEnabled();
1996
+ await waitFor(() => expect(navigateCalls.length).toBe(1));
1997
+ expect(navigateCalls[0]).toEqual({ screenId: "task-edit", entityId: "child" });
1998
+ });
1999
+
1892
2000
  // #2416: entityEdit-create passes the newly created record's id through to
1893
2001
  // the post-submit navigate as `entityId` (extractCreatedId); actionForm's
1894
2002
  // redirect did the same navigate but never extracted the id. The with-id
@@ -54,7 +54,12 @@ globalThis.IntersectionObserver = class {
54
54
  disconnect(): void {}
55
55
  } as unknown as typeof IntersectionObserver;
56
56
 
57
- function fireIntersect(): void {
57
+ // Waiting for the row alone races the observer-registering effect; each page
58
+ // registers a fresh observer, so wait for one more than we last fired.
59
+ let firedObservers = 0;
60
+ async function fireIntersect(): Promise<void> {
61
+ await waitFor(() => expect(observers.length).toBeGreaterThan(firedObservers));
62
+ firedObservers = observers.length;
58
63
  observers[observers.length - 1]?.([{ isIntersecting: true }]);
59
64
  }
60
65
 
@@ -118,7 +123,7 @@ describe("InfinityList", () => {
118
123
  });
119
124
  renderWithDispatcher(list("inbox:query:message:list"), dispatcher);
120
125
  await waitFor(() => expect(screen.getByText("Erste")).toBeTruthy());
121
- fireIntersect();
126
+ await fireIntersect();
122
127
  await waitFor(() => expect(screen.getByText("Zweite")).toBeTruthy());
123
128
  expect(screen.getByText("Erste")).toBeTruthy();
124
129
  expect(calls).toBe(2);
@@ -157,7 +162,7 @@ describe("InfinityList", () => {
157
162
  });
158
163
  renderWithDispatcher(list("inbox:query:message:list"), dispatcher);
159
164
  await waitFor(() => expect(screen.getByText("Zweite")).toBeTruthy());
160
- fireIntersect();
165
+ await fireIntersect();
161
166
  await waitFor(() => expect(screen.getByText("Dritte")).toBeTruthy());
162
167
  expect(screen.getAllByText("Zweite")).toHaveLength(1);
163
168
  });
@@ -316,7 +321,7 @@ describe("InfinityList", () => {
316
321
  );
317
322
 
318
323
  await waitFor(() => expect(screen.getByText("Alt-1")).toBeTruthy());
319
- fireIntersect();
324
+ await fireIntersect();
320
325
  await waitFor(() => expect(screen.getByText("Alt-2")).toBeTruthy());
321
326
  expect(calls.length).toBe(2);
322
327
 
@@ -434,7 +439,7 @@ describe("InfinityList", () => {
434
439
  );
435
440
 
436
441
  await waitFor(() => expect(screen.getByText("Erste")).toBeTruthy());
437
- fireIntersect();
442
+ await fireIntersect();
438
443
  await waitFor(() => expect(screen.getByText("Zweite")).toBeTruthy());
439
444
  expect(calls.length).toBe(2);
440
445