@cosmicdrift/kumiko-renderer 0.241.0 → 0.242.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",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.242.0",
|
|
4
4
|
"description": "Platform-agnostic React renderer for Kumiko screens. Contains the shared logic — primitives-contract, hooks, KumikoScreen, navigation & SSE abstractions — that any platform-specific renderer (web, native) composes. No DOM, no EventSource, no react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.242.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.242.0",
|
|
20
20
|
"react": "^19.2.6",
|
|
21
21
|
"temporal-polyfill": "^0.3.2",
|
|
22
22
|
"zod": "^4.4.3"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"@types/react-dom": "^19.2.3",
|
|
28
28
|
"jsdom": "^29.1.1",
|
|
29
29
|
"react-dom": "^19.2.6",
|
|
30
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
30
|
+
"@cosmicdrift/kumiko-locale-de": "0.242.0"
|
|
31
31
|
},
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|
|
@@ -3,7 +3,7 @@ import type { RowAction } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
|
3
3
|
import type { Dispatcher, EditRelatedListSectionViewModel } from "@cosmicdrift/kumiko-headless";
|
|
4
4
|
import { render, screen as rtlScreen, waitFor } from "@testing-library/react";
|
|
5
5
|
import type { ComponentType, ReactNode } from "react";
|
|
6
|
-
import { NavProvider } from "../../app/nav";
|
|
6
|
+
import { type NavApi, NavProvider } from "../../app/nav";
|
|
7
7
|
import { DispatcherProvider } from "../../context/dispatcher-context";
|
|
8
8
|
import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
|
|
9
9
|
import { kumikoDefaultTranslations } from "../../i18n-defaults";
|
|
@@ -26,22 +26,27 @@ const testSection: ComponentType<SectionProps> = ({ testId, children }) => (
|
|
|
26
26
|
// wired straight to the action's onTrigger — enough to prove
|
|
27
27
|
// RelatedListSection wires rowActions through to a real dispatch, without
|
|
28
28
|
// needing the production DataTable's sorting/paging/kebab-menu chrome.
|
|
29
|
+
// The isVisible filter mirrors the production DataTable's own row-action
|
|
30
|
+
// filter (renderer-web primitives/index.tsx) so per-row gating is exercised
|
|
31
|
+
// here rather than assumed.
|
|
29
32
|
const testDataTable: ComponentType<DataTableProps> = ({ rows, rowActions }) => (
|
|
30
33
|
<table>
|
|
31
34
|
<tbody>
|
|
32
35
|
{rows.map((row) => (
|
|
33
36
|
<tr key={row.id} data-testid={`row-${row.id}`}>
|
|
34
37
|
<td>
|
|
35
|
-
{(rowActions ?? [])
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
{(rowActions ?? [])
|
|
39
|
+
.filter((action) => action.isVisible === undefined || action.isVisible(row))
|
|
40
|
+
.map((action) => (
|
|
41
|
+
<button
|
|
42
|
+
key={action.id}
|
|
43
|
+
type="button"
|
|
44
|
+
data-testid={`action-${action.id}-${row.id}`}
|
|
45
|
+
onClick={() => void action.onTrigger(row)}
|
|
46
|
+
>
|
|
47
|
+
{action.label}
|
|
48
|
+
</button>
|
|
49
|
+
))}
|
|
45
50
|
</td>
|
|
46
51
|
</tr>
|
|
47
52
|
))}
|
|
@@ -74,7 +79,7 @@ function testPrimitives(): CorePrimitives {
|
|
|
74
79
|
} as unknown as CorePrimitives;
|
|
75
80
|
}
|
|
76
81
|
|
|
77
|
-
function stubDispatcher(): {
|
|
82
|
+
function stubDispatcher(rows: readonly Record<string, unknown>[] = [{ id: "r1", name: "Alice" }]): {
|
|
78
83
|
dispatcher: Dispatcher;
|
|
79
84
|
writes: Array<{ type: string; payload: unknown }>;
|
|
80
85
|
queryCount: () => number;
|
|
@@ -88,10 +93,7 @@ function stubDispatcher(): {
|
|
|
88
93
|
}) as Dispatcher["write"],
|
|
89
94
|
query: (async () => {
|
|
90
95
|
queryCalls += 1;
|
|
91
|
-
return {
|
|
92
|
-
isSuccess: true,
|
|
93
|
-
data: { rows: [{ id: "r1", name: "Alice" }], nextCursor: null },
|
|
94
|
-
};
|
|
96
|
+
return { isSuccess: true, data: { rows, nextCursor: null } };
|
|
95
97
|
}) as Dispatcher["query"],
|
|
96
98
|
batch: (async () => ({ isSuccess: true, results: [] })) as Dispatcher["batch"],
|
|
97
99
|
statusStore: {
|
|
@@ -105,6 +107,28 @@ function stubDispatcher(): {
|
|
|
105
107
|
return { dispatcher, writes, queryCount: () => queryCalls };
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
function stubNav(): {
|
|
111
|
+
nav: NavApi;
|
|
112
|
+
navigations: unknown[];
|
|
113
|
+
searchParams: Array<Record<string, string | null>>;
|
|
114
|
+
} {
|
|
115
|
+
const navigations: unknown[] = [];
|
|
116
|
+
const searchParams: Array<Record<string, string | null>> = [];
|
|
117
|
+
const nav: NavApi = {
|
|
118
|
+
route: undefined,
|
|
119
|
+
navigate: (target: unknown) => {
|
|
120
|
+
navigations.push(target);
|
|
121
|
+
},
|
|
122
|
+
replace: noop,
|
|
123
|
+
hrefFor: () => "#",
|
|
124
|
+
searchParams: {},
|
|
125
|
+
setSearchParams: (params: Record<string, string | null>) => {
|
|
126
|
+
searchParams.push(params);
|
|
127
|
+
},
|
|
128
|
+
} as unknown as NavApi;
|
|
129
|
+
return { nav, navigations, searchParams };
|
|
130
|
+
}
|
|
131
|
+
|
|
108
132
|
const rowActions: readonly RowAction[] = [
|
|
109
133
|
{
|
|
110
134
|
id: "resend",
|
|
@@ -122,7 +146,11 @@ const historySection: EditRelatedListSectionViewModel = {
|
|
|
122
146
|
rowActions,
|
|
123
147
|
};
|
|
124
148
|
|
|
125
|
-
function renderRelatedList(
|
|
149
|
+
function renderRelatedList(
|
|
150
|
+
dispatcher: Dispatcher,
|
|
151
|
+
section: EditRelatedListSectionViewModel = historySection,
|
|
152
|
+
nav: NavApi = stubNav().nav,
|
|
153
|
+
) {
|
|
126
154
|
return render(
|
|
127
155
|
<LocaleProvider
|
|
128
156
|
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
@@ -130,17 +158,8 @@ function renderRelatedList(dispatcher: Dispatcher) {
|
|
|
130
158
|
>
|
|
131
159
|
<DispatcherProvider dispatcher={dispatcher}>
|
|
132
160
|
<PrimitivesProvider value={testPrimitives()}>
|
|
133
|
-
<NavProvider
|
|
134
|
-
|
|
135
|
-
route: undefined,
|
|
136
|
-
navigate: noop,
|
|
137
|
-
replace: noop,
|
|
138
|
-
hrefFor: () => "#",
|
|
139
|
-
searchParams: {},
|
|
140
|
-
setSearchParams: noop,
|
|
141
|
-
}}
|
|
142
|
-
>
|
|
143
|
-
<RelatedListSection section={historySection} parentId="order-1" featureName="orders" />
|
|
161
|
+
<NavProvider value={nav}>
|
|
162
|
+
<RelatedListSection section={section} parentId="order-1" featureName="orders" />
|
|
144
163
|
</NavProvider>
|
|
145
164
|
</PrimitivesProvider>
|
|
146
165
|
</DispatcherProvider>
|
|
@@ -160,4 +179,61 @@ describe("RelatedListSection — rowActions", () => {
|
|
|
160
179
|
expect(writes[0]).toEqual({ type: "orders:write:resend", payload: { id: "r1" } });
|
|
161
180
|
await waitFor(() => expect(queryCount()).toBe(2));
|
|
162
181
|
});
|
|
182
|
+
|
|
183
|
+
test("a navigate row action targets its screen and carries the clicked row's own values as search params", async () => {
|
|
184
|
+
const { dispatcher } = stubDispatcher([{ id: "item-7", name: "Rent 2024", amount: 1200 }]);
|
|
185
|
+
const { nav, navigations, searchParams } = stubNav();
|
|
186
|
+
renderRelatedList(
|
|
187
|
+
dispatcher,
|
|
188
|
+
{
|
|
189
|
+
kind: "relatedList",
|
|
190
|
+
title: "Positions",
|
|
191
|
+
query: "lease:query:items:list",
|
|
192
|
+
columns: [{ field: "name" }],
|
|
193
|
+
rowActions: [
|
|
194
|
+
{
|
|
195
|
+
kind: "navigate",
|
|
196
|
+
id: "adjust-rent",
|
|
197
|
+
label: "actions.adjustRent",
|
|
198
|
+
screen: "adjust-rent-form",
|
|
199
|
+
params: { map: { itemId: "id", currentAmount: "amount" } },
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
nav,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
await waitFor(() => expect(rtlScreen.getByTestId("row-item-7")).toBeTruthy());
|
|
207
|
+
rtlScreen.getByTestId("action-adjust-rent-item-7").click();
|
|
208
|
+
|
|
209
|
+
await waitFor(() => expect(navigations).toHaveLength(1));
|
|
210
|
+
expect(navigations[0]).toEqual({ screenId: "adjust-rent-form" });
|
|
211
|
+
expect(searchParams).toEqual([{ itemId: "item-7", currentAmount: "1200" }]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("a row action with a visible condition renders only on the rows that satisfy it", async () => {
|
|
215
|
+
const { dispatcher } = stubDispatcher([
|
|
216
|
+
{ id: "active-1", name: "Running", status: "active" },
|
|
217
|
+
{ id: "ended-1", name: "Closed", status: "ended" },
|
|
218
|
+
]);
|
|
219
|
+
renderRelatedList(dispatcher, {
|
|
220
|
+
kind: "relatedList",
|
|
221
|
+
title: "Positions",
|
|
222
|
+
query: "lease:query:items:list",
|
|
223
|
+
columns: [{ field: "name" }],
|
|
224
|
+
rowActions: [
|
|
225
|
+
{
|
|
226
|
+
id: "end-item",
|
|
227
|
+
label: "actions.endItem",
|
|
228
|
+
handler: "lease:write:end-item",
|
|
229
|
+
payload: { pick: ["id"] },
|
|
230
|
+
visible: { field: "status", eq: "active" },
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
await waitFor(() => expect(rtlScreen.getByTestId("row-ended-1")).toBeTruthy());
|
|
236
|
+
expect(rtlScreen.getByTestId("action-end-item-active-1")).toBeTruthy();
|
|
237
|
+
expect(rtlScreen.queryByTestId("action-end-item-ended-1")).toBeNull();
|
|
238
|
+
});
|
|
163
239
|
});
|
package/src/primitives.tsx
CHANGED
|
@@ -448,8 +448,9 @@ export type InputProps =
|
|
|
448
448
|
readonly name: string;
|
|
449
449
|
readonly value: string;
|
|
450
450
|
readonly onChange: (v: string) => void;
|
|
451
|
-
/**
|
|
452
|
-
*
|
|
451
|
+
/** Visible rows. Acts as a minimum height — the field starts this tall
|
|
452
|
+
* and still grows with its content. Omitted, the default primitive
|
|
453
|
+
* falls back to its own floor (#2677). */
|
|
453
454
|
readonly rows?: number;
|
|
454
455
|
readonly disabled?: boolean;
|
|
455
456
|
readonly required?: boolean;
|