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