@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,165 +0,0 @@
1
- // fw#2662: projectionDetail fields have no EntityDefinition to carry a real
2
- // "reference" field type (the shim hardcodes every field as "text"), so a
3
- // declared reference field used to render the raw id. This renders the real
4
- // path (KumikoScreen -> ProjectionDetailScreen -> RenderEdit -> RenderField
5
- // -> ReadOnlyReferenceValue) under a stub dispatcher.
6
-
7
- import { describe, expect, test } from "bun:test";
8
- import type { ProjectionDetailScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
9
- import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
10
- import { render, waitFor } from "@testing-library/react";
11
- import type { ComponentType, ReactNode } from "react";
12
- import { DispatcherProvider } from "../../context/dispatcher-context";
13
- import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
14
- import { kumikoDefaultTranslations } from "../../i18n-defaults";
15
- import { type CorePrimitives, PrimitivesProvider, type TextProps } from "../../primitives";
16
- import type { FeatureSchema } from "../feature-schema";
17
- import { KumikoScreen } from "../kumiko-screen";
18
- import type { NavApi } from "../nav";
19
- import { NavProvider } from "../nav";
20
-
21
- const SYSTEM_USER_ID = "00000000-0000-0000-0000-000000000000";
22
-
23
- const noop = (): ReactNode => null;
24
- const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
25
- // passChildren would drop testId — ReadOnlyReferenceValue/readOnlyDisplayText
26
- // render their resolved text into `<Text testId="field-value-<field>">`, and
27
- // the assertions below read it back by that testid.
28
- const TestText: ComponentType<TextProps> = ({ children, testId }) => (
29
- <span data-testid={testId}>{children}</span>
30
- );
31
-
32
- const testPrimitives: CorePrimitives = {
33
- Button: noop,
34
- Banner: passChildren,
35
- Field: passChildren,
36
- Input: noop,
37
- DataTable: noop,
38
- Form: passChildren,
39
- Section: passChildren,
40
- Card: passChildren,
41
- Grid: passChildren,
42
- GridCell: passChildren,
43
- Text: TestText,
44
- Heading: noop,
45
- Dialog: noop,
46
- Modal: noop,
47
- Lightbox: noop,
48
- ConfigSourceBadge: noop,
49
- ConfigCascadeView: noop,
50
- Link: noop,
51
- };
52
-
53
- function stubDispatcher(): Dispatcher {
54
- return {
55
- write: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["write"],
56
- query: (async (type: string) => {
57
- if (type === "sessions:query:session:detail") {
58
- return {
59
- isSuccess: true,
60
- data: { id: "sess-1", userId: "u-1", actedBy: SYSTEM_USER_ID, ip: "10.0.0.1" },
61
- };
62
- }
63
- // user:user resolves through the tenant-scoped member directory, not the
64
- // SystemAdmin-only user:query:user:list a TenantAdmin can't call (fw#3107).
65
- if (type === "tenant:query:member-directory") {
66
- return {
67
- isSuccess: true,
68
- data: { rows: [{ id: "u-1", label: "Jane Doe" }], nextCursor: null },
69
- };
70
- }
71
- return { isSuccess: true, data: { rows: [], nextCursor: null } };
72
- }) as unknown as Dispatcher["query"],
73
- batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
74
- statusStore: {
75
- getState: () => "online",
76
- subscribe: () => () => {},
77
- } as unknown as Dispatcher["statusStore"],
78
- async *stream() {},
79
- pendingWrites: () => [],
80
- pendingFiles: () => [],
81
- };
82
- }
83
-
84
- const detailScreen: ProjectionDetailScreenDefinition = {
85
- id: "session-detail",
86
- type: "projectionDetail",
87
- query: "sessions:query:session:detail",
88
- layout: {
89
- sections: [
90
- {
91
- fields: [
92
- { field: "userId", refEntity: "user:user", refLabelField: "displayName" },
93
- // Mirrors the audit-log detail actor field (fw#3103) — a system
94
- // write has no read_users row for the bulk lookup to find.
95
- { field: "actedBy", refEntity: "user:user", refLabelField: "displayName" },
96
- "ip",
97
- ],
98
- },
99
- ],
100
- },
101
- };
102
-
103
- function buildSchema(): FeatureSchema {
104
- return {
105
- featureName: "sessions",
106
- entities: {},
107
- screens: [detailScreen],
108
- } as FeatureSchema;
109
- }
110
-
111
- const staticNav: NavApi = {
112
- route: { screenId: "sessions:screen:session-detail" },
113
- navigate: () => {},
114
- replace: () => {},
115
- hrefFor: () => "",
116
- searchParams: {},
117
- setSearchParams: () => {},
118
- };
119
-
120
- function renderDetailScreen(): ReturnType<typeof render> {
121
- return render(
122
- <LocaleProvider
123
- resolver={createStaticLocaleResolver({ locale: "en" })}
124
- fallbackBundles={[kumikoDefaultTranslations]}
125
- >
126
- <DispatcherProvider dispatcher={stubDispatcher()}>
127
- <NavProvider value={staticNav}>
128
- <PrimitivesProvider value={testPrimitives}>
129
- <KumikoScreen
130
- schema={buildSchema()}
131
- qn="sessions:screen:session-detail"
132
- entityId="sess-1"
133
- />
134
- </PrimitivesProvider>
135
- </NavProvider>
136
- </DispatcherProvider>
137
- </LocaleProvider>,
138
- );
139
- }
140
-
141
- describe("projectionDetail reference field resolves labels (fw#2662)", () => {
142
- test("a field with refEntity metadata shows the resolved display name instead of the raw id", async () => {
143
- const { getByTestId } = renderDetailScreen();
144
-
145
- await waitFor(() => {
146
- expect(getByTestId("field-value-userId").textContent).toBe("Jane Doe");
147
- });
148
- });
149
-
150
- test("SYSTEM_USER_ID resolves to the system label instead of the raw id (fw#3103)", async () => {
151
- const { getByTestId } = renderDetailScreen();
152
-
153
- await waitFor(() => {
154
- expect(getByTestId("field-value-actedBy").textContent).toBe("System");
155
- });
156
- });
157
-
158
- test("non-regression: a field without refEntity metadata renders its raw value unchanged", async () => {
159
- const { getByTestId } = renderDetailScreen();
160
-
161
- await waitFor(() => {
162
- expect(getByTestId("field-value-ip").textContent).toBe("10.0.0.1");
163
- });
164
- });
165
- });
@@ -1,188 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type { ProjectionDetailScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
3
- import {
4
- synthesizeProjectionDetailEntity,
5
- synthesizeProjectionDetailScreen,
6
- } from "../projection-detail-shim";
7
-
8
- // Regression for the writeForm extension (fw editable-detail-screens): the
9
- // hard readOnly:true enforcement in synthesizeProjectionDetailScreen must
10
- // still apply to every ordinary fields-section, unchanged — a writeForm
11
- // section is the one deliberate exception (isFieldsEditSection excludes it,
12
- // same as relatedList/extension), it must pass through editable.
13
- describe("synthesizeProjectionDetailScreen", () => {
14
- test("forces readOnly:true on a fields section, but leaves a writeForm section's own readOnly untouched", () => {
15
- const screen: ProjectionDetailScreenDefinition = {
16
- id: "order-detail",
17
- type: "projectionDetail",
18
- query: "orders:query:order:detail",
19
- layout: {
20
- sections: [
21
- { title: "Basics", fields: [{ field: "name", readOnly: false }] },
22
- {
23
- kind: "writeForm",
24
- title: "Add note",
25
- fieldDefs: { note: { type: "text" } },
26
- fields: [{ field: "note", readOnly: false }],
27
- handler: "orders:write:add-note",
28
- },
29
- ],
30
- },
31
- };
32
-
33
- const result = synthesizeProjectionDetailScreen(screen);
34
-
35
- const [fieldsSection, writeFormSection] = result.layout.sections;
36
- if (fieldsSection === undefined || !("fields" in fieldsSection) || "kind" in fieldsSection) {
37
- throw new Error("expected the first section to stay a plain fields section");
38
- }
39
- expect(fieldsSection.fields[0]).toMatchObject({ field: "name", readOnly: true });
40
-
41
- if (writeFormSection === undefined || writeFormSection.kind !== "writeForm") {
42
- throw new Error("expected the second section to stay kind: writeForm, untouched");
43
- }
44
- expect(writeFormSection.fields[0]).toEqual({ field: "note", readOnly: false });
45
- });
46
-
47
- test("carries description through so RenderEdit can render it as the form subtitle (fw#2723)", () => {
48
- const withDescription = synthesizeProjectionDetailScreen({
49
- id: "order-detail",
50
- type: "projectionDetail",
51
- description: "Read-only view of a placed order.",
52
- query: "orders:query:order:detail",
53
- layout: { sections: [{ title: "Basics", fields: ["name"] }] },
54
- });
55
- expect(withDescription.description).toBe("Read-only view of a placed order.");
56
-
57
- const withoutDescription = synthesizeProjectionDetailScreen({
58
- id: "order-detail",
59
- type: "projectionDetail",
60
- query: "orders:query:order:detail",
61
- layout: { sections: [{ title: "Basics", fields: ["name"] }] },
62
- });
63
- expect("description" in withoutDescription).toBe(false);
64
- });
65
- });
66
-
67
- // Regression for fw#2982: a section that puts its fields in `groups`
68
- // instead of `fields` used to vanish entirely — synthesizeProjectionDetailEntity
69
- // never registered the field names, so computeEditViewModel threw "references
70
- // unknown field" for every one of them, and synthesizeProjectionDetailScreen
71
- // let a group field's own readOnly value pass through unforced.
72
- describe("groups-sections (fw#2982)", () => {
73
- const groupsOnlyScreen: ProjectionDetailScreenDefinition = {
74
- id: "contact-detail",
75
- type: "projectionDetail",
76
- query: "contacts:query:contact:detail",
77
- layout: {
78
- sections: [
79
- {
80
- title: "Stammdaten",
81
- fields: [],
82
- groups: [
83
- {
84
- title: "Kontakt",
85
- fields: [{ field: "contactKind", readOnly: false }, "email"],
86
- },
87
- ],
88
- },
89
- ],
90
- },
91
- };
92
-
93
- test("synthesizeProjectionDetailEntity registers every field declared only through groups", () => {
94
- const entity = synthesizeProjectionDetailEntity(groupsOnlyScreen.layout);
95
- expect(entity.fields["contactKind"]).toEqual({ type: "text" });
96
- expect(entity.fields["email"]).toEqual({ type: "text" });
97
- });
98
-
99
- test("synthesizeProjectionDetailScreen forces readOnly:true on a group field even when the author set readOnly:false", () => {
100
- const result = synthesizeProjectionDetailScreen(groupsOnlyScreen);
101
- const [section] = result.layout.sections;
102
- if (section === undefined || !("fields" in section) || "kind" in section) {
103
- throw new Error("expected the section to stay a plain fields section");
104
- }
105
- if (!("groups" in section) || section.groups === undefined) {
106
- throw new Error("expected the section to keep its groups");
107
- }
108
- expect(section.groups[0]?.fields).toEqual([
109
- { field: "contactKind", readOnly: true },
110
- { field: "email", readOnly: true },
111
- ]);
112
- });
113
-
114
- test("a screen mixing a plain fields section and a groups section resolves both, for both functions", () => {
115
- const mixedScreen: ProjectionDetailScreenDefinition = {
116
- id: "deposit-detail",
117
- type: "projectionDetail",
118
- query: "deposits:query:deposit:detail",
119
- layout: {
120
- sections: [
121
- { title: "Basics", fields: [{ field: "amount", readOnly: false }] },
122
- {
123
- title: "Parties",
124
- fields: [],
125
- groups: [{ title: "Tenant", fields: ["tenantName"] }],
126
- },
127
- ],
128
- },
129
- };
130
-
131
- const entity = synthesizeProjectionDetailEntity(mixedScreen.layout);
132
- expect(entity.fields["amount"]).toEqual({ type: "text" });
133
- expect(entity.fields["tenantName"]).toEqual({ type: "text" });
134
-
135
- const result = synthesizeProjectionDetailScreen(mixedScreen);
136
- const [fieldsSection, groupsSection] = result.layout.sections;
137
- if (fieldsSection === undefined || !("fields" in fieldsSection) || "kind" in fieldsSection) {
138
- throw new Error("expected the first section to stay a plain fields section");
139
- }
140
- expect(fieldsSection.fields[0]).toMatchObject({ field: "amount", readOnly: true });
141
-
142
- if (
143
- groupsSection === undefined ||
144
- !("groups" in groupsSection) ||
145
- groupsSection.groups === undefined
146
- ) {
147
- throw new Error("expected the second section to keep its groups");
148
- }
149
- expect(groupsSection.groups[0]?.fields).toEqual([{ field: "tenantName", readOnly: true }]);
150
- });
151
-
152
- // The boot-validator rejects fields+groups both non-empty on one section at
153
- // boot time, so an author can never actually ship this shape — but the shim
154
- // is not itself a validation pass, and this pins the union, not the
155
- // either/or a naive `groups !== undefined ? grouped : fields` would take.
156
- test("a single section carrying both fields and groups resolves both, for both functions", () => {
157
- const bothScreen: ProjectionDetailScreenDefinition = {
158
- id: "invoice-detail",
159
- type: "projectionDetail",
160
- query: "invoices:query:invoice:detail",
161
- layout: {
162
- sections: [
163
- {
164
- title: "Mixed",
165
- fields: [{ field: "amount", readOnly: false }],
166
- groups: [{ title: "Tenant", fields: ["tenantName"] }],
167
- },
168
- ],
169
- },
170
- };
171
-
172
- const entity = synthesizeProjectionDetailEntity(bothScreen.layout);
173
- expect(entity.fields["amount"]).toEqual({ type: "text" });
174
- expect(entity.fields["tenantName"]).toEqual({ type: "text" });
175
-
176
- const result = synthesizeProjectionDetailScreen(bothScreen);
177
- const [section] = result.layout.sections;
178
- if (section === undefined || !("fields" in section) || "kind" in section) {
179
- throw new Error("expected the section to stay a plain fields section");
180
- }
181
- expect(section.fields[0]).toMatchObject({ field: "amount", readOnly: true });
182
-
183
- if (!("groups" in section) || section.groups === undefined) {
184
- throw new Error("expected the section to keep its groups");
185
- }
186
- expect(section.groups[0]?.fields).toEqual([{ field: "tenantName", readOnly: true }]);
187
- });
188
- });
@@ -1,191 +0,0 @@
1
- // fw#2312: `singleton: true` marks a projectionDetail screen whose query
2
- // determines the shown row from the caller's session/context instead of a
3
- // row id in the path (a self-service "my profile"/"my data" screen has no
4
- // row to link to). Before this flag, ProjectionDetailBody always rendered
5
- // the "needs a row id in the path" error banner when `entityId` was
6
- // undefined (kumiko-screen.tsx) — both user-profile and user-data-rights'
7
- // privacy-center screen hit exactly that, unrenderable, until the flag
8
- // existed (fw#2312 bug report). This proves the fix, the no-flag regression
9
- // case stays covered, and that a stray/spoofed path id never reaches the
10
- // query under singleton (renderer-side half of the security requirement —
11
- // the boot-validator half lives in framework's projection-detail-
12
- // singleton.test.ts).
13
-
14
- import { describe, expect, test } from "bun:test";
15
- import type { ProjectionDetailScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
16
- import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
17
- import { render, waitFor } from "@testing-library/react";
18
- import type { ComponentType, ReactNode } from "react";
19
- import { DispatcherProvider } from "../../context/dispatcher-context";
20
- import { UserRolesProvider } from "../../context/user-roles-context";
21
- import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
22
- import { kumikoDefaultTranslations } from "../../i18n-defaults";
23
- import {
24
- type BannerProps,
25
- type CorePrimitives,
26
- type FormProps,
27
- PrimitivesProvider,
28
- } from "../../primitives";
29
- import { AppFeaturesProvider } from "../app-features-context";
30
- import type { FeatureSchema } from "../feature-schema";
31
- import { KumikoScreen } from "../kumiko-screen";
32
- import type { NavApi } from "../nav";
33
- import { NavProvider } from "../nav";
34
-
35
- const noop = (): ReactNode => null;
36
- const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
37
-
38
- const FormWithActions: ComponentType<FormProps> = ({ children }) => (
39
- <div data-testid="form-body">{children}</div>
40
- );
41
-
42
- const TestBanner: ComponentType<BannerProps> = ({ children, testId }) => (
43
- <div data-testid={testId}>{children}</div>
44
- );
45
-
46
- const testPrimitives: CorePrimitives = {
47
- Button: noop,
48
- Banner: TestBanner,
49
- Field: passChildren,
50
- Input: noop,
51
- DataTable: noop,
52
- Form: FormWithActions,
53
- Section: passChildren,
54
- Card: passChildren,
55
- Grid: passChildren,
56
- GridCell: passChildren,
57
- Text: passChildren,
58
- Heading: noop,
59
- Dialog: noop,
60
- Modal: noop,
61
- Lightbox: noop,
62
- ConfigSourceBadge: noop,
63
- ConfigCascadeView: noop,
64
- Link: noop,
65
- };
66
-
67
- function stubDispatcher(
68
- record: Readonly<Record<string, unknown>> | null,
69
- queries: Array<{ type: string; payload: unknown }>,
70
- ): Dispatcher {
71
- return {
72
- write: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["write"],
73
- query: (async (type: string, payload: unknown) => {
74
- queries.push({ type, payload });
75
- return { isSuccess: true, data: record };
76
- }) as unknown as Dispatcher["query"],
77
- batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
78
- statusStore: {
79
- getState: () => "online",
80
- subscribe: () => () => {},
81
- } as unknown as Dispatcher["statusStore"],
82
- async *stream() {},
83
- pendingWrites: () => [],
84
- pendingFiles: () => [],
85
- };
86
- }
87
-
88
- function meScreen(
89
- overrides?: Partial<ProjectionDetailScreenDefinition>,
90
- ): FeatureSchema["screens"][number] {
91
- return {
92
- id: "profile",
93
- type: "projectionDetail",
94
- query: "app:query:me",
95
- layout: { sections: [{ title: "s", fields: ["name"] }] },
96
- ...overrides,
97
- } as FeatureSchema["screens"][number];
98
- }
99
-
100
- function renderScreen(opts: {
101
- readonly screen: FeatureSchema["screens"][number];
102
- readonly entityId?: string;
103
- readonly queries: Array<{ type: string; payload: unknown }>;
104
- readonly record?: Readonly<Record<string, unknown>> | null;
105
- }): ReturnType<typeof render> {
106
- const schema: FeatureSchema = { featureName: "app", entities: {}, screens: [opts.screen] };
107
- const navApi: NavApi = {
108
- route: { screenId: "app:screen:profile" },
109
- navigate: () => {},
110
- replace: () => {},
111
- hrefFor: () => "",
112
- searchParams: {},
113
- setSearchParams: () => {},
114
- };
115
- return render(
116
- <LocaleProvider
117
- resolver={createStaticLocaleResolver({ locale: "de-DE" })}
118
- fallbackBundles={[kumikoDefaultTranslations]}
119
- >
120
- <DispatcherProvider
121
- dispatcher={stubDispatcher(
122
- opts.record !== undefined ? opts.record : { id: "u1", name: "Ada" },
123
- opts.queries,
124
- )}
125
- >
126
- <AppFeaturesProvider features={[schema]}>
127
- <UserRolesProvider roles={[]}>
128
- <NavProvider value={navApi}>
129
- <PrimitivesProvider value={testPrimitives}>
130
- <KumikoScreen
131
- schema={schema}
132
- qn="app:screen:profile"
133
- {...(opts.entityId !== undefined && { entityId: opts.entityId })}
134
- />
135
- </PrimitivesProvider>
136
- </NavProvider>
137
- </UserRolesProvider>
138
- </AppFeaturesProvider>
139
- </DispatcherProvider>
140
- </LocaleProvider>,
141
- );
142
- }
143
-
144
- describe("projectionDetail singleton (fw#2312)", () => {
145
- test("singleton + no path id renders the record instead of the missing-id banner", async () => {
146
- const queries: Array<{ type: string; payload: unknown }> = [];
147
- const { queryByTestId, getByTestId } = renderScreen({
148
- screen: meScreen({ singleton: true }),
149
- queries,
150
- });
151
-
152
- await waitFor(() => expect(getByTestId("form-body")).toBeTruthy());
153
- expect(queryByTestId("kumiko-screen-projection-detail-missing-id")).toBeNull();
154
- expect(queries).toEqual([{ type: "app:query:me", payload: {} }]);
155
- });
156
-
157
- test("no singleton flag + no path id still shows the missing-id banner (no regression)", async () => {
158
- const queries: Array<{ type: string; payload: unknown }> = [];
159
- const { getByTestId } = renderScreen({ screen: meScreen(), queries });
160
-
161
- await waitFor(() =>
162
- expect(getByTestId("kumiko-screen-projection-detail-missing-id")).toBeTruthy(),
163
- );
164
- });
165
-
166
- test("singleton ignores a stray path id — the query never receives it", async () => {
167
- const queries: Array<{ type: string; payload: unknown }> = [];
168
- const { getByTestId } = renderScreen({
169
- screen: meScreen({ singleton: true }),
170
- entityId: "attacker-controlled-id",
171
- queries,
172
- });
173
-
174
- await waitFor(() => expect(getByTestId("form-body")).toBeTruthy());
175
- expect(queries).toEqual([{ type: "app:query:me", payload: {} }]);
176
- });
177
-
178
- test("singleton + record-not-found banner never shows the (ignored) path id", async () => {
179
- const queries: Array<{ type: string; payload: unknown }> = [];
180
- const { getByTestId } = renderScreen({
181
- screen: meScreen({ singleton: true }),
182
- entityId: "99",
183
- record: null,
184
- queries,
185
- });
186
-
187
- const banner = await waitFor(() => getByTestId("kumiko-screen-record-missing"));
188
- expect(banner.textContent).toBe("Record not found.");
189
- expect(banner.textContent).not.toContain("99");
190
- });
191
- });