@cosmicdrift/kumiko-renderer 0.328.0 → 0.328.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.
Files changed (259) hide show
  1. package/dist/app/action-form-shim.d.ts +14 -0
  2. package/{src/app/action-form-shim.ts → dist/app/action-form-shim.js} +26 -46
  3. package/dist/app/app-features-context.d.ts +8 -0
  4. package/dist/app/app-features-context.js +16 -0
  5. package/dist/app/column-renderers.d.ts +30 -0
  6. package/dist/app/column-renderers.js +34 -0
  7. package/dist/app/config-edit-shim.d.ts +11 -0
  8. package/{src/app/config-edit-shim.ts → dist/app/config-edit-shim.js} +12 -25
  9. package/dist/app/content-editors.d.ts +38 -0
  10. package/dist/app/content-editors.js +26 -0
  11. package/dist/app/content-preview.d.ts +11 -0
  12. package/dist/app/content-preview.js +30 -0
  13. package/dist/app/custom-screens.d.ts +11 -0
  14. package/dist/app/custom-screens.js +20 -0
  15. package/dist/app/dashboard-body.d.ts +16 -0
  16. package/dist/app/dashboard-body.js +11 -0
  17. package/dist/app/extension-form-submit.d.ts +32 -0
  18. package/dist/app/extension-form-submit.js +73 -0
  19. package/dist/app/extension-sections.d.ts +72 -0
  20. package/dist/app/extension-sections.js +34 -0
  21. package/dist/app/feature-schema.d.ts +4 -0
  22. package/{src/app/feature-schema.ts → dist/app/feature-schema.js} +12 -22
  23. package/dist/app/form-schema.d.ts +4 -0
  24. package/dist/app/form-schema.js +85 -0
  25. package/dist/app/kumiko-screen.d.ts +43 -0
  26. package/dist/app/kumiko-screen.js +2206 -0
  27. package/dist/app/layout-fields.d.ts +3 -0
  28. package/dist/app/layout-fields.js +20 -0
  29. package/dist/app/list-facets.d.ts +48 -0
  30. package/dist/app/list-facets.js +185 -0
  31. package/dist/app/nav.d.ts +61 -0
  32. package/dist/app/nav.js +139 -0
  33. package/dist/app/projection-detail-shim.d.ts +8 -0
  34. package/dist/app/projection-detail-shim.js +71 -0
  35. package/dist/app/projection-list-shim.d.ts +13 -0
  36. package/{src/app/projection-list-shim.ts → dist/app/projection-list-shim.js} +26 -41
  37. package/dist/app/qn.d.ts +7 -0
  38. package/{src/app/qn.ts → dist/app/qn.js} +9 -11
  39. package/dist/app/qualify-screen-id.d.ts +2 -0
  40. package/{src/app/qualify-screen-id.ts → dist/app/qualify-screen-id.js} +5 -6
  41. package/dist/app/reference-facet-bridge.d.ts +10 -0
  42. package/dist/app/reference-facet-bridge.js +22 -0
  43. package/dist/app/return-to.d.ts +31 -0
  44. package/dist/app/return-to.js +188 -0
  45. package/dist/app/row-actions.d.ts +77 -0
  46. package/dist/app/row-actions.js +416 -0
  47. package/dist/app/screen-access.d.ts +8 -0
  48. package/dist/app/screen-access.js +48 -0
  49. package/dist/app/secret-mint-body.d.ts +10 -0
  50. package/dist/app/secret-mint-body.js +140 -0
  51. package/dist/app/secrets-edit-body.d.ts +8 -0
  52. package/dist/app/secrets-edit-body.js +76 -0
  53. package/dist/app/use-embedded-screen.d.ts +6 -0
  54. package/dist/app/use-embedded-screen.js +19 -0
  55. package/dist/app/variable-chips.d.ts +7 -0
  56. package/dist/app/variable-chips.js +16 -0
  57. package/dist/app/write-failed-error.d.ts +6 -0
  58. package/dist/app/write-failed-error.js +21 -0
  59. package/dist/components/embedded-list-field.d.ts +10 -0
  60. package/dist/components/embedded-list-field.js +259 -0
  61. package/dist/components/grid-cell-for-field.d.ts +25 -0
  62. package/dist/components/grid-cell-for-field.js +31 -0
  63. package/dist/components/multi-select-checkboxes.d.ts +14 -0
  64. package/dist/components/multi-select-checkboxes.js +36 -0
  65. package/dist/components/reference-create-dialog.d.ts +13 -0
  66. package/dist/components/reference-create-dialog.js +46 -0
  67. package/dist/components/related-list-section.d.ts +25 -0
  68. package/dist/components/related-list-section.js +273 -0
  69. package/dist/components/render-edit-action-button.d.ts +22 -0
  70. package/dist/components/render-edit-action-button.js +43 -0
  71. package/dist/components/render-edit-logic.d.ts +6 -0
  72. package/dist/components/render-edit-logic.js +63 -0
  73. package/dist/components/render-edit-types.d.ts +209 -0
  74. package/dist/components/render-edit-types.js +1 -0
  75. package/dist/components/render-edit.d.ts +6 -0
  76. package/dist/components/render-edit.js +1169 -0
  77. package/dist/components/render-field.d.ts +38 -0
  78. package/dist/components/render-field.js +544 -0
  79. package/dist/components/render-list.d.ts +117 -0
  80. package/dist/components/render-list.js +232 -0
  81. package/dist/components/write-form-section.d.ts +17 -0
  82. package/dist/components/write-form-section.js +104 -0
  83. package/dist/context/dispatcher-context.d.ts +10 -0
  84. package/{src/context/dispatcher-context.tsx → dist/context/dispatcher-context.js} +16 -29
  85. package/dist/context/draft-storage-context.d.ts +12 -0
  86. package/dist/context/draft-storage-context.js +18 -0
  87. package/dist/context/user-roles-context.d.ts +7 -0
  88. package/{src/context/user-roles-context.tsx → dist/context/user-roles-context.js} +7 -15
  89. package/dist/format-when.d.ts +1 -0
  90. package/{src/format-when.ts → dist/format-when.js} +7 -7
  91. package/dist/hooks/reference-limits.d.ts +3 -0
  92. package/{src/hooks/reference-limits.ts → dist/hooks/reference-limits.js} +0 -1
  93. package/dist/hooks/use-ai-text.d.ts +52 -0
  94. package/dist/hooks/use-ai-text.js +90 -0
  95. package/dist/hooks/use-disclosure.d.ts +7 -0
  96. package/dist/hooks/use-disclosure.js +8 -0
  97. package/dist/hooks/use-form.d.ts +7 -0
  98. package/dist/hooks/use-form.js +36 -0
  99. package/dist/hooks/use-list-url-state.d.ts +43 -0
  100. package/dist/hooks/use-list-url-state.js +131 -0
  101. package/dist/hooks/use-mutation.d.ts +9 -0
  102. package/dist/hooks/use-mutation.js +37 -0
  103. package/dist/hooks/use-query.d.ts +14 -0
  104. package/dist/hooks/use-query.js +118 -0
  105. package/dist/hooks/use-reference-lookup.d.ts +14 -0
  106. package/dist/hooks/use-reference-lookup.js +63 -0
  107. package/dist/hooks/use-store.d.ts +3 -0
  108. package/dist/hooks/use-store.js +38 -0
  109. package/dist/hooks/use-stream-handler.d.ts +14 -0
  110. package/dist/hooks/use-stream-handler.js +93 -0
  111. package/dist/i18n-defaults.d.ts +2 -0
  112. package/dist/i18n-defaults.js +201 -0
  113. package/dist/i18n.d.ts +64 -0
  114. package/dist/i18n.js +166 -0
  115. package/dist/index.d.ts +77 -0
  116. package/dist/index.js +55 -0
  117. package/dist/inside-drawer.d.ts +2 -0
  118. package/{src/inside-drawer.ts → dist/inside-drawer.js} +2 -5
  119. package/dist/page-header-slot.d.ts +2 -0
  120. package/{src/page-header-slot.ts → dist/page-header-slot.js} +2 -5
  121. package/dist/primitives.d.ts +1351 -0
  122. package/dist/primitives.js +73 -0
  123. package/dist/screen-fills-height.d.ts +3 -0
  124. package/dist/screen-fills-height.js +3 -0
  125. package/dist/share-links.d.ts +4 -0
  126. package/dist/share-links.js +11 -0
  127. package/dist/sort-by-accessor.d.ts +5 -0
  128. package/dist/sort-by-accessor.js +16 -0
  129. package/dist/sse/live-events.d.ts +23 -0
  130. package/dist/sse/live-events.js +28 -0
  131. package/dist/tokens.d.ts +83 -0
  132. package/dist/tokens.js +80 -0
  133. package/package.json +16 -9
  134. package/src/__tests__/error-i18n-defaults.test.ts +0 -82
  135. package/src/__tests__/format-when.test.ts +0 -12
  136. package/src/__tests__/i18n.test.tsx +0 -283
  137. package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
  138. package/src/__tests__/qn.test.ts +0 -83
  139. package/src/__tests__/share-links.test.ts +0 -26
  140. package/src/__tests__/sort-by-accessor.test.ts +0 -48
  141. package/src/__tests__/stringify-nav-params.test.ts +0 -33
  142. package/src/__tests__/use-list-url-state.test.tsx +0 -250
  143. package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
  144. package/src/app/__tests__/action-form-shim.test.ts +0 -137
  145. package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
  146. package/src/app/__tests__/config-edit-shim.test.ts +0 -57
  147. package/src/app/__tests__/content-editors.test.tsx +0 -128
  148. package/src/app/__tests__/content-preview.test.tsx +0 -184
  149. package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
  150. package/src/app/__tests__/entity-edit-fill-height.test.tsx +0 -118
  151. package/src/app/__tests__/entity-edit-jsonb-embedded-submit.test.tsx +0 -239
  152. package/src/app/__tests__/entity-list-create-singleton.test.tsx +0 -128
  153. package/src/app/__tests__/entity-list-row-action-entity-target.test.tsx +0 -424
  154. package/src/app/__tests__/entity-list-row-action-icon.test.tsx +0 -248
  155. package/src/app/__tests__/entity-list-row-action-kumiko-actions-view.test.tsx +0 -162
  156. package/src/app/__tests__/entity-list-row-action-refetch.test.tsx +0 -401
  157. package/src/app/__tests__/entity-list-screen-padding.test.tsx +0 -128
  158. package/src/app/__tests__/feature-schema.test.ts +0 -40
  159. package/src/app/__tests__/form-schema.test.ts +0 -247
  160. package/src/app/__tests__/list-daterange-facet.test.ts +0 -165
  161. package/src/app/__tests__/list-filter-facets.test.tsx +0 -521
  162. package/src/app/__tests__/nav.test.ts +0 -198
  163. package/src/app/__tests__/projection-detail-actions.test.tsx +0 -719
  164. package/src/app/__tests__/projection-detail-reference-field.test.tsx +0 -165
  165. package/src/app/__tests__/projection-detail-shim.test.ts +0 -188
  166. package/src/app/__tests__/projection-detail-singleton.test.tsx +0 -191
  167. package/src/app/__tests__/projection-list-bad-shape.test.tsx +0 -152
  168. package/src/app/__tests__/projection-list-reference-column.test.tsx +0 -219
  169. package/src/app/__tests__/projection-list-search-sort.test.tsx +0 -212
  170. package/src/app/__tests__/qualified-names.test.ts +0 -37
  171. package/src/app/__tests__/return-to.test.ts +0 -396
  172. package/src/app/__tests__/row-actions-navigate-tab.test.ts +0 -223
  173. package/src/app/__tests__/row-default-edit-action.test.tsx +0 -324
  174. package/src/app/__tests__/screen-access-allows.test.ts +0 -34
  175. package/src/app/__tests__/secret-mint-body.test.tsx +0 -404
  176. package/src/app/__tests__/variable-chips.test.tsx +0 -70
  177. package/src/app/app-features-context.tsx +0 -25
  178. package/src/app/column-renderers.tsx +0 -68
  179. package/src/app/content-editors.tsx +0 -85
  180. package/src/app/content-preview.tsx +0 -67
  181. package/src/app/custom-screens.tsx +0 -29
  182. package/src/app/dashboard-body.tsx +0 -33
  183. package/src/app/extension-form-submit.tsx +0 -123
  184. package/src/app/extension-sections.tsx +0 -111
  185. package/src/app/form-schema.ts +0 -91
  186. package/src/app/kumiko-screen.tsx +0 -3801
  187. package/src/app/layout-fields.ts +0 -31
  188. package/src/app/list-facets.ts +0 -259
  189. package/src/app/nav.tsx +0 -258
  190. package/src/app/projection-detail-shim.ts +0 -86
  191. package/src/app/reference-facet-bridge.tsx +0 -58
  192. package/src/app/return-to.tsx +0 -234
  193. package/src/app/row-actions.ts +0 -640
  194. package/src/app/screen-access.ts +0 -63
  195. package/src/app/secret-mint-body.tsx +0 -259
  196. package/src/app/secrets-edit-body.tsx +0 -194
  197. package/src/app/use-embedded-screen.ts +0 -27
  198. package/src/app/variable-chips.tsx +0 -39
  199. package/src/app/write-failed-error.ts +0 -27
  200. package/src/components/__tests__/embedded-list-field.test.tsx +0 -531
  201. package/src/components/__tests__/multi-select-checkboxes.test.tsx +0 -162
  202. package/src/components/__tests__/related-list-section-empty-state.test.tsx +0 -143
  203. package/src/components/__tests__/related-list-section.test.tsx +0 -1733
  204. package/src/components/__tests__/render-edit-action-button.test.tsx +0 -205
  205. package/src/components/__tests__/render-edit-hidden-section.test.tsx +0 -128
  206. package/src/components/__tests__/render-edit-logic.test.ts +0 -314
  207. package/src/components/__tests__/render-edit-screen-description.test.tsx +0 -152
  208. package/src/components/__tests__/render-edit-submit-actions.test.tsx +0 -460
  209. package/src/components/__tests__/render-field-app-locale.test.tsx +0 -142
  210. package/src/components/__tests__/render-field-enum-option.test.tsx +0 -99
  211. package/src/components/__tests__/render-field-icon-derivation.test.tsx +0 -119
  212. package/src/components/__tests__/render-field-json-format.test.tsx +0 -164
  213. package/src/components/__tests__/render-field-money-embedded-list-consistency.test.tsx +0 -212
  214. package/src/components/__tests__/render-field-money-roundtrip.test.tsx +0 -217
  215. package/src/components/__tests__/render-field-number-grouping.test.tsx +0 -76
  216. package/src/components/__tests__/render-field-number-unit.test.tsx +0 -227
  217. package/src/components/__tests__/render-field-password-format.test.tsx +0 -101
  218. package/src/components/__tests__/render-field-select-display.test.tsx +0 -134
  219. package/src/components/__tests__/render-field-unit-format.test.tsx +0 -107
  220. package/src/components/__tests__/render-field-unsupported-types.test.tsx +0 -195
  221. package/src/components/__tests__/render-list-toolbar-icon-only.test.tsx +0 -146
  222. package/src/components/__tests__/write-form-section.test.tsx +0 -375
  223. package/src/components/embedded-list-field.tsx +0 -354
  224. package/src/components/grid-cell-for-field.tsx +0 -88
  225. package/src/components/multi-select-checkboxes.tsx +0 -93
  226. package/src/components/reference-create-dialog.tsx +0 -91
  227. package/src/components/related-list-section.tsx +0 -491
  228. package/src/components/render-edit-action-button.tsx +0 -109
  229. package/src/components/render-edit-logic.ts +0 -85
  230. package/src/components/render-edit-types.ts +0 -226
  231. package/src/components/render-edit.tsx +0 -1871
  232. package/src/components/render-field.tsx +0 -951
  233. package/src/components/render-list.tsx +0 -593
  234. package/src/components/write-form-section.tsx +0 -198
  235. package/src/context/draft-storage-context.tsx +0 -39
  236. package/src/hooks/__tests__/use-ai-text.test.tsx +0 -177
  237. package/src/hooks/__tests__/use-disclosure.test.tsx +0 -25
  238. package/src/hooks/__tests__/use-form.test.tsx +0 -65
  239. package/src/hooks/__tests__/use-mutation.test.tsx +0 -77
  240. package/src/hooks/__tests__/use-query.test.tsx +0 -149
  241. package/src/hooks/__tests__/use-stream-handler.test.tsx +0 -107
  242. package/src/hooks/use-ai-text.ts +0 -172
  243. package/src/hooks/use-disclosure.ts +0 -20
  244. package/src/hooks/use-form.ts +0 -78
  245. package/src/hooks/use-list-url-state.ts +0 -207
  246. package/src/hooks/use-mutation.ts +0 -61
  247. package/src/hooks/use-query.ts +0 -166
  248. package/src/hooks/use-reference-lookup.ts +0 -76
  249. package/src/hooks/use-store.ts +0 -48
  250. package/src/hooks/use-stream-handler.ts +0 -134
  251. package/src/i18n-defaults.ts +0 -240
  252. package/src/i18n.tsx +0 -286
  253. package/src/index.ts +0 -279
  254. package/src/primitives.tsx +0 -1567
  255. package/src/screen-fills-height.ts +0 -3
  256. package/src/share-links.ts +0 -12
  257. package/src/sort-by-accessor.ts +0 -20
  258. package/src/sse/live-events.tsx +0 -56
  259. package/src/tokens.tsx +0 -170
