@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,424 +0,0 @@
1
- // fw#2228: RowActionNavigate can now name an ObjectTarget (`entity`) instead
2
- // of a ScreenTarget (`screen`). This renders the real entityList pipeline
3
- // (KumikoScreen → EntityListBody) and asserts a click on the entity-target
4
- // row action calls nav.navigate with `{ entity, id }` — resolution to an
5
- // actual screen is the NavApi impl's job (renderer-web's resolveTarget), not
6
- // this platform-neutral package's. Mirrors
7
- // entity-list-row-action-kumiko-actions-view.test.tsx's harness.
8
-
9
- import { describe, expect, test } from "bun:test";
10
- import type {
11
- EntityDefinition,
12
- EntityListScreenDefinition,
13
- ProjectionDetailScreenDefinition,
14
- ProjectionListScreenDefinition,
15
- RowActionNavigate,
16
- } from "@cosmicdrift/kumiko-framework/ui-types";
17
- import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
18
- import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
19
- import type { ComponentType, ReactNode } from "react";
20
- import { DispatcherProvider } from "../../context/dispatcher-context";
21
- import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
22
- import { kumikoDefaultTranslations } from "../../i18n-defaults";
23
- import {
24
- type ButtonProps,
25
- type CardProps,
26
- type CorePrimitives,
27
- type DataTableProps,
28
- type DataTableRowAction,
29
- type FormProps,
30
- PrimitivesProvider,
31
- } from "../../primitives";
32
- import type { FeatureSchema } from "../feature-schema";
33
- import { KumikoScreen } from "../kumiko-screen";
34
- import type { NavTarget } from "../nav";
35
- import { NavProvider } from "../nav";
36
-
37
- function stubDispatcher(): Dispatcher {
38
- return {
39
- write: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["write"],
40
- query: (async () => ({
41
- isSuccess: true,
42
- data: {
43
- rows: [{ id: "invoice-42", customerId: "cust-7", status: "open" }],
44
- nextCursor: null,
45
- total: 1,
46
- },
47
- })) as unknown as Dispatcher["query"],
48
- batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
49
- statusStore: {
50
- getState: () => "online",
51
- subscribe: () => () => {},
52
- } as unknown as Dispatcher["statusStore"],
53
- async *stream() {},
54
- pendingWrites: () => [],
55
- pendingFiles: () => [],
56
- };
57
- }
58
-
59
- let capturedRowActions: readonly DataTableRowAction[] | undefined;
60
- const captureDataTable: ComponentType<DataTableProps> = (props) => {
61
- capturedRowActions = props.rowActions;
62
- return null;
63
- };
64
- const noop = (): ReactNode => null;
65
- const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
66
- // Card's title/subtitle/headerActions arrive as `slots` instead of raw
67
- // JSX children.
68
- const cardWithSlots: ComponentType<CardProps> = ({ slots, children }) => (
69
- <>
70
- {slots?.title}
71
- {slots?.subtitle}
72
- {slots?.headerActions}
73
- {children}
74
- </>
75
- );
76
-
77
- const testPrimitives: CorePrimitives = {
78
- Button: noop,
79
- Banner: passChildren,
80
- Field: passChildren,
81
- Input: noop,
82
- DataTable: captureDataTable,
83
- Form: passChildren,
84
- Section: passChildren,
85
- Card: cardWithSlots,
86
- Grid: passChildren,
87
- GridCell: passChildren,
88
- Text: passChildren,
89
- Heading: noop,
90
- Dialog: noop,
91
- Modal: noop,
92
- Lightbox: noop,
93
- ConfigSourceBadge: noop,
94
- ConfigCascadeView: noop,
95
- Link: noop,
96
- };
97
-
98
- function buildSchema(rowAction: EntityListScreenDefinition["rowActions"]): FeatureSchema {
99
- const entity: EntityDefinition = {
100
- fields: {
101
- customerId: {
102
- type: "text",
103
- maxLength: 50,
104
- required: false,
105
- searchable: false,
106
- sortable: false,
107
- },
108
- status: { type: "text", maxLength: 50, required: false, searchable: false, sortable: false },
109
- },
110
- };
111
- const listScreen: EntityListScreenDefinition = {
112
- id: "invoice-list",
113
- type: "entityList",
114
- entity: "invoice",
115
- columns: ["status"],
116
- rowActions: rowAction,
117
- };
118
- return {
119
- featureName: "billing",
120
- entities: { invoice: entity },
121
- screens: [listScreen],
122
- } as FeatureSchema;
123
- }
124
-
125
- function renderListScreen(schema: FeatureSchema, navigateSpy: (target: NavTarget) => void): void {
126
- render(
127
- <LocaleProvider
128
- resolver={createStaticLocaleResolver({ locale: "en" })}
129
- fallbackBundles={[kumikoDefaultTranslations]}
130
- >
131
- <DispatcherProvider dispatcher={stubDispatcher()}>
132
- <NavProvider
133
- value={{
134
- route: { screenId: "billing:invoice-list" },
135
- navigate: navigateSpy,
136
- replace: () => {},
137
- hrefFor: () => "",
138
- searchParams: {},
139
- setSearchParams: () => {},
140
- }}
141
- >
142
- <PrimitivesProvider value={testPrimitives}>
143
- <KumikoScreen schema={schema} qn="billing:screen:invoice-list" />
144
- </PrimitivesProvider>
145
- </NavProvider>
146
- </DispatcherProvider>
147
- </LocaleProvider>,
148
- );
149
- }
150
-
151
- function requireAction(id: string): DataTableRowAction {
152
- const action = capturedRowActions?.find((a) => a.id === id);
153
- if (!action) throw new Error(`expected the '${id}' row action to be captured`);
154
- return action;
155
- }
156
-
157
- describe("entityList navigate row-action with an entity-target (fw#2228)", () => {
158
- test("clicking the row action calls nav.navigate with { entity, id }, not a screenId", async () => {
159
- capturedRowActions = undefined;
160
- const navigateCalls: NavTarget[] = [];
161
-
162
- renderListScreen(
163
- buildSchema([
164
- { kind: "navigate", id: "view", label: "kumiko.actions.view", entity: "invoice" },
165
- ]),
166
- (target) => navigateCalls.push(target),
167
- );
168
-
169
- await waitFor(() => {
170
- expect(capturedRowActions).toBeDefined();
171
- });
172
-
173
- await requireAction("view").onTrigger({
174
- id: "invoice-42",
175
- values: { id: "invoice-42", customerId: "cust-7", status: "open" },
176
- });
177
-
178
- expect(navigateCalls).toHaveLength(1);
179
- expect(navigateCalls[0]).toEqual({ entity: "invoice", id: "invoice-42" });
180
- });
181
-
182
- test("an explicit entityId field-name still overrides the row['id'] default", async () => {
183
- capturedRowActions = undefined;
184
- const navigateCalls: NavTarget[] = [];
185
-
186
- renderListScreen(
187
- buildSchema([
188
- {
189
- kind: "navigate",
190
- id: "view-customer",
191
- label: "kumiko.actions.view",
192
- entity: "customer",
193
- entityId: "customerId",
194
- },
195
- ]),
196
- (target) => navigateCalls.push(target),
197
- );
198
-
199
- await waitFor(() => {
200
- expect(capturedRowActions).toBeDefined();
201
- });
202
-
203
- await requireAction("view-customer").onTrigger({
204
- id: "invoice-42",
205
- values: { id: "invoice-42", customerId: "cust-7" },
206
- });
207
-
208
- expect(navigateCalls).toEqual([{ entity: "customer", id: "cust-7" }]);
209
- });
210
- });
211
-
212
- // projectionList.runNavigate has NO row["id"] fallback (unlike EntityListBody
213
- // above) — a projectionList row comes from an arbitrary query without a
214
- // guaranteed "id" field, so an entity-target there requires an explicit
215
- // entityId. This proves that path resolves { entity, id } correctly and
216
- // still forwards `params` via setSearchParams.
217
- function buildProjectionListSchema(rowAction: RowActionNavigate): FeatureSchema {
218
- const listScreen: ProjectionListScreenDefinition = {
219
- id: "invoice-projection-list",
220
- type: "projectionList",
221
- query: "billing:query:invoice:list",
222
- columns: [{ field: "status", label: "Status" }],
223
- rowActions: [rowAction],
224
- };
225
- return {
226
- featureName: "billing",
227
- entities: {},
228
- screens: [listScreen],
229
- } as FeatureSchema;
230
- }
231
-
232
- function renderProjectionListScreen(
233
- schema: FeatureSchema,
234
- navigateSpy: (target: NavTarget) => void,
235
- setSearchParamsSpy: (params: Record<string, string | null>) => void,
236
- ): void {
237
- render(
238
- <LocaleProvider
239
- resolver={createStaticLocaleResolver({ locale: "en" })}
240
- fallbackBundles={[kumikoDefaultTranslations]}
241
- >
242
- <DispatcherProvider dispatcher={stubDispatcher()}>
243
- <NavProvider
244
- value={{
245
- route: { screenId: "billing:invoice-projection-list" },
246
- navigate: navigateSpy,
247
- replace: () => {},
248
- hrefFor: () => "",
249
- searchParams: {},
250
- setSearchParams: setSearchParamsSpy,
251
- }}
252
- >
253
- <PrimitivesProvider value={testPrimitives}>
254
- <KumikoScreen schema={schema} qn="billing:screen:invoice-projection-list" />
255
- </PrimitivesProvider>
256
- </NavProvider>
257
- </DispatcherProvider>
258
- </LocaleProvider>,
259
- );
260
- }
261
-
262
- describe("projectionList navigate row-action with an entity-target (fw#2228)", () => {
263
- test("entity-target with entityId calls nav.navigate with { entity, id } and forwards params via setSearchParams", async () => {
264
- capturedRowActions = undefined;
265
- const navigateCalls: NavTarget[] = [];
266
- const searchParamsCalls: Record<string, string | null>[] = [];
267
-
268
- renderProjectionListScreen(
269
- buildProjectionListSchema({
270
- kind: "navigate",
271
- id: "view",
272
- label: "kumiko.actions.view",
273
- entity: "invoice",
274
- entityId: "invoiceId",
275
- params: { pick: ["status"] },
276
- }),
277
- (target) => navigateCalls.push(target),
278
- (params) => searchParamsCalls.push(params),
279
- );
280
-
281
- await waitFor(() => {
282
- expect(capturedRowActions).toBeDefined();
283
- });
284
-
285
- await requireAction("view").onTrigger({
286
- id: "proj-1",
287
- values: { id: "proj-1", invoiceId: "invoice-42", status: "open" },
288
- });
289
-
290
- expect(navigateCalls).toEqual([{ entity: "invoice", id: "invoice-42" }]);
291
- expect(searchParamsCalls).toEqual([{ status: "open" }]);
292
- });
293
- });
294
-
295
- // projectionDetail's header actions run through the same entity-target
296
- // resolution (ProjectionDetailBody, no record["id"] fallback either) as a
297
- // distinct code path from both entityList and projectionList above — this
298
- // renders the real KumikoScreen → ProjectionDetailBody → RenderEdit pipeline
299
- // and clicks the real action button.
300
- const TestButton: ComponentType<ButtonProps> = ({ children, onClick, testId }) => (
301
- <button type="button" data-testid={testId} onClick={() => void onClick?.()}>
302
- {children}
303
- </button>
304
- );
305
-
306
- // headerRegion carries projectionDetail's header actions since fw#2713 — a
307
- // stub that only forwards children/actions/secondaryActions drops them
308
- // silently, as this test's own action button did until it grew this slot.
309
- const FormWithActions: ComponentType<FormProps> = ({
310
- children,
311
- actions,
312
- secondaryActions,
313
- headerRegion,
314
- }) => (
315
- <>
316
- {headerRegion !== undefined && <div data-testid="form-header">{headerRegion}</div>}
317
- <div data-testid="form-body">{children}</div>
318
- <div data-testid="form-actions">
319
- {secondaryActions}
320
- {actions}
321
- </div>
322
- </>
323
- );
324
-
325
- const detailTestPrimitives: CorePrimitives = {
326
- ...testPrimitives,
327
- Button: TestButton,
328
- Form: FormWithActions,
329
- };
330
-
331
- function detailStubDispatcher(record: Readonly<Record<string, unknown>>): Dispatcher {
332
- return {
333
- write: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["write"],
334
- query: (async () => ({ isSuccess: true, data: record })) as unknown as Dispatcher["query"],
335
- batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
336
- statusStore: {
337
- getState: () => "online",
338
- subscribe: () => () => {},
339
- } as unknown as Dispatcher["statusStore"],
340
- async *stream() {},
341
- pendingWrites: () => [],
342
- pendingFiles: () => [],
343
- };
344
- }
345
-
346
- function buildProjectionDetailSchema(action: RowActionNavigate): FeatureSchema {
347
- const detailScreen: ProjectionDetailScreenDefinition = {
348
- id: "invoice-detail",
349
- type: "projectionDetail",
350
- query: "billing:query:invoice:detail",
351
- layout: { sections: [{ title: "s", fields: ["status"] }] },
352
- actions: [action],
353
- };
354
- return {
355
- featureName: "billing",
356
- entities: {},
357
- screens: [detailScreen],
358
- } as FeatureSchema;
359
- }
360
-
361
- function renderProjectionDetailScreen(
362
- schema: FeatureSchema,
363
- record: Readonly<Record<string, unknown>>,
364
- navigateSpy: (target: NavTarget) => void,
365
- setSearchParamsSpy: (params: Record<string, string | null>) => void,
366
- ): void {
367
- render(
368
- <LocaleProvider
369
- resolver={createStaticLocaleResolver({ locale: "en" })}
370
- fallbackBundles={[kumikoDefaultTranslations]}
371
- >
372
- <DispatcherProvider dispatcher={detailStubDispatcher(record)}>
373
- <NavProvider
374
- value={{
375
- route: { screenId: "billing:invoice-detail" },
376
- navigate: navigateSpy,
377
- replace: () => {},
378
- hrefFor: () => "",
379
- searchParams: {},
380
- setSearchParams: setSearchParamsSpy,
381
- }}
382
- >
383
- <PrimitivesProvider value={detailTestPrimitives}>
384
- <KumikoScreen
385
- schema={schema}
386
- qn="billing:screen:invoice-detail"
387
- entityId={String(record["id"])}
388
- />
389
- </PrimitivesProvider>
390
- </NavProvider>
391
- </DispatcherProvider>
392
- </LocaleProvider>,
393
- );
394
- }
395
-
396
- describe("projectionDetail navigate action with an entity-target (fw#2228)", () => {
397
- test("entity-target with entityId calls nav.navigate with { entity, id } and forwards params via setSearchParams", async () => {
398
- const navigateCalls: NavTarget[] = [];
399
- const searchParamsCalls: Record<string, string | null>[] = [];
400
-
401
- renderProjectionDetailScreen(
402
- buildProjectionDetailSchema({
403
- kind: "navigate",
404
- id: "view-customer",
405
- label: "kumiko.actions.view",
406
- entity: "customer",
407
- entityId: "customerId",
408
- params: { pick: ["status"] },
409
- }),
410
- { id: "invoice-42", customerId: "cust-7", status: "open" },
411
- (target) => navigateCalls.push(target),
412
- (params) => searchParamsCalls.push(params),
413
- );
414
-
415
- await waitFor(() => expect(screen.queryByText("Loading…")).toBeNull());
416
-
417
- await act(async () => {
418
- fireEvent.click(screen.getByTestId("render-edit-action-view-customer"));
419
- });
420
-
421
- expect(navigateCalls).toEqual([{ entity: "customer", id: "cust-7" }]);
422
- expect(searchParamsCalls).toEqual([{ status: "open" }]);
423
- });
424
- });
@@ -1,248 +0,0 @@
1
- // fw-ui-defaults: RowAction had no icon field at all, so every framework-
2
- // generated action button rendered as a same-looking text button. This
3
- // renders the real entityList pipeline (KumikoScreen → EntityListBody) and
4
- // asserts on the resolved `icon` KumikoScreen attaches to each
5
- // DataTableRowAction — the id-derived default (ACTION_ICON_BY_ID in
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).
9
-
10
- import { describe, expect, test } from "bun:test";
11
- import type {
12
- EntityDefinition,
13
- EntityListScreenDefinition,
14
- } from "@cosmicdrift/kumiko-framework/ui-types";
15
- import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
16
- import { render, waitFor } from "@testing-library/react";
17
- import type { ComponentType, ReactNode } from "react";
18
- import { DispatcherProvider } from "../../context/dispatcher-context";
19
- import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
20
- import { kumikoDefaultTranslations } from "../../i18n-defaults";
21
- import {
22
- type CorePrimitives,
23
- type DataTableProps,
24
- type DataTableRowAction,
25
- PrimitivesProvider,
26
- } from "../../primitives";
27
- import type { FeatureSchema } from "../feature-schema";
28
- import { KumikoScreen } from "../kumiko-screen";
29
- import { NavProvider } from "../nav";
30
-
31
- function stubDispatcher(): Dispatcher {
32
- return {
33
- write: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["write"],
34
- query: (async () => ({
35
- isSuccess: true,
36
- data: { rows: [{ id: "row-1" }], nextCursor: null, total: 1 },
37
- })) as unknown as Dispatcher["query"],
38
- batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
39
- statusStore: {
40
- getState: () => "online",
41
- subscribe: () => () => {},
42
- } as unknown as Dispatcher["statusStore"],
43
- async *stream() {},
44
- pendingWrites: () => [],
45
- pendingFiles: () => [],
46
- };
47
- }
48
-
49
- let capturedRowActions: readonly DataTableRowAction[] | undefined;
50
- const captureDataTable: ComponentType<DataTableProps> = (props) => {
51
- capturedRowActions = props.rowActions;
52
- return null;
53
- };
54
- const noop = (): ReactNode => null;
55
- const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
56
-
57
- const testPrimitives: CorePrimitives = {
58
- Button: noop,
59
- Banner: passChildren,
60
- Field: passChildren,
61
- Input: noop,
62
- DataTable: captureDataTable,
63
- Form: passChildren,
64
- Section: passChildren,
65
- Card: passChildren,
66
- Grid: passChildren,
67
- GridCell: passChildren,
68
- Text: passChildren,
69
- Heading: noop,
70
- Dialog: noop,
71
- Modal: noop,
72
- Lightbox: noop,
73
- ConfigSourceBadge: noop,
74
- ConfigCascadeView: noop,
75
- Link: noop,
76
- };
77
-
78
- function buildSchema(): FeatureSchema {
79
- const entity: EntityDefinition = {
80
- fields: {
81
- name: { type: "text", maxLength: 50, required: false, searchable: false, sortable: false },
82
- },
83
- };
84
- const listScreen: EntityListScreenDefinition = {
85
- id: "widget-list",
86
- type: "entityList",
87
- entity: "widget",
88
- columns: ["name"],
89
- rowActions: [
90
- // No declared icon — must fall back to ACTION_ICON_BY_ID["delete"].
91
- {
92
- id: "delete",
93
- label: "Delete",
94
- handler: "icon-fixture:write:widget:delete",
95
- },
96
- // Declared icon on an id that DOES have an id-derived default
97
- // ("edit" -> pencil) — the declared icon must win.
98
- {
99
- kind: "navigate",
100
- id: "edit",
101
- label: "Edit",
102
- screen: "widget-edit",
103
- icon: "archive",
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
- },
135
- ],
136
- };
137
- return {
138
- featureName: "icon-fixture",
139
- entities: { widget: entity },
140
- screens: [listScreen],
141
- } as FeatureSchema;
142
- }
143
-
144
- function renderListScreen(): void {
145
- render(
146
- <LocaleProvider
147
- resolver={createStaticLocaleResolver({ locale: "en" })}
148
- fallbackBundles={[kumikoDefaultTranslations]}
149
- >
150
- <DispatcherProvider dispatcher={stubDispatcher()}>
151
- <NavProvider
152
- value={{
153
- route: { screenId: "icon-fixture:screen:widget-list" },
154
- navigate: () => {},
155
- replace: () => {},
156
- hrefFor: () => "",
157
- searchParams: {},
158
- setSearchParams: () => {},
159
- }}
160
- >
161
- <PrimitivesProvider value={testPrimitives}>
162
- <KumikoScreen schema={buildSchema()} qn="icon-fixture:screen:widget-list" />
163
- </PrimitivesProvider>
164
- </NavProvider>
165
- </DispatcherProvider>
166
- </LocaleProvider>,
167
- );
168
- }
169
-
170
- function requireAction(id: string): DataTableRowAction {
171
- const action = capturedRowActions?.find((a) => a.id === id);
172
- if (!action) throw new Error(`expected the '${id}' row action to be captured`);
173
- return action;
174
- }
175
-
176
- describe("entityList rowActions resolve an icon (fw-ui-defaults)", () => {
177
- test("an action with no declared icon derives it from its id ('delete' -> trash)", async () => {
178
- capturedRowActions = undefined;
179
-
180
- renderListScreen();
181
-
182
- await waitFor(() => {
183
- expect(capturedRowActions).toBeDefined();
184
- });
185
-
186
- expect(requireAction("delete").icon).toBe("trash");
187
- });
188
-
189
- test("a declared icon overrides the id-derived default ('edit' would derive pencil)", async () => {
190
- capturedRowActions = undefined;
191
-
192
- renderListScreen();
193
-
194
- await waitFor(() => {
195
- expect(capturedRowActions).toBeDefined();
196
- });
197
-
198
- expect(requireAction("edit").icon).toBe("archive");
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
- });
248
- });