@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,239 +0,0 @@
1
- // Finding: render-field-unsupported-types.test.tsx only proves jsonb/embedded
2
- // (no embeddedListCells) fields render read-only (Banner, no Input wired up).
3
- // It never proves the actual claim that matters: saving the form after
4
- // editing an UNRELATED field does not corrupt or overwrite those fields'
5
- // real data. Because RenderField never wires an onChange for them, their
6
- // value in the form controller stays reference-identical to the record it
7
- // was seeded from — the update payload's `changes` diff (reference-equality
8
- // based, see form-controller.ts valuesDiff) must therefore omit them
9
- // entirely, never re-include them as e.g. a stringified copy.
10
- //
11
- // This renders the real update path (KumikoScreen → EntityEditScreen →
12
- // EntityEditUpdateBody → RenderEdit → RenderField) under a stub dispatcher
13
- // and inspects the actual write() payload, not just the render tree.
14
-
15
- import { describe, expect, test } from "bun:test";
16
- import type {
17
- EntityDefinition,
18
- EntityEditScreenDefinition,
19
- } from "@cosmicdrift/kumiko-framework/ui-types";
20
- import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
21
- import { act, render, waitFor } from "@testing-library/react";
22
- import type { ComponentType, FormEvent, ReactNode } from "react";
23
- import { DispatcherProvider } from "../../context/dispatcher-context";
24
- import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
25
- import { kumikoDefaultTranslations } from "../../i18n-defaults";
26
- import {
27
- type CorePrimitives,
28
- type FormProps,
29
- type InputProps,
30
- PrimitivesProvider,
31
- } from "../../primitives";
32
- import type { FeatureSchema } from "../feature-schema";
33
- import { KumikoScreen } from "../kumiko-screen";
34
- import { NavProvider } from "../nav";
35
-
36
- const capturedInputs: Record<string, InputProps | undefined> = {};
37
- const captureInput: ComponentType<InputProps> = (props) => {
38
- capturedInputs[props.name] = props;
39
- return null;
40
- };
41
- let capturedFormSubmit: ((e?: FormEvent) => void) | undefined;
42
- const captureForm: ComponentType<FormProps> = (props) => {
43
- capturedFormSubmit = props.onSubmit;
44
- return <>{props.children}</>;
45
- };
46
- const noop = (): ReactNode => null;
47
- const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
48
-
49
- const testPrimitives: CorePrimitives = {
50
- Button: noop,
51
- Banner: passChildren,
52
- Field: passChildren,
53
- Input: captureInput,
54
- DataTable: noop,
55
- Form: captureForm,
56
- Section: passChildren,
57
- Card: passChildren,
58
- Grid: passChildren,
59
- GridCell: passChildren,
60
- Text: passChildren,
61
- Heading: noop,
62
- Dialog: noop,
63
- Modal: noop,
64
- Lightbox: noop,
65
- ConfigSourceBadge: noop,
66
- ConfigCascadeView: noop,
67
- Link: noop,
68
- };
69
-
70
- let lastWrite: { type: string; payload: unknown } | undefined;
71
-
72
- // The annotation resets TS's flow-narrowing, which only tracks the
73
- // straight-line `lastWrite = undefined` reset, not the reassignment inside the dispatcher closure.
74
- function requireLastWrite(): { type: string; payload: unknown } {
75
- const write: { type: string; payload: unknown } | undefined = lastWrite;
76
- if (!write) throw new Error("expected a write() call");
77
- return write;
78
- }
79
-
80
- function stubDispatcher(record: Readonly<Record<string, unknown>>): Dispatcher {
81
- return {
82
- write: (async (type: string, payload: unknown) => {
83
- lastWrite = { type, payload };
84
- return { isSuccess: true, data: {} };
85
- }) as unknown as Dispatcher["write"],
86
- query: (async () => ({ isSuccess: true, data: record })) as unknown as Dispatcher["query"],
87
- batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
88
- statusStore: {
89
- getState: () => "online",
90
- subscribe: () => () => {},
91
- } as unknown as Dispatcher["statusStore"],
92
- async *stream() {},
93
- pendingWrites: () => [],
94
- pendingFiles: () => [],
95
- };
96
- }
97
-
98
- function buildSchema(): FeatureSchema {
99
- const entity: EntityDefinition = {
100
- fields: {
101
- name: { type: "text", maxLength: 200, required: false, searchable: false, sortable: false },
102
- extra: { type: "jsonb" },
103
- meta: {
104
- type: "embedded",
105
- schema: { note: { type: "text" } },
106
- },
107
- },
108
- };
109
- const screen: EntityEditScreenDefinition = {
110
- id: "widget-edit",
111
- type: "entityEdit",
112
- entity: "widget",
113
- layout: { sections: [{ columns: 1, fields: ["name", "extra", "meta"] }] },
114
- };
115
- return {
116
- featureName: "widgets",
117
- entities: { widget: entity },
118
- screens: [screen],
119
- } as FeatureSchema;
120
- }
121
-
122
- describe("EntityEditUpdateForm — untouched jsonb/embedded fields survive submit", () => {
123
- test("changing an unrelated field and submitting keeps jsonb/embedded fields out of the diff (not stringified, not overwritten)", async () => {
124
- lastWrite = undefined;
125
- capturedFormSubmit = undefined;
126
- for (const key of Object.keys(capturedInputs)) delete capturedInputs[key];
127
-
128
- const record = {
129
- id: "w1",
130
- version: 3,
131
- name: "Old name",
132
- extra: { a: 1 },
133
- meta: { note: "keep me" },
134
- };
135
-
136
- render(
137
- <LocaleProvider
138
- resolver={createStaticLocaleResolver({ locale: "de-DE" })}
139
- fallbackBundles={[kumikoDefaultTranslations]}
140
- >
141
- <DispatcherProvider dispatcher={stubDispatcher(record)}>
142
- <NavProvider
143
- value={{
144
- route: { screenId: "widgets:widget-edit" },
145
- navigate: () => {},
146
- replace: () => {},
147
- hrefFor: () => "",
148
- searchParams: {},
149
- setSearchParams: () => {},
150
- }}
151
- >
152
- <PrimitivesProvider value={testPrimitives}>
153
- <KumikoScreen schema={buildSchema()} qn="widgets:screen:widget-edit" entityId="w1" />
154
- </PrimitivesProvider>
155
- </NavProvider>
156
- </DispatcherProvider>
157
- </LocaleProvider>,
158
- );
159
-
160
- await waitFor(() => {
161
- expect(capturedInputs["name"]).toBeDefined();
162
- });
163
-
164
- const nameInput = capturedInputs["name"];
165
- if (nameInput?.kind !== "text") throw new Error("expected a text input for the name field");
166
- act(() => nameInput.onChange("New name"));
167
-
168
- await waitFor(() => {
169
- expect(capturedFormSubmit).toBeDefined();
170
- });
171
- act(() => capturedFormSubmit?.());
172
-
173
- await waitFor(() => {
174
- expect(lastWrite).toBeDefined();
175
- });
176
-
177
- const payload = requireLastWrite().payload as { changes: Record<string, unknown> };
178
- expect(payload.changes["name"]).toBe("New name");
179
- // The untouched jsonb/embedded fields must never resurface in the diff —
180
- // if they did (the pre-fix bug this guards against), it would mean the
181
- // update overwrites them, and the old code path could send a corrupted
182
- // (e.g. stringified) copy instead of the real object.
183
- expect(Object.hasOwn(payload.changes, "extra")).toBe(false);
184
- expect(Object.hasOwn(payload.changes, "meta")).toBe(false);
185
- });
186
-
187
- test("touching the jsonb field itself keeps it a real object in the diff, never a string", async () => {
188
- lastWrite = undefined;
189
- capturedFormSubmit = undefined;
190
- for (const key of Object.keys(capturedInputs)) delete capturedInputs[key];
191
-
192
- const record = {
193
- id: "w1",
194
- version: 3,
195
- name: "Old name",
196
- extra: { a: 1 },
197
- meta: { note: "keep me" },
198
- };
199
-
200
- render(
201
- <LocaleProvider
202
- resolver={createStaticLocaleResolver({ locale: "de-DE" })}
203
- fallbackBundles={[kumikoDefaultTranslations]}
204
- >
205
- <DispatcherProvider dispatcher={stubDispatcher(record)}>
206
- <NavProvider
207
- value={{
208
- route: { screenId: "widgets:widget-edit" },
209
- navigate: () => {},
210
- replace: () => {},
211
- hrefFor: () => "",
212
- searchParams: {},
213
- setSearchParams: () => {},
214
- }}
215
- >
216
- <PrimitivesProvider value={testPrimitives}>
217
- <KumikoScreen schema={buildSchema()} qn="widgets:screen:widget-edit" entityId="w1" />
218
- </PrimitivesProvider>
219
- </NavProvider>
220
- </DispatcherProvider>
221
- </LocaleProvider>,
222
- );
223
-
224
- await waitFor(() => {
225
- expect(capturedFormSubmit).toBeDefined();
226
- });
227
- act(() => capturedFormSubmit?.());
228
-
229
- await waitFor(() => {
230
- expect(lastWrite).toBeDefined();
231
- });
232
-
233
- const payload = requireLastWrite().payload as { changes: Record<string, unknown> };
234
- // Nothing was touched at all — the diff must be empty, and specifically
235
- // must not contain a stringified copy of extra/meta.
236
- expect(Object.hasOwn(payload.changes, "extra")).toBe(false);
237
- expect(Object.hasOwn(payload.changes, "meta")).toBe(false);
238
- });
239
- });
@@ -1,128 +0,0 @@
1
- // Guard-evasion-adjacent bug: useNavigateToCreateFor picked the first
2
- // entityEdit screen with allowCreate !== false as the "+ Neu" target,
3
- // without excluding singleton:true screens. A singleton screen opened
4
- // without an entityId resolves the existing record (EntityEditSingletonBody)
5
- // instead of a blank create form — so "+ Neu" silently opened a prefilled
6
- // update form. This test renders the real list path (KumikoScreen →
7
- // EntityListScreen → EntityListBody → RenderList) for an entity whose only
8
- // registered entityEdit screen is a singleton, and asserts no create button
9
- // renders — not just the hook in isolation.
10
- import { describe, expect, test } from "bun:test";
11
- import type {
12
- EntityDefinition,
13
- EntityEditScreenDefinition,
14
- EntityListScreenDefinition,
15
- } from "@cosmicdrift/kumiko-framework/ui-types";
16
- import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
17
- import { render } from "@testing-library/react";
18
- import type { ComponentType, ReactNode } from "react";
19
- import { DispatcherProvider } from "../../context/dispatcher-context";
20
- import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
21
- import { type ButtonProps, type CorePrimitives, PrimitivesProvider } from "../../primitives";
22
- import type { FeatureSchema } from "../feature-schema";
23
- import { KumikoScreen } from "../kumiko-screen";
24
- import { NavProvider } from "../nav";
25
-
26
- const capturedButtonTestIds: string[] = [];
27
- const captureButton: ComponentType<ButtonProps> = (props) => {
28
- if (props.testId !== undefined) capturedButtonTestIds.push(props.testId);
29
- return null;
30
- };
31
- const noop = (): ReactNode => null;
32
- const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
33
-
34
- const testPrimitives: CorePrimitives = {
35
- Button: captureButton,
36
- Banner: passChildren,
37
- Field: passChildren,
38
- Input: noop,
39
- DataTable: noop,
40
- Form: passChildren,
41
- Section: passChildren,
42
- Card: passChildren,
43
- Grid: passChildren,
44
- GridCell: passChildren,
45
- Text: passChildren,
46
- Heading: noop,
47
- Dialog: noop,
48
- Modal: noop,
49
- Lightbox: noop,
50
- ConfigSourceBadge: noop,
51
- ConfigCascadeView: noop,
52
- Link: noop,
53
- };
54
-
55
- function stubDispatcher(): Dispatcher {
56
- return {
57
- write: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["write"],
58
- query: (async () => ({
59
- isSuccess: true,
60
- data: { rows: [], total: 0 },
61
- })) as unknown as Dispatcher["query"],
62
- batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
63
- statusStore: {
64
- getState: () => "online",
65
- subscribe: () => () => {},
66
- } as unknown as Dispatcher["statusStore"],
67
- async *stream() {},
68
- pendingWrites: () => [],
69
- pendingFiles: () => [],
70
- };
71
- }
72
-
73
- function buildSchema(): FeatureSchema {
74
- const entity: EntityDefinition = {
75
- fields: {
76
- name: { type: "text", maxLength: 200, required: false, searchable: false, sortable: false },
77
- },
78
- };
79
- const listScreen: EntityListScreenDefinition = {
80
- id: "org-list",
81
- type: "entityList",
82
- entity: "org",
83
- columns: ["name"],
84
- };
85
- // The only entityEdit screen for "org" is a singleton — there is
86
- // deliberately no separate non-singleton entityEdit screen.
87
- const editScreen: EntityEditScreenDefinition = {
88
- id: "org-edit",
89
- type: "entityEdit",
90
- entity: "org",
91
- singleton: true,
92
- layout: { sections: [{ columns: 1, fields: ["name"] }] },
93
- };
94
- return {
95
- featureName: "orgs",
96
- entities: { org: entity },
97
- screens: [listScreen, editScreen],
98
- } as FeatureSchema;
99
- }
100
-
101
- describe("entityList '+ Neu' target excludes singleton entityEdit screens", () => {
102
- test("no create button renders when the only entityEdit screen is singleton:true", () => {
103
- capturedButtonTestIds.length = 0;
104
- render(
105
- <LocaleProvider resolver={createStaticLocaleResolver({ locale: "de-DE" })}>
106
- <DispatcherProvider dispatcher={stubDispatcher()}>
107
- <NavProvider
108
- value={{
109
- route: { screenId: "orgs:org-list" },
110
- navigate: () => {},
111
- replace: () => {},
112
- hrefFor: () => "",
113
- searchParams: {},
114
- setSearchParams: () => {},
115
- }}
116
- >
117
- <PrimitivesProvider value={testPrimitives}>
118
- <KumikoScreen schema={buildSchema()} qn="orgs:screen:org-list" />
119
- </PrimitivesProvider>
120
- </NavProvider>
121
- </DispatcherProvider>
122
- </LocaleProvider>,
123
- );
124
-
125
- expect(capturedButtonTestIds).not.toContain("render-list-create");
126
- expect(capturedButtonTestIds).not.toContain("render-list-empty-create");
127
- });
128
- });