@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,162 +0,0 @@
1
- // fw-i18n-funde: user-data-rights's export-job-list screen (bundled-features/
2
- // src/user-data-rights/screens.ts) ships a `navigate` row action labelled
3
- // "kumiko.actions.view", following the same `kumiko.actions.*` convention as
4
- // tenant/screens.ts and user/screens.ts (e.g. "kumiko.actions.edit"). Unlike
5
- // those, "kumiko.actions.view" was never declared in the framework's default
6
- // bundle (renderer/src/i18n-defaults.ts) — every mounting app rendered the
7
- // raw key instead of a translated label. This renders the real entityList
8
- // pipeline (KumikoScreen → EntityListBody) against the framework's own
9
- // kumikoDefaultTranslations, mirroring entity-list-row-action-refetch.test.tsx's
10
- // harness, and asserts the row action resolves to actual text.
11
-
12
- import { describe, expect, test } from "bun:test";
13
- import type {
14
- EntityDefinition,
15
- EntityListScreenDefinition,
16
- } from "@cosmicdrift/kumiko-framework/ui-types";
17
- import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
18
- import { render, 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 CorePrimitives,
25
- type DataTableProps,
26
- type DataTableRowAction,
27
- PrimitivesProvider,
28
- } from "../../primitives";
29
- import type { FeatureSchema } from "../feature-schema";
30
- import { KumikoScreen } from "../kumiko-screen";
31
- import { NavProvider } from "../nav";
32
-
33
- function stubDispatcher(): Dispatcher {
34
- return {
35
- write: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["write"],
36
- query: (async () => ({
37
- isSuccess: true,
38
- data: { rows: [{ id: "job-1", status: "completed" }], nextCursor: null, total: 1 },
39
- })) as unknown as Dispatcher["query"],
40
- batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
41
- statusStore: {
42
- getState: () => "online",
43
- subscribe: () => () => {},
44
- } as unknown as Dispatcher["statusStore"],
45
- async *stream() {},
46
- pendingWrites: () => [],
47
- pendingFiles: () => [],
48
- };
49
- }
50
-
51
- let capturedRowActions: readonly DataTableRowAction[] | undefined;
52
- const captureDataTable: ComponentType<DataTableProps> = (props) => {
53
- capturedRowActions = props.rowActions;
54
- return null;
55
- };
56
- const noop = (): ReactNode => null;
57
- const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
58
-
59
- const testPrimitives: CorePrimitives = {
60
- Button: noop,
61
- Banner: passChildren,
62
- Field: passChildren,
63
- Input: noop,
64
- DataTable: captureDataTable,
65
- Form: passChildren,
66
- Section: passChildren,
67
- Card: passChildren,
68
- Grid: passChildren,
69
- GridCell: passChildren,
70
- Text: passChildren,
71
- Heading: noop,
72
- Dialog: noop,
73
- Modal: noop,
74
- Lightbox: noop,
75
- ConfigSourceBadge: noop,
76
- ConfigCascadeView: noop,
77
- Link: noop,
78
- };
79
-
80
- function buildSchema(): FeatureSchema {
81
- const entity: EntityDefinition = {
82
- fields: {
83
- status: {
84
- type: "text",
85
- maxLength: 50,
86
- required: false,
87
- searchable: false,
88
- sortable: false,
89
- },
90
- },
91
- };
92
- // Mirrors user-data-rights's export-job-list rowAction verbatim (kind,
93
- // label key, entityId) — only ids/entity/screen are renamed for the fixture.
94
- const listScreen: EntityListScreenDefinition = {
95
- id: "export-job-list",
96
- type: "entityList",
97
- entity: "export-job",
98
- columns: ["status"],
99
- rowActions: [
100
- {
101
- kind: "navigate",
102
- id: "view",
103
- label: "kumiko.actions.view",
104
- screen: "export-job-detail",
105
- entityId: "id",
106
- },
107
- ],
108
- };
109
- return {
110
- featureName: "user-data-rights",
111
- entities: { "export-job": entity },
112
- screens: [listScreen],
113
- } as FeatureSchema;
114
- }
115
-
116
- function renderListScreen(): void {
117
- render(
118
- <LocaleProvider
119
- resolver={createStaticLocaleResolver({ locale: "en" })}
120
- fallbackBundles={[kumikoDefaultTranslations]}
121
- >
122
- <DispatcherProvider dispatcher={stubDispatcher()}>
123
- <NavProvider
124
- value={{
125
- route: { screenId: "user-data-rights:export-job-list" },
126
- navigate: () => {},
127
- replace: () => {},
128
- hrefFor: () => "",
129
- searchParams: {},
130
- setSearchParams: () => {},
131
- }}
132
- >
133
- <PrimitivesProvider value={testPrimitives}>
134
- <KumikoScreen schema={buildSchema()} qn="user-data-rights:screen:export-job-list" />
135
- </PrimitivesProvider>
136
- </NavProvider>
137
- </DispatcherProvider>
138
- </LocaleProvider>,
139
- );
140
- }
141
-
142
- function requireViewAction(): DataTableRowAction {
143
- const action = capturedRowActions?.find((a) => a.id === "view");
144
- if (!action) throw new Error("expected the 'view' row action to be captured");
145
- return action;
146
- }
147
-
148
- describe("entityList navigate row-action renders the framework's kumiko.actions.* labels", () => {
149
- test("kumiko.actions.view resolves to real text, not the raw key", async () => {
150
- capturedRowActions = undefined;
151
-
152
- renderListScreen();
153
-
154
- await waitFor(() => {
155
- expect(capturedRowActions).toBeDefined();
156
- });
157
-
158
- const viewAction = requireViewAction();
159
- expect(viewAction.label).not.toBe("kumiko.actions.view");
160
- expect(viewAction.label).toBe("View");
161
- });
162
- });
@@ -1,401 +0,0 @@
1
- // Bug found while archiving a cost-type in solon (kumiko-framework#113-adjacent):
2
- // a `writeHandler` row-action on an entityList screen writes successfully
3
- // (HTTP 200, projection updated) but the list keeps showing the old state,
4
- // because nothing ever refetches the rows query after the write resolves.
5
- // Actions with a `redirect`/navigate target hide this — the screen remount
6
- // reloads everything. This test renders the real list path (KumikoScreen →
7
- // EntityListScreen → EntityListBody → RenderList) under a stub dispatcher
8
- // that counts `query()` calls, and proves the rows query refetches after a
9
- // successful row-action write, but NOT after a failed one (no double-fetch,
10
- // no silent refetch loop).
11
-
12
- import { describe, expect, test } from "bun:test";
13
- import type {
14
- EntityDefinition,
15
- EntityListScreenDefinition,
16
- ProjectionListScreenDefinition,
17
- } from "@cosmicdrift/kumiko-framework/ui-types";
18
- import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
19
- import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
20
- import type { ComponentType, ReactNode } from "react";
21
- import { DispatcherProvider } from "../../context/dispatcher-context";
22
- import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
23
- import { kumikoDefaultTranslations } from "../../i18n-defaults";
24
- import {
25
- type ButtonProps,
26
- type CorePrimitives,
27
- type DataTableProps,
28
- type DataTableRowAction,
29
- PrimitivesProvider,
30
- } from "../../primitives";
31
- import type { FeatureSchema } from "../feature-schema";
32
- import { KumikoScreen } from "../kumiko-screen";
33
- import { NavProvider } from "../nav";
34
-
35
- let capturedRowActions: readonly DataTableRowAction[] | undefined;
36
- const captureDataTable: ComponentType<DataTableProps> = (props) => {
37
- capturedRowActions = props.rowActions;
38
- return null;
39
- };
40
- const noop = (): ReactNode => null;
41
- const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
42
-
43
- const testPrimitives: CorePrimitives = {
44
- Button: noop,
45
- Banner: passChildren,
46
- Field: passChildren,
47
- Input: noop,
48
- DataTable: captureDataTable,
49
- Form: passChildren,
50
- Section: passChildren,
51
- Card: passChildren,
52
- Grid: passChildren,
53
- GridCell: passChildren,
54
- Text: passChildren,
55
- Heading: noop,
56
- Dialog: noop,
57
- Modal: noop,
58
- Lightbox: noop,
59
- ConfigSourceBadge: noop,
60
- ConfigCascadeView: noop,
61
- Link: noop,
62
- };
63
-
64
- let queryCallCount = 0;
65
- let writeIsSuccess = true;
66
-
67
- function stubDispatcher(): Dispatcher {
68
- return {
69
- write: (async () => {
70
- if (!writeIsSuccess) {
71
- return {
72
- isSuccess: false,
73
- error: {
74
- code: "unknown",
75
- httpStatus: 500,
76
- i18nKey: "kumiko:error:unknown",
77
- message: "boom",
78
- },
79
- };
80
- }
81
- return { isSuccess: true, data: {} };
82
- }) as unknown as Dispatcher["write"],
83
- query: (async () => {
84
- queryCallCount += 1;
85
- return {
86
- isSuccess: true,
87
- data: { rows: [{ id: "unit-1", status: "active" }], nextCursor: null, total: 1 },
88
- };
89
- }) as unknown as Dispatcher["query"],
90
- batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
91
- statusStore: {
92
- getState: () => "online",
93
- subscribe: () => () => {},
94
- } as unknown as Dispatcher["statusStore"],
95
- async *stream() {},
96
- pendingWrites: () => [],
97
- pendingFiles: () => [],
98
- };
99
- }
100
-
101
- function buildSchema(): FeatureSchema {
102
- const entity: EntityDefinition = {
103
- fields: {
104
- status: {
105
- type: "text",
106
- maxLength: 50,
107
- required: false,
108
- searchable: false,
109
- sortable: false,
110
- },
111
- },
112
- };
113
- const listScreen: EntityListScreenDefinition = {
114
- id: "unit-list",
115
- type: "entityList",
116
- entity: "unit",
117
- columns: ["status"],
118
- rowActions: [
119
- {
120
- id: "archive",
121
- label: "Archive",
122
- handler: "units:write:unit:archive",
123
- },
124
- ],
125
- };
126
- return {
127
- featureName: "units",
128
- entities: { unit: entity },
129
- screens: [listScreen],
130
- } as FeatureSchema;
131
- }
132
-
133
- function renderListScreen(): void {
134
- render(
135
- <LocaleProvider
136
- resolver={createStaticLocaleResolver({ locale: "de-DE" })}
137
- fallbackBundles={[kumikoDefaultTranslations]}
138
- >
139
- <DispatcherProvider dispatcher={stubDispatcher()}>
140
- <NavProvider
141
- value={{
142
- route: { screenId: "units:unit-list" },
143
- navigate: () => {},
144
- replace: () => {},
145
- hrefFor: () => "",
146
- searchParams: {},
147
- setSearchParams: () => {},
148
- }}
149
- >
150
- <PrimitivesProvider value={testPrimitives}>
151
- <KumikoScreen schema={buildSchema()} qn="units:screen:unit-list" />
152
- </PrimitivesProvider>
153
- </NavProvider>
154
- </DispatcherProvider>
155
- </LocaleProvider>,
156
- );
157
- }
158
-
159
- function requireArchiveAction(): DataTableRowAction {
160
- const action = capturedRowActions?.find((a) => a.id === "archive");
161
- if (!action) throw new Error("expected the 'archive' row action to be captured");
162
- return action;
163
- }
164
-
165
- describe("entityList row-action writeHandler refetches the rows query", () => {
166
- test("a successful row-action write triggers exactly one refetch", async () => {
167
- capturedRowActions = undefined;
168
- queryCallCount = 0;
169
- writeIsSuccess = true;
170
-
171
- renderListScreen();
172
-
173
- await waitFor(() => {
174
- expect(capturedRowActions).toBeDefined();
175
- });
176
-
177
- const countAfterMount = queryCallCount;
178
- expect(countAfterMount).toBeGreaterThan(0);
179
-
180
- await act(async () => {
181
- await requireArchiveAction().onTrigger({ id: "unit-1", values: { id: "unit-1" } });
182
- });
183
-
184
- await waitFor(() => {
185
- expect(queryCallCount).toBe(countAfterMount + 1);
186
- });
187
-
188
- // Give any accidental extra refetch a chance to land before asserting
189
- // there wasn't one (no double-fetch, no refetch loop).
190
- await new Promise((resolve) => setTimeout(resolve, 20));
191
- expect(queryCallCount).toBe(countAfterMount + 1);
192
- });
193
-
194
- test("a failed row-action write does not refetch the rows query", async () => {
195
- capturedRowActions = undefined;
196
- queryCallCount = 0;
197
- writeIsSuccess = false;
198
-
199
- renderListScreen();
200
-
201
- await waitFor(() => {
202
- expect(capturedRowActions).toBeDefined();
203
- });
204
-
205
- const countAfterMount = queryCallCount;
206
- expect(countAfterMount).toBeGreaterThan(0);
207
-
208
- await act(async () => {
209
- await expect(
210
- requireArchiveAction().onTrigger({ id: "unit-1", values: { id: "unit-1" } }),
211
- ).rejects.toThrow();
212
- });
213
-
214
- await new Promise((resolve) => setTimeout(resolve, 20));
215
- expect(queryCallCount).toBe(countAfterMount);
216
- });
217
- });
218
-
219
- // toolbarActions render through RenderList's `toolbarEnd` slot via the real
220
- // ToolbarActionView primitive, not through the DataTable's `rowActions` prop
221
- // (that's what captureDataTable above captures) — a stub that ignores
222
- // `toolbarEnd` never mounts the button, so this needs its own DataTable stub
223
- // that renders that slot, plus a real Button primitive to click through.
224
- const TestButton: ComponentType<ButtonProps> = ({ children, onClick, testId }) => (
225
- <button type="button" data-testid={testId} onClick={() => void onClick?.()}>
226
- {children}
227
- </button>
228
- );
229
-
230
- const renderToolbarEnd: ComponentType<DataTableProps> = (props) => <>{props.toolbarEnd}</>;
231
-
232
- const toolbarTestPrimitives: CorePrimitives = {
233
- ...testPrimitives,
234
- Button: TestButton,
235
- DataTable: renderToolbarEnd,
236
- };
237
-
238
- function buildToolbarSchema(): FeatureSchema {
239
- const entity: EntityDefinition = {
240
- fields: {
241
- status: {
242
- type: "text",
243
- maxLength: 50,
244
- required: false,
245
- searchable: false,
246
- sortable: false,
247
- },
248
- },
249
- };
250
- const listScreen: EntityListScreenDefinition = {
251
- id: "unit-list",
252
- type: "entityList",
253
- entity: "unit",
254
- columns: ["status"],
255
- toolbarActions: [
256
- {
257
- kind: "writeHandler",
258
- id: "sync",
259
- label: "Sync",
260
- handler: "units:write:unit:sync",
261
- },
262
- ],
263
- };
264
- return {
265
- featureName: "units",
266
- entities: { unit: entity },
267
- screens: [listScreen],
268
- } as FeatureSchema;
269
- }
270
-
271
- function renderToolbarListScreen(): void {
272
- render(
273
- <LocaleProvider
274
- resolver={createStaticLocaleResolver({ locale: "de-DE" })}
275
- fallbackBundles={[kumikoDefaultTranslations]}
276
- >
277
- <DispatcherProvider dispatcher={stubDispatcher()}>
278
- <NavProvider
279
- value={{
280
- route: { screenId: "units:unit-list" },
281
- navigate: () => {},
282
- replace: () => {},
283
- hrefFor: () => "",
284
- searchParams: {},
285
- setSearchParams: () => {},
286
- }}
287
- >
288
- <PrimitivesProvider value={toolbarTestPrimitives}>
289
- <KumikoScreen schema={buildToolbarSchema()} qn="units:screen:unit-list" />
290
- </PrimitivesProvider>
291
- </NavProvider>
292
- </DispatcherProvider>
293
- </LocaleProvider>,
294
- );
295
- }
296
-
297
- describe("entityList toolbarAction writeHandler refetches the rows query", () => {
298
- test("a successful toolbarAction write triggers exactly one refetch", async () => {
299
- queryCallCount = 0;
300
- writeIsSuccess = true;
301
-
302
- renderToolbarListScreen();
303
-
304
- const button = await waitFor(() => screen.getByTestId("render-list-toolbar-action-sync"));
305
- const countAfterMount = queryCallCount;
306
- expect(countAfterMount).toBeGreaterThan(0);
307
-
308
- fireEvent.click(button);
309
-
310
- await waitFor(() => {
311
- expect(queryCallCount).toBe(countAfterMount + 1);
312
- });
313
-
314
- // Give any accidental extra refetch a chance to land before asserting
315
- // there wasn't one.
316
- await new Promise((resolve) => setTimeout(resolve, 20));
317
- expect(queryCallCount).toBe(countAfterMount + 1);
318
- });
319
- });
320
-
321
- // projectionList runs its own writeHandler-refetch path (ProjectionListBody,
322
- // separate from EntityListBody above) — the copy-paste edit that added the
323
- // refetch call there is untested without this.
324
- function buildProjectionListSchema(): FeatureSchema {
325
- const listScreen: ProjectionListScreenDefinition = {
326
- id: "unit-projection-list",
327
- type: "projectionList",
328
- query: "units:query:unit:list",
329
- columns: [{ field: "status", label: "Status" }],
330
- rowActions: [
331
- {
332
- kind: "writeHandler",
333
- id: "archive",
334
- label: "Archive",
335
- handler: "units:write:unit:archive",
336
- },
337
- ],
338
- };
339
- return {
340
- featureName: "units",
341
- entities: {},
342
- screens: [listScreen],
343
- } as FeatureSchema;
344
- }
345
-
346
- function renderProjectionListScreen(): void {
347
- render(
348
- <LocaleProvider
349
- resolver={createStaticLocaleResolver({ locale: "de-DE" })}
350
- fallbackBundles={[kumikoDefaultTranslations]}
351
- >
352
- <DispatcherProvider dispatcher={stubDispatcher()}>
353
- <NavProvider
354
- value={{
355
- route: { screenId: "units:unit-projection-list" },
356
- navigate: () => {},
357
- replace: () => {},
358
- hrefFor: () => "",
359
- searchParams: {},
360
- setSearchParams: () => {},
361
- }}
362
- >
363
- <PrimitivesProvider value={testPrimitives}>
364
- <KumikoScreen
365
- schema={buildProjectionListSchema()}
366
- qn="units:screen:unit-projection-list"
367
- />
368
- </PrimitivesProvider>
369
- </NavProvider>
370
- </DispatcherProvider>
371
- </LocaleProvider>,
372
- );
373
- }
374
-
375
- describe("projectionList row-action writeHandler refetches the rows query", () => {
376
- test("a successful row-action write triggers exactly one refetch", async () => {
377
- capturedRowActions = undefined;
378
- queryCallCount = 0;
379
- writeIsSuccess = true;
380
-
381
- renderProjectionListScreen();
382
-
383
- await waitFor(() => {
384
- expect(capturedRowActions).toBeDefined();
385
- });
386
-
387
- const countAfterMount = queryCallCount;
388
- expect(countAfterMount).toBeGreaterThan(0);
389
-
390
- await act(async () => {
391
- await requireArchiveAction().onTrigger({ id: "unit-1", values: { id: "unit-1" } });
392
- });
393
-
394
- await waitFor(() => {
395
- expect(queryCallCount).toBe(countAfterMount + 1);
396
- });
397
-
398
- await new Promise((resolve) => setTimeout(resolve, 20));
399
- expect(queryCallCount).toBe(countAfterMount + 1);
400
- });
401
- });
@@ -1,128 +0,0 @@
1
- // A list screen's chrome is the DataTable's own outer wrapper — no
2
- // FormScreenShell/PageSection sits around it. `screenPadding` is how that
3
- // wrapper reaches the shared screen-padding token, so it has to be set on the
4
- // real list path (KumikoScreen → EntityListScreen → EntityListBody →
5
- // RenderList), not just be available on the primitive (fw#2640).
6
- import { describe, expect, test } from "bun:test";
7
- import type {
8
- EntityDefinition,
9
- EntityListScreenDefinition,
10
- } from "@cosmicdrift/kumiko-framework/ui-types";
11
- import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
12
- import { render, waitFor } from "@testing-library/react";
13
- import type { ComponentType, ReactNode } from "react";
14
- import { DispatcherProvider } from "../../context/dispatcher-context";
15
- import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
16
- import { type CorePrimitives, type DataTableProps, PrimitivesProvider } from "../../primitives";
17
- import type { FeatureSchema } from "../feature-schema";
18
- import { KumikoScreen } from "../kumiko-screen";
19
- import { NavProvider } from "../nav";
20
-
21
- const noop = (): ReactNode => null;
22
- const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
23
-
24
- function stubDispatcher(): Dispatcher {
25
- return {
26
- write: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["write"],
27
- query: (async () => ({
28
- isSuccess: true,
29
- data: { rows: [], total: 0 },
30
- })) as unknown as Dispatcher["query"],
31
- batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
32
- statusStore: {
33
- getState: () => "online",
34
- subscribe: () => () => {},
35
- } as unknown as Dispatcher["statusStore"],
36
- async *stream() {},
37
- pendingWrites: () => [],
38
- pendingFiles: () => [],
39
- };
40
- }
41
-
42
- function buildSchema(fillHeight?: boolean): FeatureSchema {
43
- const entity: EntityDefinition = {
44
- fields: {
45
- name: { type: "text", maxLength: 200, required: false, searchable: false, sortable: false },
46
- },
47
- };
48
- const listScreen: EntityListScreenDefinition = {
49
- id: "org-list",
50
- type: "entityList",
51
- entity: "org",
52
- columns: ["name"],
53
- ...(fillHeight !== undefined && { fillHeight }),
54
- };
55
- return {
56
- featureName: "orgs",
57
- entities: { org: entity },
58
- screens: [listScreen],
59
- } as FeatureSchema;
60
- }
61
-
62
- async function captureListProps(fillHeight?: boolean): Promise<DataTableProps> {
63
- let captured: DataTableProps | undefined;
64
- const capturingDataTable: ComponentType<DataTableProps> = (props) => {
65
- captured = props;
66
- return null;
67
- };
68
- const primitives: CorePrimitives = {
69
- Button: noop,
70
- Banner: passChildren,
71
- Field: passChildren,
72
- Input: noop,
73
- DataTable: capturingDataTable,
74
- Form: passChildren,
75
- Section: passChildren,
76
- Card: passChildren,
77
- Grid: passChildren,
78
- GridCell: passChildren,
79
- Text: passChildren,
80
- Heading: noop,
81
- Dialog: noop,
82
- Modal: noop,
83
- Lightbox: noop,
84
- ConfigSourceBadge: noop,
85
- ConfigCascadeView: noop,
86
- Link: noop,
87
- };
88
- render(
89
- <LocaleProvider resolver={createStaticLocaleResolver({ locale: "de-DE" })}>
90
- <DispatcherProvider dispatcher={stubDispatcher()}>
91
- <NavProvider
92
- value={{
93
- route: { screenId: "orgs:org-list" },
94
- navigate: () => {},
95
- replace: () => {},
96
- hrefFor: () => "",
97
- searchParams: {},
98
- setSearchParams: () => {},
99
- }}
100
- >
101
- <PrimitivesProvider value={primitives}>
102
- <KumikoScreen schema={buildSchema(fillHeight)} qn="orgs:screen:org-list" />
103
- </PrimitivesProvider>
104
- </NavProvider>
105
- </DispatcherProvider>
106
- </LocaleProvider>,
107
- );
108
-
109
- await waitFor(() => expect(captured).toBeDefined());
110
- if (captured === undefined) throw new Error("DataTable was not rendered");
111
- return captured;
112
- }
113
-
114
- describe("entityList screen padding (fw#2640)", () => {
115
- test("the list screen marks its table as the screen body", async () => {
116
- expect((await captureListProps()).screenPadding).toBe(true);
117
- });
118
- });
119
-
120
- describe("entityList fillHeight (fw#3381)", () => {
121
- test("fills the shell height by default (scrollBody)", async () => {
122
- expect((await captureListProps()).scrollBody).toBe(true);
123
- });
124
-
125
- test("fillHeight: false restores page scroll (no scrollBody)", async () => {
126
- expect((await captureListProps(false)).scrollBody).toBeUndefined();
127
- });
128
- });