@cosmicdrift/kumiko-renderer 0.327.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 (257) 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 -44
  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 -38
  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/dist/inside-drawer.js +9 -0
  119. package/dist/page-header-slot.d.ts +2 -0
  120. package/dist/page-header-slot.js +9 -0
  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/changes.json +52 -0
  135. package/src/__tests__/error-i18n-defaults.test.ts +0 -82
  136. package/src/__tests__/format-when.test.ts +0 -12
  137. package/src/__tests__/i18n.test.tsx +0 -283
  138. package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
  139. package/src/__tests__/qn.test.ts +0 -83
  140. package/src/__tests__/share-links.test.ts +0 -26
  141. package/src/__tests__/sort-by-accessor.test.ts +0 -48
  142. package/src/__tests__/stringify-nav-params.test.ts +0 -33
  143. package/src/__tests__/use-list-url-state.test.tsx +0 -250
  144. package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
  145. package/src/app/__tests__/action-form-shim.test.ts +0 -137
  146. package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
  147. package/src/app/__tests__/config-edit-shim.test.ts +0 -57
  148. package/src/app/__tests__/content-editors.test.tsx +0 -128
  149. package/src/app/__tests__/content-preview.test.tsx +0 -184
  150. package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
  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 -111
  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 -3614
  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 -85
  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 -639
  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 -1669
  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 -60
  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 -467
  228. package/src/components/render-edit-action-button.tsx +0 -106
  229. package/src/components/render-edit-logic.ts +0 -85
  230. package/src/components/render-edit-types.ts +0 -216
  231. package/src/components/render-edit.tsx +0 -1700
  232. package/src/components/render-field.tsx +0 -942
  233. package/src/components/render-list.tsx +0 -561
  234. package/src/components/write-form-section.tsx +0 -199
  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-stream-handler.test.tsx +0 -107
  241. package/src/hooks/use-ai-text.ts +0 -172
  242. package/src/hooks/use-disclosure.ts +0 -20
  243. package/src/hooks/use-form.ts +0 -78
  244. package/src/hooks/use-list-url-state.ts +0 -171
  245. package/src/hooks/use-mutation.ts +0 -61
  246. package/src/hooks/use-query.ts +0 -132
  247. package/src/hooks/use-reference-lookup.ts +0 -76
  248. package/src/hooks/use-store.ts +0 -48
  249. package/src/hooks/use-stream-handler.ts +0 -134
  250. package/src/i18n-defaults.ts +0 -220
  251. package/src/i18n.tsx +0 -286
  252. package/src/index.ts +0 -272
  253. package/src/primitives.tsx +0 -1446
  254. package/src/share-links.ts +0 -12
  255. package/src/sort-by-accessor.ts +0 -20
  256. package/src/sse/live-events.tsx +0 -56
  257. package/src/tokens.tsx +0 -170