@@ -1,396 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type { ScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
3
- import type { FeatureSchema } from "../feature-schema";
4
- import type { NavApi, NavTarget } from "../nav";
5
- import {
6
- formatReturnTo,
7
- navigateWithReturnTo,
8
- resolveReturnTarget,
9
- returnToParams,
10
- splitReturnTo,
11
- } from "../return-to";
12
-
13
- // hrefFor maps entity ObjectTargets to their detail screen, mirroring resolveTarget.
14
- function fakeNav(searchParams: Readonly<Record<string, string>> = {}): {
15
- readonly nav: NavApi;
16
- readonly navigateCalls: NavTarget[];
17
- readonly setSearchParamsCalls: ReadonlyArray<Readonly<Record<string, string | null>>>;
18
- } {
19
- const navigateCalls: NavTarget[] = [];
20
- const setSearchParamsCalls: Array<Readonly<Record<string, string | null>>> = [];
21
- const hrefFor = (target: NavTarget): string =>
22
- "screenId" in target
23
- ? `/${[target.screenId, target.entityId].filter((s) => s !== undefined).join("/")}`
24
- : `/vehicle-detail/${target.id}`;
25
- const nav: NavApi = {
26
- route: undefined,
27
- navigate: (target) => navigateCalls.push(target),
28
- replace: () => {},
29
- hrefFor,
30
- searchParams,
31
- setSearchParams: (updates) => setSearchParamsCalls.push(updates),
32
- };
33
- return { nav, navigateCalls, setSearchParamsCalls };
34
- }
35
-
36
- function schemaWith(featureName: string, screens: readonly ScreenDefinition[]): FeatureSchema {
37
- return { featureName, entities: {}, screens };
38
- }
39
-
40
- const listScreen: ScreenDefinition = {
41
- id: "token-list",
42
- type: "entityList",
43
- entity: "token",
44
- columns: ["name"],
45
- };
46
-
47
- const editScreen: ScreenDefinition = {
48
- id: "token-edit",
49
- type: "entityEdit",
50
- entity: "token",
51
- layout: { sections: [{ fields: ["name"] }] },
52
- };
53
-
54
- const detailScreen: ScreenDefinition = {
55
- id: "token-detail",
56
- type: "projectionDetail",
57
- query: "tokens:query:token:detail",
58
- layout: { sections: [{ fields: ["name"] }] },
59
- };
60
-
61
- const singletonDetailScreen: ScreenDefinition = {
62
- id: "my-settings",
63
- type: "projectionDetail",
64
- query: "tokens:query:settings:mine",
65
- singleton: true,
66
- layout: { sections: [{ fields: ["name"] }] },
67
- };
68
-
69
- const adminOnlyScreen: ScreenDefinition = {
70
- id: "admin-only",
71
- type: "custom",
72
- renderer: { react: "stub" },
73
- access: { roles: ["Admin"] },
74
- };
75
-
76
- const features: readonly FeatureSchema[] = [
77
- schemaWith("tokens", [
78
- listScreen,
79
- editScreen,
80
- detailScreen,
81
- singletonDetailScreen,
82
- adminOnlyScreen,
83
- ]),
84
- ];
85
-
86
- describe("resolveReturnTarget", () => {
87
- test("valid short id resolves to a ScreenTarget", () => {
88
- expect(resolveReturnTarget("token-list", "token-create", features, undefined)).toEqual({
89
- screenId: "token-list",
90
- });
91
- });
92
-
93
- test("with entityId on an entityEdit screen", () => {
94
- expect(resolveReturnTarget("token-edit/abc-123", "token-create", features, undefined)).toEqual({
95
- screenId: "token-edit",
96
- entityId: "abc-123",
97
- });
98
- });
99
-
100
- test("with entityId on a projectionDetail screen", () => {
101
- expect(
102
- resolveReturnTarget("token-detail/abc-123", "token-create", features, undefined),
103
- ).toEqual({ screenId: "token-detail", entityId: "abc-123" });
104
- });
105
-
106
- test("accepts a '%'-containing entityId that decodes to a safe value", () => {
107
- expect(resolveReturnTarget("token-edit/abc%41", "token-create", features, undefined)).toEqual({
108
- screenId: "token-edit",
109
- entityId: "abc%41",
110
- });
111
- });
112
-
113
- test("undefined raw value", () => {
114
- expect(resolveReturnTarget(undefined, "token-create", features, undefined)).toBeUndefined();
115
- });
116
-
117
- test("empty string", () => {
118
- expect(resolveReturnTarget("", "token-create", features, undefined)).toBeUndefined();
119
- });
120
-
121
- test("rejects an unknown screen id", () => {
122
- expect(
123
- resolveReturnTarget("unknown-screen", "token-create", features, undefined),
124
- ).toBeUndefined();
125
- });
126
-
127
- test("rejects a qualified screen id ('feature:screen:x')", () => {
128
- expect(
129
- resolveReturnTarget("tokens:screen:token-list", "token-create", features, undefined),
130
- ).toBeUndefined();
131
- });
132
-
133
- test("rejects a protocol-relative value ('//evil.com')", () => {
134
- expect(resolveReturnTarget("//evil.com", "token-create", features, undefined)).toBeUndefined();
135
- });
136
-
137
- test("rejects an absolute URL ('https://evil.com')", () => {
138
- expect(
139
- resolveReturnTarget("https://evil.com", "token-create", features, undefined),
140
- ).toBeUndefined();
141
- });
142
-
143
- test("rejects more than two segments ('a/b/c')", () => {
144
- expect(resolveReturnTarget("a/b/c", "token-create", features, undefined)).toBeUndefined();
145
- });
146
-
147
- test("rejects a trailing slash ('x/')", () => {
148
- expect(resolveReturnTarget("token-list/", "token-create", features, undefined)).toBeUndefined();
149
- });
150
-
151
- test("rejects a leading slash ('/x')", () => {
152
- expect(resolveReturnTarget("/token-list", "token-create", features, undefined)).toBeUndefined();
153
- });
154
-
155
- test("rejects a role-gated screen when roles are missing", () => {
156
- expect(resolveReturnTarget("admin-only", "token-create", features, undefined)).toBeUndefined();
157
- });
158
-
159
- test("accepts a role-gated screen when the role matches", () => {
160
- expect(resolveReturnTarget("admin-only", "token-create", features, ["Admin"])).toEqual({
161
- screenId: "admin-only",
162
- });
163
- });
164
-
165
- test("rejects the own screen (self)", () => {
166
- expect(
167
- resolveReturnTarget("token-create", "token-create", features, undefined),
168
- ).toBeUndefined();
169
- });
170
-
171
- test("rejects an entityId on a list screen", () => {
172
- expect(
173
- resolveReturnTarget("token-list/abc-123", "token-create", features, undefined),
174
- ).toBeUndefined();
175
- });
176
-
177
- test("rejects entityId '..'", () => {
178
- expect(
179
- resolveReturnTarget("token-edit/..", "token-create", features, undefined),
180
- ).toBeUndefined();
181
- });
182
-
183
- test("rejects entityId '.'", () => {
184
- expect(
185
- resolveReturnTarget("token-edit/.", "token-create", features, undefined),
186
- ).toBeUndefined();
187
- });
188
-
189
- test("rejects an entityId whose decoded form carries '?'", () => {
190
- expect(
191
- resolveReturnTarget("token-edit/abc%3Fx=1", "token-create", features, undefined),
192
- ).toBeUndefined();
193
- });
194
-
195
- test("a trailing host-state query resolves to the bare target", () => {
196
- expect(
197
- resolveReturnTarget("token-edit/abc?tab=keys", "token-create", features, undefined),
198
- ).toEqual({ screenId: "token-edit", entityId: "abc" });
199
- });
200
-
201
- test("a host-state query does not loosen validation", () => {
202
- expect(
203
- resolveReturnTarget("https://evil.com?tab=keys", "token-create", features, undefined),
204
- ).toBeUndefined();
205
- expect(
206
- resolveReturnTarget("//evil.com?tab=keys", "token-create", features, undefined),
207
- ).toBeUndefined();
208
- expect(
209
- resolveReturnTarget("token-edit/..?tab=keys", "token-create", features, undefined),
210
- ).toBeUndefined();
211
- expect(
212
- resolveReturnTarget("unknown-screen?tab=keys", "token-create", features, undefined),
213
- ).toBeUndefined();
214
- expect(
215
- resolveReturnTarget("admin-only?tab=keys", "token-create", features, undefined),
216
- ).toBeUndefined();
217
- expect(resolveReturnTarget("?tab=keys", "token-create", features, undefined)).toBeUndefined();
218
- });
219
-
220
- test("rejects entityId with '#'", () => {
221
- expect(
222
- resolveReturnTarget("token-edit/abc#frag", "token-create", features, undefined),
223
- ).toBeUndefined();
224
- });
225
-
226
- test("rejects entityId with '\\\\'", () => {
227
- expect(
228
- resolveReturnTarget("token-edit/abc\\def", "token-create", features, undefined),
229
- ).toBeUndefined();
230
- });
231
-
232
- test("rejects entityId with a space", () => {
233
- expect(
234
- resolveReturnTarget("token-edit/abc def", "token-create", features, undefined),
235
- ).toBeUndefined();
236
- });
237
-
238
- test("rejects a percent-encoded entityId that decodes to '..'", () => {
239
- expect(
240
- resolveReturnTarget("token-edit/%2e%2e", "token-create", features, undefined),
241
- ).toBeUndefined();
242
- });
243
-
244
- test("rejects a percent-encoded entityId that decodes to '.'", () => {
245
- expect(
246
- resolveReturnTarget("token-edit/%2E", "token-create", features, undefined),
247
- ).toBeUndefined();
248
- });
249
-
250
- test("rejects a percent-encoded entityId that decodes to a slash", () => {
251
- expect(
252
- resolveReturnTarget("token-edit/%2Fx", "token-create", features, undefined),
253
- ).toBeUndefined();
254
- });
255
-
256
- test("rejects a malformed percent-encoded entityId", () => {
257
- expect(
258
- resolveReturnTarget("token-edit/%E0%A4%A", "token-create", features, undefined),
259
- ).toBeUndefined();
260
- });
261
-
262
- test("rejects a percent-encoded entityId that decodes to whitespace", () => {
263
- expect(
264
- resolveReturnTarget("token-edit/a%20b", "token-create", features, undefined),
265
- ).toBeUndefined();
266
- });
267
-
268
- test("rejects a non-singleton projectionDetail without an entityId", () => {
269
- expect(
270
- resolveReturnTarget("token-detail", "token-create", features, undefined),
271
- ).toBeUndefined();
272
- });
273
-
274
- test("accepts a singleton projectionDetail without an entityId", () => {
275
- expect(resolveReturnTarget("my-settings", "token-create", features, undefined)).toEqual({
276
- screenId: "my-settings",
277
- });
278
- });
279
- });
280
-
281
- describe("returnToParams", () => {
282
- test("no host -> {}", () => {
283
- expect(returnToParams(undefined, { screenId: "token-list" })).toEqual({});
284
- });
285
-
286
- test("target equals the host (self) -> {}", () => {
287
- expect(returnToParams({ screenId: "token-list" }, { screenId: "token-list" })).toEqual({});
288
- });
289
-
290
- test("target equals the host including entityId -> {}", () => {
291
- expect(
292
- returnToParams(
293
- { screenId: "token-edit", entityId: "abc" },
294
- { screenId: "token-edit", entityId: "abc" },
295
- ),
296
- ).toEqual({});
297
- });
298
-
299
- test("different target -> returnTo param", () => {
300
- expect(returnToParams({ screenId: "token-list" }, { screenId: "token-create" })).toEqual({
301
- returnTo: "token-list",
302
- });
303
- });
304
-
305
- test("host with entityId formats as 'screenId/entityId'", () => {
306
- expect(
307
- returnToParams({ screenId: "token-edit", entityId: "abc" }, { screenId: "token-create" }),
308
- ).toEqual({ returnTo: "token-edit/abc" });
309
- });
310
- });
311
-
312
- describe("formatReturnTo / splitReturnTo", () => {
313
- test("no host params formats exactly like before the snapshot existed", () => {
314
- expect(formatReturnTo({ screenId: "token-list" })).toBe("token-list");
315
- expect(formatReturnTo({ screenId: "token-list" }, {})).toBe("token-list");
316
- expect(formatReturnTo({ screenId: "token-edit", entityId: "abc" })).toBe("token-edit/abc");
317
- });
318
-
319
- test("host params round-trip through the value", () => {
320
- const raw = formatReturnTo(
321
- { screenId: "token-detail", entityId: "abc" },
322
- { tab: "keys", "orders.sort": "name", "orders.dir": "asc" },
323
- );
324
- expect(splitReturnTo(raw)).toEqual({
325
- path: "token-detail/abc",
326
- state: { tab: "keys", "orders.sort": "name", "orders.dir": "asc" },
327
- });
328
- });
329
-
330
- test("empty param values are left out of the snapshot", () => {
331
- expect(formatReturnTo({ screenId: "token-list" }, { q: "", tab: "keys" })).toBe(
332
- "token-list?tab=keys",
333
- );
334
- });
335
-
336
- test("two nesting levels round-trip one level per split", () => {
337
- const inner = formatReturnTo({ screenId: "token-list" }, { returnTo: "settings" });
338
- const outer = formatReturnTo({ screenId: "token-edit", entityId: "abc" }, { returnTo: inner });
339
-
340
- const first = splitReturnTo(outer);
341
- expect(first.path).toBe("token-edit/abc");
342
- expect(first.state["returnTo"]).toBe("token-list?returnTo=settings");
343
-
344
- const second = splitReturnTo(first.state["returnTo"] as string);
345
- expect(second.path).toBe("token-list");
346
- expect(second.state).toEqual({ returnTo: "settings" });
347
- });
348
-
349
- test("a fourth level is dropped, the rest of the snapshot survives", () => {
350
- const level3 = formatReturnTo(
351
- { screenId: "token-list" },
352
- { returnTo: formatReturnTo({ screenId: "token-detail" }, { returnTo: "settings" }) },
353
- );
354
- const level4 = formatReturnTo({ screenId: "token-edit" }, { returnTo: level3, tab: "keys" });
355
-
356
- expect(splitReturnTo(level4)).toEqual({ path: "token-edit", state: { tab: "keys" } });
357
- });
358
-
359
- test("an over-long snapshot degrades to the bare target", () => {
360
- expect(formatReturnTo({ screenId: "token-list" }, { q: "x".repeat(600) })).toBe("token-list");
361
- });
362
-
363
- test("returnToParams carries the host snapshot", () => {
364
- expect(
365
- returnToParams({ screenId: "token-list" }, { screenId: "token-create" }, { tab: "keys" }),
366
- ).toEqual({ returnTo: "token-list?tab=keys" });
367
- });
368
- });
369
-
370
- describe("navigateWithReturnTo — ObjectTarget", () => {
371
- test("target resolves away from the host: returnTo carries the host's search-params snapshot", () => {
372
- const { nav, navigateCalls, setSearchParamsCalls } = fakeNav({ tab: "active" });
373
- navigateWithReturnTo(nav, { entity: "vehicle", id: "abc" }, { screenId: "vehicle-list" });
374
-
375
- expect(navigateCalls).toEqual([{ entity: "vehicle", id: "abc" }]);
376
- expect(setSearchParamsCalls).toEqual([{ returnTo: "vehicle-list?tab=active" }]);
377
- });
378
-
379
- test("target resolves to the host itself: no returnTo", () => {
380
- const { nav, setSearchParamsCalls } = fakeNav();
381
- navigateWithReturnTo(
382
- nav,
383
- { entity: "vehicle", id: "abc" },
384
- { screenId: "vehicle-detail", entityId: "abc" },
385
- );
386
-
387
- expect(setSearchParamsCalls).toEqual([]);
388
- });
389
-
390
- test("no host: only params, no returnTo", () => {
391
- const { nav, setSearchParamsCalls } = fakeNav();
392
- navigateWithReturnTo(nav, { entity: "vehicle", id: "abc" }, undefined, { name: "Vehicle A" });
393
-
394
- expect(setSearchParamsCalls).toEqual([{ name: "Vehicle A" }]);
395
- });
396
- });
@@ -1,223 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type {
3
- RelatedListToolbarAction,
4
- RowActionNavigate,
5
- } from "@cosmicdrift/kumiko-framework/ui-types";
6
- import type { NavApi, NavTarget } from "../nav";
7
- import { RETURN_TO_PARAM, type ReturnHost } from "../return-to";
8
- import {
9
- buildProjectionToolbarActions,
10
- buildRecordActions,
11
- runProjectionRowNavigate,
12
- } from "../row-actions";
13
-
14
- function recordingNav(hrefFor: (target: NavTarget) => string = () => ""): {
15
- readonly nav: NavApi;
16
- readonly navigateCalls: NavTarget[];
17
- readonly setSearchParamsCalls: ReadonlyArray<Readonly<Record<string, string | null>>>;
18
- } {
19
- const navigateCalls: NavTarget[] = [];
20
- const setSearchParamsCalls: Array<Readonly<Record<string, string | null>>> = [];
21
- const nav: NavApi = {
22
- route: undefined,
23
- navigate: (target) => navigateCalls.push(target),
24
- replace: () => {},
25
- hrefFor,
26
- searchParams: {},
27
- setSearchParams: (updates) => setSearchParamsCalls.push(updates),
28
- };
29
- return { nav, navigateCalls, setSearchParamsCalls };
30
- }
31
-
32
- // hrefFor maps entity ObjectTargets to their detail screen, mirroring resolveTarget.
33
- const hrefForVehicleDetail = (target: NavTarget): string =>
34
- "screenId" in target
35
- ? `/${[target.screenId, target.entityId].filter((s) => s !== undefined).join("/")}`
36
- : `/vehicle-detail/${target.id}`;
37
-
38
- const host: ReturnHost = { screenId: "vehicle-list" };
39
- const noOpenDrawer = (): void => {};
40
- const noOnWriteSuccess = async (): Promise<void> => {};
41
-
42
- describe("buildRecordActions — RowActionNavigate.tab", () => {
43
- test("screen-target: tab lands in the same setSearchParams call as returnTo", () => {
44
- const { nav, navigateCalls, setSearchParamsCalls } = recordingNav();
45
- const action: RowActionNavigate = {
46
- kind: "navigate",
47
- id: "view-channels",
48
- label: "app.actions.viewChannels",
49
- screen: "vehicle-detail",
50
- entityId: "id",
51
- tab: "channels",
52
- params: { pick: ["name"] },
53
- };
54
- const actions = buildRecordActions({
55
- actions: [action],
56
- record: { id: "abc", name: "Vehicle A" },
57
- translate: (key) => key,
58
- nav,
59
- host,
60
- dispatcher: undefined,
61
- openDrawer: noOpenDrawer,
62
- onWriteSuccess: noOnWriteSuccess,
63
- });
64
- actions?.[0]?.onPress();
65
-
66
- expect(navigateCalls).toEqual([{ screenId: "vehicle-detail", entityId: "abc" }]);
67
- expect(setSearchParamsCalls).toHaveLength(1);
68
- expect(setSearchParamsCalls[0]).toMatchObject({
69
- tab: "channels",
70
- name: "Vehicle A",
71
- [RETURN_TO_PARAM]: "vehicle-list",
72
- });
73
- });
74
-
75
- test("entity-target: tab is included in the navigate search params", () => {
76
- const { nav, setSearchParamsCalls } = recordingNav();
77
- const action: RowActionNavigate = {
78
- kind: "navigate",
79
- id: "view-channels",
80
- label: "app.actions.viewChannels",
81
- entity: "vehicle",
82
- entityId: "id",
83
- tab: "channels",
84
- };
85
- const actions = buildRecordActions({
86
- actions: [action],
87
- record: { id: "abc" },
88
- translate: (key) => key,
89
- nav,
90
- host: undefined,
91
- dispatcher: undefined,
92
- openDrawer: noOpenDrawer,
93
- onWriteSuccess: noOnWriteSuccess,
94
- });
95
- actions?.[0]?.onPress();
96
-
97
- expect(setSearchParamsCalls).toHaveLength(1);
98
- expect(setSearchParamsCalls[0]).toEqual({ tab: "channels" });
99
- });
100
-
101
- test("entity-target: carries returnTo when the target resolves away from the host", () => {
102
- const { nav, navigateCalls, setSearchParamsCalls } = recordingNav(hrefForVehicleDetail);
103
- const action: RowActionNavigate = {
104
- kind: "navigate",
105
- id: "view-channels",
106
- label: "app.actions.viewChannels",
107
- entity: "vehicle",
108
- entityId: "id",
109
- tab: "channels",
110
- };
111
- const actions = buildRecordActions({
112
- actions: [action],
113
- record: { id: "abc" },
114
- translate: (key) => key,
115
- nav,
116
- host,
117
- dispatcher: undefined,
118
- openDrawer: noOpenDrawer,
119
- onWriteSuccess: noOnWriteSuccess,
120
- });
121
- actions?.[0]?.onPress();
122
-
123
- expect(navigateCalls).toEqual([{ entity: "vehicle", id: "abc" }]);
124
- expect(setSearchParamsCalls).toEqual([{ tab: "channels", [RETURN_TO_PARAM]: "vehicle-list" }]);
125
- });
126
-
127
- test("no tab set: search params carry no tab key", () => {
128
- const { nav, setSearchParamsCalls } = recordingNav();
129
- const action: RowActionNavigate = {
130
- kind: "navigate",
131
- id: "view-channels",
132
- label: "app.actions.viewChannels",
133
- screen: "vehicle-detail",
134
- entityId: "id",
135
- params: { pick: ["name"] },
136
- };
137
- const actions = buildRecordActions({
138
- actions: [action],
139
- record: { id: "abc", name: "Vehicle A" },
140
- translate: (key) => key,
141
- nav,
142
- host,
143
- dispatcher: undefined,
144
- openDrawer: noOpenDrawer,
145
- onWriteSuccess: noOnWriteSuccess,
146
- });
147
- actions?.[0]?.onPress();
148
-
149
- expect(setSearchParamsCalls).toHaveLength(1);
150
- expect(setSearchParamsCalls[0]).not.toHaveProperty("tab");
151
- });
152
- });
153
-
154
- describe("buildProjectionToolbarActions — navigate.tab", () => {
155
- test("tab lands in the setSearchParams call", () => {
156
- const { nav, navigateCalls, setSearchParamsCalls } = recordingNav();
157
- const action: RelatedListToolbarAction = {
158
- kind: "navigate",
159
- id: "view-channels",
160
- label: "app.actions.viewChannels",
161
- screen: "vehicle-detail",
162
- tab: "channels",
163
- };
164
- const actions = buildProjectionToolbarActions({
165
- toolbarActions: [action],
166
- translate: (key) => key,
167
- dispatcher: undefined,
168
- nav,
169
- refetch: async () => {},
170
- host: undefined,
171
- });
172
- actions?.[0]?.onTrigger();
173
-
174
- expect(navigateCalls).toEqual([{ screenId: "vehicle-detail" }]);
175
- expect(setSearchParamsCalls).toEqual([{ tab: "channels" }]);
176
- });
177
-
178
- test("navigatePrefill and tab both survive into the same setSearchParams call, plus returnTo with a host", () => {
179
- const { nav, setSearchParamsCalls } = recordingNav();
180
- const action: RelatedListToolbarAction = {
181
- kind: "navigate",
182
- id: "create-channel",
183
- label: "app.actions.createChannel",
184
- screen: "channel-create",
185
- tab: "channels",
186
- };
187
- const actions = buildProjectionToolbarActions({
188
- toolbarActions: [action],
189
- translate: (key) => key,
190
- dispatcher: undefined,
191
- nav,
192
- refetch: async () => {},
193
- navigatePrefill: { vehicleId: "abc" },
194
- host,
195
- });
196
- actions?.[0]?.onTrigger();
197
-
198
- expect(setSearchParamsCalls).toHaveLength(1);
199
- expect(setSearchParamsCalls[0]).toMatchObject({
200
- tab: "channels",
201
- vehicleId: "abc",
202
- [RETURN_TO_PARAM]: "vehicle-list",
203
- });
204
- });
205
- });
206
-
207
- describe("runProjectionRowNavigate — entity target carries returnTo", () => {
208
- test("entity action with a host and tab: navigate gets the ObjectTarget, returnTo and tab land together", () => {
209
- const { nav, navigateCalls, setSearchParamsCalls } = recordingNav(hrefForVehicleDetail);
210
- const action: RowActionNavigate = {
211
- kind: "navigate",
212
- id: "view-channels",
213
- label: "app.actions.viewChannels",
214
- entity: "vehicle",
215
- entityId: "id",
216
- tab: "channels",
217
- };
218
- runProjectionRowNavigate(nav, action, { id: "abc", values: { id: "abc" } }, host);
219
-
220
- expect(navigateCalls).toEqual([{ entity: "vehicle", id: "abc" }]);
221
- expect(setSearchParamsCalls).toEqual([{ tab: "channels", [RETURN_TO_PARAM]: "vehicle-list" }]);
222
- });
223
- });