@cosmicdrift/kumiko-renderer 0.247.0 → 0.249.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.249.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.249.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.249.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.249.0"
|
|
31
31
|
},
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// renders the real entityList pipeline (KumikoScreen → EntityListBody) and
|
|
4
4
|
// asserts on the resolved `icon` KumikoScreen attaches to each
|
|
5
5
|
// DataTableRowAction — the id-derived default (ACTION_ICON_BY_ID in
|
|
6
|
-
// kumiko-screen.tsx) for an action with no declared icon,
|
|
7
|
-
//
|
|
6
|
+
// kumiko-screen.tsx) for an action with no declared icon, the declared icon
|
|
7
|
+
// overriding that default for one that has it, and the full id / last
|
|
8
|
+
// segment / first segment fallback order for compound ids (fw#2749).
|
|
8
9
|
|
|
9
10
|
import { describe, expect, test } from "bun:test";
|
|
10
11
|
import type {
|
|
@@ -101,6 +102,36 @@ function buildSchema(): FeatureSchema {
|
|
|
101
102
|
screen: "widget-edit",
|
|
102
103
|
icon: "archive",
|
|
103
104
|
},
|
|
105
|
+
// Full id and last segment ("item") have no entry — falls through to
|
|
106
|
+
// the first segment ("add" -> plus).
|
|
107
|
+
{
|
|
108
|
+
id: "add-item",
|
|
109
|
+
label: "Add Item",
|
|
110
|
+
handler: "icon-fixture:write:widget:add-item",
|
|
111
|
+
},
|
|
112
|
+
// Same fallback, different verb ("open" -> eye).
|
|
113
|
+
{
|
|
114
|
+
kind: "navigate",
|
|
115
|
+
id: "open-lease",
|
|
116
|
+
label: "Open Lease",
|
|
117
|
+
screen: "widget-edit",
|
|
118
|
+
},
|
|
119
|
+
// Neither the full id nor either segment ("order", "ship") has an
|
|
120
|
+
// entry — the new stage must not invent an icon.
|
|
121
|
+
{
|
|
122
|
+
kind: "navigate",
|
|
123
|
+
id: "order-ship",
|
|
124
|
+
label: "Order Ship",
|
|
125
|
+
screen: "widget-edit",
|
|
126
|
+
},
|
|
127
|
+
// Last segment ("copy") and first segment ("send") both have entries —
|
|
128
|
+
// the last segment must keep priority over the new first-segment stage.
|
|
129
|
+
{
|
|
130
|
+
kind: "navigate",
|
|
131
|
+
id: "send-copy",
|
|
132
|
+
label: "Send Copy",
|
|
133
|
+
screen: "widget-edit",
|
|
134
|
+
},
|
|
104
135
|
],
|
|
105
136
|
};
|
|
106
137
|
return {
|
|
@@ -166,4 +197,52 @@ describe("entityList rowActions resolve an icon (fw-ui-defaults)", () => {
|
|
|
166
197
|
|
|
167
198
|
expect(requireAction("edit").icon).toBe("archive");
|
|
168
199
|
});
|
|
200
|
+
|
|
201
|
+
test("an id whose full form and last segment miss falls back to its first segment ('add-item' -> plus)", async () => {
|
|
202
|
+
capturedRowActions = undefined;
|
|
203
|
+
|
|
204
|
+
renderListScreen();
|
|
205
|
+
|
|
206
|
+
await waitFor(() => {
|
|
207
|
+
expect(capturedRowActions).toBeDefined();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
expect(requireAction("add-item").icon).toBe("plus");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("the first-segment fallback also applies to 'open-lease' -> eye", async () => {
|
|
214
|
+
capturedRowActions = undefined;
|
|
215
|
+
|
|
216
|
+
renderListScreen();
|
|
217
|
+
|
|
218
|
+
await waitFor(() => {
|
|
219
|
+
expect(capturedRowActions).toBeDefined();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(requireAction("open-lease").icon).toBe("eye");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("an id with no matching segment stays without an icon ('order-ship')", async () => {
|
|
226
|
+
capturedRowActions = undefined;
|
|
227
|
+
|
|
228
|
+
renderListScreen();
|
|
229
|
+
|
|
230
|
+
await waitFor(() => {
|
|
231
|
+
expect(capturedRowActions).toBeDefined();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(requireAction("order-ship").icon).toBeUndefined();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("the last segment keeps priority over the first ('send-copy' -> copy, not send)", async () => {
|
|
238
|
+
capturedRowActions = undefined;
|
|
239
|
+
|
|
240
|
+
renderListScreen();
|
|
241
|
+
|
|
242
|
+
await waitFor(() => {
|
|
243
|
+
expect(capturedRowActions).toBeDefined();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
expect(requireAction("send-copy").icon).toBe("copy");
|
|
247
|
+
});
|
|
169
248
|
});
|
package/src/app/row-actions.ts
CHANGED
|
@@ -57,19 +57,29 @@ const ACTION_ICON_BY_ID: Readonly<Partial<Record<string, IconKey>>> = {
|
|
|
57
57
|
send: "send",
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
-
// Ids are kebab-case (RowAction.id doc)
|
|
61
|
-
//
|
|
60
|
+
// Ids are kebab-case (RowAction.id doc): aggregate-object-verb
|
|
61
|
+
// ("order-ship") or verb-prefix ("add-item").
|
|
62
62
|
function kebabLastSegment(id: string): string {
|
|
63
63
|
const idx = id.lastIndexOf("-");
|
|
64
64
|
return idx === -1 ? id : id.slice(idx + 1);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
function kebabFirstSegment(id: string): string {
|
|
68
|
+
const idx = id.indexOf("-");
|
|
69
|
+
return idx === -1 ? id : id.slice(0, idx);
|
|
70
|
+
}
|
|
71
|
+
|
|
67
72
|
// Resolution order: author-declared `icon` wins, then the id-derived
|
|
68
|
-
// default (full id, then its last kebab segment
|
|
69
|
-
// for ToolbarAction, which has no
|
|
73
|
+
// default (full id, then its last kebab segment, then its first kebab
|
|
74
|
+
// segment). `declared` is `undefined` for ToolbarAction, which has no
|
|
75
|
+
// author-facing icon field.
|
|
70
76
|
export function resolveActionIcon(id: string, declared?: IconKey): IconKey | undefined {
|
|
71
77
|
if (declared !== undefined) return declared;
|
|
72
|
-
return
|
|
78
|
+
return (
|
|
79
|
+
ACTION_ICON_BY_ID[id] ??
|
|
80
|
+
ACTION_ICON_BY_ID[kebabLastSegment(id)] ??
|
|
81
|
+
ACTION_ICON_BY_ID[kebabFirstSegment(id)]
|
|
82
|
+
);
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
// Row-action column mode for a resolved action set: a group where every
|