@@ -1,719 +0,0 @@
1
- // fw#2166: projectionDetail screens can declare header `actions`, and get a
2
- // default "edit" action for free when `detailFor` names an entity that has a
3
- // visible entityEdit screen somewhere in the app (not just this feature —
4
- // the motivating case is a projectionDetail whose query belongs to one
5
- // feature while the entity's entityEdit screen lives in another). This
6
- // renders the real path (KumikoScreen → ProjectionDetailBody → RenderEdit)
7
- // under a stub dispatcher + AppFeaturesProvider, and proves the default-edit
8
- // resolution rules from the boot-validator's detailFor doc (cross-feature
9
- // lookup, access-gating, id: "edit" declared-action suppression).
10
-
11
- import { describe, expect, test } from "bun:test";
12
- import type {
13
- AccessRule,
14
- EntityEditScreenDefinition,
15
- ProjectionDetailScreenDefinition,
16
- } from "@cosmicdrift/kumiko-framework/ui-types";
17
- import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
18
- import { act, fireEvent, render, waitFor } from "@testing-library/react";
19
- import type { ComponentType, ReactNode } from "react";
20
- import { useState } from "react";
21
- import { DispatcherProvider } from "../../context/dispatcher-context";
22
- import { UserRolesProvider } from "../../context/user-roles-context";
23
- import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
24
- import { kumikoDefaultTranslations } from "../../i18n-defaults";
25
- import {
26
- type ActionOverflowMenuProps,
27
- type BannerProps,
28
- type ButtonProps,
29
- type CardProps,
30
- type CorePrimitives,
31
- type DialogProps,
32
- type FormProps,
33
- PrimitivesProvider,
34
- } from "../../primitives";
35
- import { AppFeaturesProvider } from "../app-features-context";
36
- import type { FeatureSchema } from "../feature-schema";
37
- import { KumikoScreen } from "../kumiko-screen";
38
- import type { NavApi, ScreenTarget } from "../nav";
39
- import { NavProvider } from "../nav";
40
-
41
- const noop = (): ReactNode => null;
42
- const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
43
-
44
- // Card's title/subtitle/headerActions arrive as `slots`; this stub renders
45
- // them so tests asserting on header-action content still see it.
46
- const CardWithSlots: ComponentType<CardProps> = ({ slots, children }) => (
47
- <>
48
- {slots?.title}
49
- {slots?.subtitle}
50
- {slots?.headerActions}
51
- {children}
52
- </>
53
- );
54
-
55
- const TestButton: ComponentType<ButtonProps> = ({ children, onClick, testId }) => (
56
- <button type="button" data-testid={testId} onClick={() => void onClick?.()}>
57
- {children}
58
- </button>
59
- );
60
-
61
- // Header actions (and their error banner) now render inside ProjectionDetail's
62
- // own head Card, passed through as `headerRegion` (fw#2713) rather than
63
- // through Form's `actions`/`secondaryActions` slots — wrapped in a distinct
64
- // testid'd container so a test can assert both live there, not in the
65
- // actions/body regions Form itself owns.
66
- const FormWithActions: ComponentType<FormProps> = ({
67
- children,
68
- actions,
69
- secondaryActions,
70
- headerRegion,
71
- }) => (
72
- <>
73
- {headerRegion !== undefined && <div data-testid="form-header">{headerRegion}</div>}
74
- <div data-testid="form-body">{children}</div>
75
- <div data-testid="form-actions">
76
- {secondaryActions}
77
- {actions}
78
- </div>
79
- </>
80
- );
81
-
82
- // passChildren would drop testId — this test asserts on Banner structure/
83
- // placement, so it needs a real (if minimal) wrapper element.
84
- const TestBanner: ComponentType<BannerProps> = ({ children, testId }) => (
85
- <div data-testid={testId}>{children}</div>
86
- );
87
-
88
- // Minimal overflow-menu double (A7): a toggle button reveals the items as
89
- // plain buttons, own testid per item so a test can assert an item is
90
- // reachable only after the menu opens — no dropdown/portal machinery, this
91
- // package doesn't test the real (renderer-web) widget.
92
- const TestActionOverflowMenu: ComponentType<ActionOverflowMenuProps> = ({
93
- items,
94
- label,
95
- testId,
96
- }) => {
97
- const [open, setOpen] = useState(false);
98
- return (
99
- <div>
100
- <button
101
- type="button"
102
- aria-label={label}
103
- data-testid={testId ?? "action-overflow-trigger"}
104
- onClick={() => setOpen((o) => !o)}
105
- >
106
- {"⋯"}
107
- </button>
108
- {open &&
109
- items.map((item) => (
110
- <button
111
- key={item.id}
112
- type="button"
113
- data-testid={`${testId ?? "action-overflow"}-item-${item.id}`}
114
- disabled={item.disabled}
115
- onClick={item.onSelect}
116
- >
117
- {item.label}
118
- </button>
119
- ))}
120
- </div>
121
- );
122
- };
123
-
124
- // Records the props of the last Dialog rendered *open* (a closed Dialog from
125
- // an always-mounted RenderEditActionButton never overwrites it) — lets a
126
- // test assert a confirm was requested and drive its onConfirm, without a
127
- // real dialog/portal implementation.
128
- type DialogSpy = ComponentType<DialogProps> & { lastProps: DialogProps | null };
129
- const TestDialog: DialogSpy = Object.assign(
130
- (props: DialogProps): ReactNode => {
131
- if (props.open) TestDialog.lastProps = props;
132
- return null;
133
- },
134
- { lastProps: null as DialogProps | null },
135
- );
136
-
137
- const testPrimitives: CorePrimitives = {
138
- Button: TestButton,
139
- Banner: TestBanner,
140
- Field: passChildren,
141
- Input: noop,
142
- DataTable: noop,
143
- Form: FormWithActions,
144
- Section: passChildren,
145
- Card: CardWithSlots,
146
- Grid: passChildren,
147
- GridCell: passChildren,
148
- Text: passChildren,
149
- Heading: noop,
150
- Dialog: TestDialog,
151
- Modal: noop,
152
- Lightbox: noop,
153
- ConfigSourceBadge: noop,
154
- ConfigCascadeView: noop,
155
- Link: noop,
156
- ActionOverflowMenu: TestActionOverflowMenu,
157
- };
158
-
159
- function stubDispatcher(
160
- record: Readonly<Record<string, unknown>>,
161
- writeErrorMessage?: string,
162
- ): Dispatcher {
163
- const writeResult =
164
- writeErrorMessage !== undefined
165
- ? {
166
- isSuccess: false,
167
- error: {
168
- code: "write-failed",
169
- httpStatus: 500,
170
- i18nKey: "kumiko.errors.does-not-exist-in-any-bundle",
171
- message: writeErrorMessage,
172
- },
173
- }
174
- : { isSuccess: true, data: {} };
175
- return {
176
- write: (async () => writeResult) as unknown as Dispatcher["write"],
177
- query: (async () => ({ isSuccess: true, data: record })) as unknown as Dispatcher["query"],
178
- batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
179
- statusStore: {
180
- getState: () => "online",
181
- subscribe: () => () => {},
182
- } as unknown as Dispatcher["statusStore"],
183
- async *stream() {},
184
- pendingWrites: () => [],
185
- pendingFiles: () => [],
186
- };
187
- }
188
-
189
- function detailScreen(
190
- overrides?: Partial<ProjectionDetailScreenDefinition & { readonly detailFor?: string }>,
191
- ): FeatureSchema["screens"][number] {
192
- return {
193
- id: "rent-detail",
194
- type: "projectionDetail",
195
- query: "app:query:rent:detail",
196
- layout: { sections: [{ title: "s", fields: ["description"] }] },
197
- detailFor: "rent",
198
- ...overrides,
199
- } as FeatureSchema["screens"][number];
200
- }
201
-
202
- // Registry-qualified id ("<feature>:screen:<short>"), matching real schema
203
- // output — a bare short id here would hide the QN-vs-short-form bug this
204
- // test suite exists to catch (fw#2166 review finding 1/4).
205
- function editScreen(
206
- entity: string,
207
- access?: AccessRule,
208
- featureName = "app",
209
- ): EntityEditScreenDefinition {
210
- return {
211
- id: `${featureName}:screen:rent-edit`,
212
- type: "entityEdit",
213
- entity,
214
- layout: { sections: [{ columns: 1, fields: ["name"] }] },
215
- ...(access !== undefined && { access }),
216
- };
217
- }
218
-
219
- function renderDetail(opts: {
220
- readonly primarySchema: FeatureSchema;
221
- readonly features: readonly FeatureSchema[];
222
- readonly userRoles?: readonly string[];
223
- readonly record?: Readonly<Record<string, unknown>>;
224
- readonly onNavigate?: (target: ScreenTarget) => void;
225
- readonly writeErrorMessage?: string;
226
- }): ReturnType<typeof render> {
227
- const navApi: NavApi = {
228
- route: { screenId: "app:screen:rent-detail" },
229
- // Header actions only ever navigate with a ScreenTarget (screenId +
230
- // entityId) — same narrowing idiom as nav.tsx's resolveTarget.
231
- navigate: (target) => {
232
- if ("screenId" in target) opts.onNavigate?.(target);
233
- },
234
- replace: () => {},
235
- hrefFor: () => "",
236
- searchParams: {},
237
- setSearchParams: () => {},
238
- };
239
- return render(
240
- <LocaleProvider
241
- resolver={createStaticLocaleResolver({ locale: "de-DE" })}
242
- fallbackBundles={[kumikoDefaultTranslations]}
243
- >
244
- <DispatcherProvider
245
- dispatcher={stubDispatcher(
246
- opts.record ?? { id: "rent-1", description: "Rent for April" },
247
- opts.writeErrorMessage,
248
- )}
249
- >
250
- <AppFeaturesProvider features={opts.features}>
251
- <UserRolesProvider roles={opts.userRoles}>
252
- <NavProvider value={navApi}>
253
- <PrimitivesProvider value={testPrimitives}>
254
- <KumikoScreen
255
- schema={opts.primarySchema}
256
- qn="app:screen:rent-detail"
257
- entityId="rent-1"
258
- />
259
- </PrimitivesProvider>
260
- </NavProvider>
261
- </UserRolesProvider>
262
- </AppFeaturesProvider>
263
- </DispatcherProvider>
264
- </LocaleProvider>,
265
- );
266
- }
267
-
268
- describe("projectionDetail default edit action (fw#2166)", () => {
269
- test("detailFor + matching entityEdit in the SAME feature → edit button navigates with the record id", async () => {
270
- const schema: FeatureSchema = {
271
- featureName: "app",
272
- entities: {},
273
- screens: [detailScreen(), editScreen("rent")],
274
- };
275
- let navigated: ScreenTarget | undefined;
276
- const { getByTestId, queryByText } = renderDetail({
277
- primarySchema: schema,
278
- features: [schema],
279
- userRoles: [],
280
- onNavigate: (target) => {
281
- navigated = target;
282
- },
283
- });
284
- await waitFor(() => expect(queryByText("Loading…")).toBeNull());
285
-
286
- // trigger() is async (busy-state bookkeeping around onPress), so the
287
- // click's state updates land after this tick — act() flushes them.
288
- await act(async () => {
289
- fireEvent.click(getByTestId("render-edit-action-edit"));
290
- });
291
- expect(navigated).toEqual({ screenId: "rent-edit", entityId: "rent-1" });
292
- });
293
-
294
- test("edit screen access denied → no edit button", async () => {
295
- const schema: FeatureSchema = {
296
- featureName: "app",
297
- entities: {},
298
- screens: [detailScreen(), editScreen("rent", { roles: ["admin"] })],
299
- };
300
- const { queryByTestId, queryByText } = renderDetail({
301
- primarySchema: schema,
302
- features: [schema],
303
- userRoles: [],
304
- });
305
- await waitFor(() => expect(queryByText("Loading…")).toBeNull());
306
-
307
- expect(queryByTestId("render-edit-action-edit")).toBeNull();
308
- });
309
-
310
- test("no entityEdit screen for the detailFor entity → no edit button", async () => {
311
- const schema: FeatureSchema = {
312
- featureName: "app",
313
- entities: {},
314
- screens: [detailScreen()],
315
- };
316
- const { queryByTestId, queryByText } = renderDetail({
317
- primarySchema: schema,
318
- features: [schema],
319
- userRoles: [],
320
- });
321
- await waitFor(() => expect(queryByText("Loading…")).toBeNull());
322
-
323
- expect(queryByTestId("render-edit-action-edit")).toBeNull();
324
- });
325
-
326
- test('a declared actions entry with id: "edit" wins — exactly one edit button, the declared one', async () => {
327
- const schema: FeatureSchema = {
328
- featureName: "app",
329
- entities: {},
330
- screens: [
331
- detailScreen({
332
- actions: [
333
- { kind: "navigate", id: "edit", label: "custom-edit-label", screen: "rent-edit" },
334
- ],
335
- }),
336
- editScreen("rent"),
337
- ],
338
- };
339
- const { getAllByTestId, queryByText } = renderDetail({
340
- primarySchema: schema,
341
- features: [schema],
342
- userRoles: [],
343
- });
344
- await waitFor(() => expect(queryByText("Loading…")).toBeNull());
345
-
346
- const editButtons = getAllByTestId("render-edit-action-edit");
347
- expect(editButtons).toHaveLength(1);
348
- // "custom-edit-label" has no translation registered, so translate()
349
- // returns the raw key — proving this is the declared action, not the
350
- // default (which renders the translated "kumiko.actions.edit").
351
- expect(editButtons[0]?.textContent).toBe("custom-edit-label");
352
- });
353
-
354
- test("two entityEdit screens for the same entity, only the second accessible → button still renders, targeting the second (review finding 2)", async () => {
355
- const lockedEdit: EntityEditScreenDefinition = {
356
- id: "app:screen:rent-edit-locked",
357
- type: "entityEdit",
358
- entity: "rent",
359
- layout: { sections: [{ columns: 1, fields: ["name"] }] },
360
- access: { roles: ["admin"] },
361
- };
362
- const openEdit: EntityEditScreenDefinition = {
363
- id: "app:screen:rent-edit-open",
364
- type: "entityEdit",
365
- entity: "rent",
366
- layout: { sections: [{ columns: 1, fields: ["name"] }] },
367
- };
368
- const schema: FeatureSchema = {
369
- featureName: "app",
370
- entities: {},
371
- screens: [detailScreen(), lockedEdit, openEdit],
372
- };
373
- let navigated: ScreenTarget | undefined;
374
- const { getByTestId, queryByText } = renderDetail({
375
- primarySchema: schema,
376
- features: [schema],
377
- userRoles: [],
378
- onNavigate: (target) => {
379
- navigated = target;
380
- },
381
- });
382
- await waitFor(() => expect(queryByText("Loading…")).toBeNull());
383
-
384
- await act(async () => {
385
- fireEvent.click(getByTestId("render-edit-action-edit"));
386
- });
387
- expect(navigated).toEqual({ screenId: "rent-edit-open", entityId: "rent-1" });
388
- });
389
-
390
- test("declared navigate action without entityId, targeting an entityEdit of the SAME entity → auto-fills the record id (review finding 3)", async () => {
391
- const schema: FeatureSchema = {
392
- featureName: "app",
393
- entities: {},
394
- screens: [
395
- detailScreen({
396
- actions: [
397
- {
398
- kind: "navigate",
399
- id: "open-record",
400
- label: "actions.openRecord",
401
- screen: "rent-edit",
402
- },
403
- ],
404
- }),
405
- editScreen("rent"),
406
- ],
407
- };
408
- let navigated: ScreenTarget | undefined;
409
- const { getByTestId, queryByText } = renderDetail({
410
- primarySchema: schema,
411
- features: [schema],
412
- userRoles: [],
413
- onNavigate: (target) => {
414
- navigated = target;
415
- },
416
- });
417
- await waitFor(() => expect(queryByText("Loading…")).toBeNull());
418
-
419
- await act(async () => {
420
- fireEvent.click(getByTestId("render-edit-action-open-record"));
421
- });
422
- expect(navigated).toEqual({ screenId: "rent-edit", entityId: "rent-1" });
423
- });
424
-
425
- test("declared action with a non-matching visible condition → no button rendered", async () => {
426
- const schema: FeatureSchema = {
427
- featureName: "app",
428
- entities: {},
429
- screens: [
430
- detailScreen({
431
- actions: [
432
- {
433
- kind: "navigate",
434
- id: "archive",
435
- label: "actions.archive",
436
- screen: "rent-edit",
437
- visible: { field: "status", eq: "closed" },
438
- },
439
- ],
440
- }),
441
- ],
442
- };
443
- const { queryByTestId, queryByText } = renderDetail({
444
- primarySchema: schema,
445
- features: [schema],
446
- userRoles: [],
447
- record: { id: "rent-1", description: "Rent for April", status: "open" },
448
- });
449
- await waitFor(() => expect(queryByText("Loading…")).toBeNull());
450
-
451
- expect(queryByTestId("render-edit-action-archive")).toBeNull();
452
- });
453
-
454
- test("entityEdit screen lives in ANOTHER feature → edit button is still there (cross-feature resolution)", async () => {
455
- const appSchema: FeatureSchema = {
456
- featureName: "app",
457
- entities: {},
458
- screens: [detailScreen()],
459
- };
460
- const billingSchema: FeatureSchema = {
461
- featureName: "billing",
462
- entities: {},
463
- screens: [editScreen("rent", undefined, "billing")],
464
- };
465
- let navigated: ScreenTarget | undefined;
466
- const { getByTestId, queryByText } = renderDetail({
467
- primarySchema: appSchema,
468
- features: [appSchema, billingSchema],
469
- userRoles: [],
470
- onNavigate: (target) => {
471
- navigated = target;
472
- },
473
- });
474
- await waitFor(() => expect(queryByText("Loading…")).toBeNull());
475
-
476
- // trigger() is async (busy-state bookkeeping around onPress), so the
477
- // click's state updates land after this tick — act() flushes them.
478
- await act(async () => {
479
- fireEvent.click(getByTestId("render-edit-action-edit"));
480
- });
481
- expect(navigated).toEqual({ screenId: "rent-edit", entityId: "rent-1" });
482
- });
483
-
484
- test("two header actions → both render as buttons, no overflow trigger (fw bedienkonzept A7)", async () => {
485
- const schema: FeatureSchema = {
486
- featureName: "app",
487
- entities: {},
488
- screens: [
489
- detailScreen({
490
- actions: [
491
- { kind: "navigate", id: "edit", label: "actions.edit", screen: "rent-edit" },
492
- { kind: "navigate", id: "duplicate", label: "actions.duplicate", screen: "rent-edit" },
493
- ],
494
- }),
495
- editScreen("rent"),
496
- ],
497
- };
498
- const { getByTestId, queryByTestId, queryByText } = renderDetail({
499
- primarySchema: schema,
500
- features: [schema],
501
- userRoles: [],
502
- });
503
- await waitFor(() => expect(queryByText("Loading…")).toBeNull());
504
-
505
- expect(getByTestId("render-edit-action-edit").textContent).toBe("actions.edit");
506
- expect(getByTestId("render-edit-action-duplicate").textContent).toBe("actions.duplicate");
507
- expect(queryByTestId("kumiko-screen-projection-detail-actions-overflow")).toBeNull();
508
- });
509
-
510
- test("three header actions → edit stays a text button, the rest collapse into the overflow menu (fw bedienkonzept A7)", async () => {
511
- const schema: FeatureSchema = {
512
- featureName: "app",
513
- entities: {},
514
- screens: [
515
- detailScreen({
516
- actions: [
517
- { kind: "navigate", id: "edit", label: "actions.edit", screen: "rent-edit" },
518
- { kind: "navigate", id: "duplicate", label: "actions.duplicate", screen: "rent-edit" },
519
- { kind: "navigate", id: "audit-log", label: "actions.auditLog", screen: "rent-edit" },
520
- ],
521
- }),
522
- editScreen("rent"),
523
- ],
524
- };
525
- const { getByTestId, queryByTestId, queryByText } = renderDetail({
526
- primarySchema: schema,
527
- features: [schema],
528
- userRoles: [],
529
- });
530
- await waitFor(() => expect(queryByText("Loading…")).toBeNull());
531
-
532
- expect(getByTestId("render-edit-action-edit").textContent).toBe("actions.edit");
533
- expect(queryByTestId("render-edit-action-duplicate")).toBeNull();
534
- expect(queryByTestId("render-edit-action-audit-log")).toBeNull();
535
-
536
- fireEvent.click(getByTestId("kumiko-screen-projection-detail-actions-overflow"));
537
- expect(
538
- getByTestId("kumiko-screen-projection-detail-actions-overflow-item-duplicate").textContent,
539
- ).toBe("actions.duplicate");
540
- expect(
541
- getByTestId("kumiko-screen-projection-detail-actions-overflow-item-audit-log").textContent,
542
- ).toBe("actions.auditLog");
543
- });
544
-
545
- test("four header actions incl. edit → edit stays a text button, the other three are only reachable via the overflow menu (fw bedienkonzept A7)", async () => {
546
- const schema: FeatureSchema = {
547
- featureName: "app",
548
- entities: {},
549
- screens: [
550
- detailScreen({
551
- actions: [
552
- { kind: "navigate", id: "edit", label: "actions.edit", screen: "rent-edit" },
553
- { kind: "navigate", id: "duplicate", label: "actions.duplicate", screen: "rent-edit" },
554
- { kind: "navigate", id: "audit-log", label: "actions.auditLog", screen: "rent-edit" },
555
- { kind: "navigate", id: "archive", label: "actions.archive", screen: "rent-edit" },
556
- ],
557
- }),
558
- editScreen("rent"),
559
- ],
560
- };
561
- const { getByTestId, queryByTestId, queryByText } = renderDetail({
562
- primarySchema: schema,
563
- features: [schema],
564
- userRoles: [],
565
- });
566
- await waitFor(() => expect(queryByText("Loading…")).toBeNull());
567
-
568
- expect(getByTestId("render-edit-action-edit").textContent).toBe("actions.edit");
569
- expect(queryByTestId("render-edit-action-duplicate")).toBeNull();
570
- expect(queryByTestId("render-edit-action-audit-log")).toBeNull();
571
- expect(queryByTestId("render-edit-action-archive")).toBeNull();
572
-
573
- fireEvent.click(getByTestId("kumiko-screen-projection-detail-actions-overflow"));
574
- expect(
575
- getByTestId("kumiko-screen-projection-detail-actions-overflow-item-duplicate").textContent,
576
- ).toBe("actions.duplicate");
577
- expect(
578
- getByTestId("kumiko-screen-projection-detail-actions-overflow-item-audit-log").textContent,
579
- ).toBe("actions.auditLog");
580
- expect(
581
- getByTestId("kumiko-screen-projection-detail-actions-overflow-item-archive").textContent,
582
- ).toBe("actions.archive");
583
- });
584
-
585
- test('header action without an id: "edit" entry → the style: "primary" action stays the visible button (fw bedienkonzept A7)', async () => {
586
- const schema: FeatureSchema = {
587
- featureName: "app",
588
- entities: {},
589
- screens: [
590
- detailScreen({
591
- actions: [
592
- { kind: "navigate", id: "duplicate", label: "actions.duplicate", screen: "rent-edit" },
593
- {
594
- kind: "navigate",
595
- id: "approve",
596
- label: "actions.approve",
597
- screen: "rent-edit",
598
- style: "primary",
599
- },
600
- { kind: "navigate", id: "audit-log", label: "actions.auditLog", screen: "rent-edit" },
601
- ],
602
- }),
603
- ],
604
- };
605
- const { getByTestId, queryByTestId, queryByText } = renderDetail({
606
- primarySchema: schema,
607
- features: [schema],
608
- userRoles: [],
609
- });
610
- await waitFor(() => expect(queryByText("Loading…")).toBeNull());
611
-
612
- expect(getByTestId("render-edit-action-approve").textContent).toBe("actions.approve");
613
- expect(queryByTestId("render-edit-action-duplicate")).toBeNull();
614
- expect(queryByTestId("render-edit-action-audit-log")).toBeNull();
615
-
616
- fireEvent.click(getByTestId("kumiko-screen-projection-detail-actions-overflow"));
617
- expect(
618
- getByTestId("kumiko-screen-projection-detail-actions-overflow-item-duplicate").textContent,
619
- ).toBe("actions.duplicate");
620
- expect(
621
- getByTestId("kumiko-screen-projection-detail-actions-overflow-item-audit-log").textContent,
622
- ).toBe("actions.auditLog");
623
- });
624
-
625
- test("a danger overflow-menu action still asks for confirmation before firing (fw bedienkonzept A7)", async () => {
626
- const schema: FeatureSchema = {
627
- featureName: "app",
628
- entities: {},
629
- screens: [
630
- detailScreen({
631
- actions: [
632
- { kind: "navigate", id: "edit", label: "actions.edit", screen: "rent-edit" },
633
- { kind: "navigate", id: "duplicate", label: "actions.duplicate", screen: "rent-edit" },
634
- {
635
- kind: "writeHandler",
636
- id: "archive",
637
- label: "actions.archive",
638
- handler: "app:write:archive",
639
- style: "danger",
640
- },
641
- ],
642
- }),
643
- editScreen("rent"),
644
- ],
645
- };
646
- // `as DialogProps | null` widens the assignment so tsc doesn't narrow
647
- // the property to the literal `null` type, which turns the later
648
- // optional-chained reads below into `never` (compiler quirk).
649
- TestDialog.lastProps = null as DialogProps | null;
650
- const { getByTestId, queryByTestId, queryByText } = renderDetail({
651
- primarySchema: schema,
652
- features: [schema],
653
- userRoles: [],
654
- writeErrorMessage: "archive failed: rent is still active",
655
- });
656
- await waitFor(() => expect(queryByText("Loading…")).toBeNull());
657
-
658
- fireEvent.click(getByTestId("kumiko-screen-projection-detail-actions-overflow"));
659
- fireEvent.click(getByTestId("kumiko-screen-projection-detail-actions-overflow-item-archive"));
660
-
661
- // Danger action must not fire on the menu click alone — it needs the
662
- // confirm dialog first, same as a danger row action.
663
- expect(queryByTestId("render-edit-action-error")).toBeNull();
664
- expect(TestDialog.lastProps?.open).toBe(true);
665
- expect(TestDialog.lastProps?.title).toBe("actions.archive");
666
-
667
- await act(async () => {
668
- await TestDialog.lastProps?.onConfirm();
669
- });
670
-
671
- const errorBanner = await waitFor(() => getByTestId("render-edit-action-error"));
672
- expect(errorBanner.textContent).toBe("archive failed: rent is still active");
673
- });
674
-
675
- test("a failed writeHandler action shows its error in the head region, alongside the action button, NOT in Form's own actions/body regions (fw#2713)", async () => {
676
- const schema: FeatureSchema = {
677
- featureName: "app",
678
- entities: {},
679
- screens: [
680
- detailScreen({
681
- actions: [
682
- {
683
- kind: "writeHandler",
684
- id: "archive",
685
- label: "actions.archive",
686
- handler: "app:write:archive",
687
- },
688
- ],
689
- }),
690
- ],
691
- };
692
- const { getByTestId, queryByText } = renderDetail({
693
- primarySchema: schema,
694
- features: [schema],
695
- userRoles: [],
696
- writeErrorMessage: "archive failed: rent is still active",
697
- });
698
- await waitFor(() => expect(queryByText("Loading…")).toBeNull());
699
-
700
- await act(async () => {
701
- fireEvent.click(getByTestId("render-edit-action-archive"));
702
- });
703
-
704
- const errorBanner = await waitFor(() => getByTestId("render-edit-action-error"));
705
- expect(errorBanner.textContent).toBe("archive failed: rent is still active");
706
- // Header actions (and their failure banner) render in the head Card via
707
- // `headerRegion` now (fw#2713) — not through Form's own actions/body
708
- // slots, so both a tab switch and Form's own footer leave them untouched.
709
- expect(errorBanner.closest('[data-testid="form-header"]')).not.toBeNull();
710
- expect(errorBanner.closest('[data-testid="form-body"]')).toBeNull();
711
- expect(errorBanner.closest('[data-testid="form-actions"]')).toBeNull();
712
- expect(
713
- getByTestId("render-edit-action-archive").closest('[data-testid="form-header"]'),
714
- ).not.toBeNull();
715
- expect(
716
- getByTestId("render-edit-action-archive").closest('[data-testid="form-actions"]'),
717
- ).toBeNull();
718
- });
719
- });