@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,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
- });