@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,354 +0,0 @@
1
- import type {
2
- EditFieldViewModel,
3
- EmbeddedListCellViewModel,
4
- FieldIssue,
5
- } from "@cosmicdrift/kumiko-headless";
6
- import {
7
- computeDerivedCellValue,
8
- currencyDecimals,
9
- groupEmbeddedListIssues,
10
- roundDerivedCellValue,
11
- sumEmbeddedListColumn,
12
- } from "@cosmicdrift/kumiko-headless";
13
- import { type ReactNode, useState } from "react";
14
- import { REFERENCE_COMBOBOX_LIMIT } from "../hooks/reference-limits";
15
- import { useQuery } from "../hooks/use-query";
16
- import { referenceLookupSource } from "../hooks/use-reference-lookup";
17
- import { useTranslation } from "../i18n";
18
- import type { EmbeddedListColumn, EmbeddedListTotal } from "../primitives";
19
- import { usePrimitives } from "../primitives";
20
-
21
- export type EmbeddedListFieldProps = {
22
- readonly field: EditFieldViewModel;
23
- readonly id: string;
24
- readonly onChange: (value: unknown) => void;
25
- readonly allIssues: Readonly<Record<string, readonly FieldIssue[]>>;
26
- readonly featureName: string;
27
- };
28
-
29
- type EmbeddedRow = Readonly<Record<string, unknown>>;
30
-
31
- function withRecomputedDerived(
32
- row: EmbeddedRow,
33
- derived: EditFieldViewModel["embeddedListDerived"],
34
- cells: readonly EmbeddedListCellViewModel[],
35
- ): EmbeddedRow {
36
- if (derived === undefined) return row;
37
- // Read sources from an unchanged snapshot, never from `result` (which this
38
- // loop mutates) — mirrors withDerivedCells server-side so chained derived
39
- // cells (e.g. net=qty*price, then gross=sum(net,tax)) can't drift between
40
- // client preview and server depending on Object.entries(derived) order.
41
- const source = { ...row };
42
- const result: Record<string, unknown> = { ...row };
43
- for (const [derivedField, def] of Object.entries(derived)) {
44
- const values = def.from.map((src) => {
45
- const v = source[src];
46
- return typeof v === "number" ? v : undefined;
47
- });
48
- const computed = computeDerivedCellValue(def.op, values);
49
- const target = cells.find((c) => c.field === derivedField);
50
- result[derivedField] =
51
- computed !== undefined && target !== undefined
52
- ? roundDerivedCellValue(computed, target)
53
- : computed;
54
- }
55
- return result;
56
- }
57
-
58
- // Locale-aware decimal parse for pasted text: strips space/NBSP thousands
59
- // separators outright, then treats whichever of "," or "." appears LAST in
60
- // the string as the decimal separator and strips the other one — handles
61
- // both "1.234,56" (de) and "1,234.56" (en-US) pastes. Returns undefined for
62
- // anything that still doesn't parse as a finite number.
63
- function parseLocaleAwareDecimal(raw: string): number | undefined {
64
- const stripped = raw.replace(/[\s ]/g, "");
65
- if (stripped === "") return undefined;
66
- const lastComma = stripped.lastIndexOf(",");
67
- const lastDot = stripped.lastIndexOf(".");
68
- const decimalIndex = Math.max(lastComma, lastDot);
69
- if (decimalIndex === -1) {
70
- const n = Number(stripped);
71
- return Number.isFinite(n) ? n : undefined;
72
- }
73
- const integerPart = stripped.slice(0, decimalIndex).replace(/[.,]/g, "");
74
- const fractionPart = stripped.slice(decimalIndex + 1);
75
- if (!/^\d*$/.test(integerPart) || !/^\d*$/.test(fractionPart)) return undefined;
76
- const n = Number(
77
- `${integerPart === "" ? "0" : integerPart}.${fractionPart === "" ? "0" : fractionPart}`,
78
- );
79
- return Number.isFinite(n) ? n : undefined;
80
- }
81
-
82
- function coerceCellValue(column: EmbeddedListColumn, text: string, currency: string): unknown {
83
- switch (column.type) {
84
- case "text":
85
- return text;
86
- case "number":
87
- case "decimal": {
88
- if (text.trim() === "") return undefined;
89
- const n = Number(text);
90
- return Number.isFinite(n) ? n : undefined;
91
- }
92
- case "money": {
93
- // Money cells are minor-unit integers (cents) in storage — paste
94
- // arrives as a major-unit decimal string ("12,99"/"12.99"), so scale
95
- // by the currency's decimal places. Must agree with the typed-in path
96
- // (renderCellControl's MoneyInput, which scales by the same
97
- // currencyDecimals) — a hardcoded ×100 here diverged for zero-/three-
98
- // decimal currencies (JPY, BHD, ...), landing a pasted value 100x off
99
- // from the same value typed by hand (kumiko-framework#1972).
100
- // parseLocaleAwareDecimal handles both "1.234,56" (de) and
101
- // "1,234.56" (en-US) thousands/decimal conventions — a naive
102
- // `replace(",", ".")` mangled DE-locale pastes with thousands
103
- // separators into NaN (kumiko-framework#1838).
104
- if (text.trim() === "") return undefined;
105
- const n = parseLocaleAwareDecimal(text);
106
- return n === undefined ? undefined : Math.round(n * 10 ** currencyDecimals(currency));
107
- }
108
- case "boolean":
109
- return ["true", "1", "yes", "y", "ja"].includes(text.trim().toLowerCase());
110
- case "date":
111
- case "timestamp":
112
- return text.trim();
113
- case "select": {
114
- const match = (column.options ?? []).find(
115
- (opt) => opt === text || column.optionLabels?.[opt] === text,
116
- );
117
- return match;
118
- }
119
- case "reference": {
120
- const match = (column.referenceOptions ?? []).find(
121
- (o) => o.label === text || o.value === text,
122
- );
123
- return match?.value;
124
- }
125
- default: {
126
- const exhaustiveCheck: never = column.type;
127
- return exhaustiveCheck;
128
- }
129
- }
130
- }
131
-
132
- export function EmbeddedListField({
133
- field,
134
- id,
135
- onChange,
136
- allIssues,
137
- featureName,
138
- }: EmbeddedListFieldProps): ReactNode {
139
- const { EmbeddedListInput } = usePrimitives();
140
- const t = useTranslation();
141
- const [pasteWarning, setPasteWarning] = useState<{
142
- readonly droppedRows: number;
143
- readonly unmatchedCells: number;
144
- } | null>(null);
145
-
146
- const cells = field.embeddedListCells ?? [];
147
- const rows = Array.isArray(field.value) ? (field.value as readonly EmbeddedRow[]) : [];
148
- const derived = field.embeddedListDerived;
149
-
150
- const referenceCells = cells.filter((c) => c.type === "reference");
151
- const referenceSources = referenceCells.map((cell) =>
152
- cell.refOptionsQuery !== undefined
153
- ? { queryQn: cell.refOptionsQuery, labelKey: "label" }
154
- : referenceLookupSource(
155
- cell.refFeature ?? featureName,
156
- cell.refEntity ?? "",
157
- cell.refLabelField ?? "id",
158
- ),
159
- );
160
- const referenceQueries = referenceSources.map((source) =>
161
- // biome-ignore lint/correctness/useHookAtTopLevel: referenceCells comes from the entity-schema definition — fixed for the screen's lifetime, not a real conditional-hook risk.
162
- useQuery<{ rows: ReadonlyArray<Record<string, unknown>> }>(source.queryQn, {
163
- limit: REFERENCE_COMBOBOX_LIMIT,
164
- }),
165
- );
166
-
167
- if (EmbeddedListInput === undefined) return null;
168
-
169
- const columns: EmbeddedListColumn[] = cells.map((cell) => {
170
- const isDerived = derived?.[cell.field] !== undefined;
171
- if (cell.type === "reference") {
172
- const idx = referenceCells.indexOf(cell);
173
- const query = referenceQueries[idx];
174
- const labelKey = referenceSources[idx]?.labelKey ?? "id";
175
- const referenceOptions = (query?.data?.rows ?? []).map((row) => ({
176
- value: String(row["id"] ?? ""),
177
- label: String(row[labelKey] ?? row["id"] ?? ""),
178
- }));
179
- return {
180
- field: cell.field,
181
- label: cell.label,
182
- type: cell.type,
183
- required: cell.required,
184
- derived: isDerived,
185
- referenceOptions,
186
- referenceLoading: query?.loading ?? false,
187
- };
188
- }
189
- return {
190
- field: cell.field,
191
- label: cell.label,
192
- type: cell.type,
193
- required: cell.required,
194
- derived: isDerived,
195
- ...(cell.options !== undefined && { options: cell.options }),
196
- ...(cell.optionLabels !== undefined && { optionLabels: cell.optionLabels }),
197
- };
198
- });
199
-
200
- const totals: EmbeddedListTotal[] = (field.embeddedListTotals ?? []).map((subFieldName) => {
201
- const cell = cells.find((c) => c.field === subFieldName);
202
- return {
203
- field: subFieldName,
204
- label: cell?.label ?? subFieldName,
205
- value: sumEmbeddedListColumn(rows, subFieldName),
206
- };
207
- });
208
-
209
- const { listIssues, rowIssues, cellIssues } = groupEmbeddedListIssues(allIssues, field.field);
210
- const combinedListIssues: readonly FieldIssue[] =
211
- pasteWarning === null
212
- ? listIssues
213
- : [
214
- ...listIssues,
215
- ...(pasteWarning.droppedRows > 0
216
- ? [
217
- {
218
- path: field.field,
219
- code: "paste-rows-truncated",
220
- i18nKey: "kumiko.field.embedded-list.paste-rows-truncated",
221
- params: { count: pasteWarning.droppedRows },
222
- },
223
- ]
224
- : []),
225
- ...(pasteWarning.unmatchedCells > 0
226
- ? [
227
- {
228
- path: field.field,
229
- code: "paste-cells-unmatched",
230
- i18nKey: "kumiko.field.embedded-list.paste-cells-unmatched",
231
- params: { count: pasteWarning.unmatchedCells },
232
- },
233
- ]
234
- : []),
235
- ];
236
-
237
- function replaceRow(rowIndex: number, updater: (row: EmbeddedRow) => EmbeddedRow): void {
238
- const nextRows = rows.map((row, i) => (i === rowIndex ? updater(row) : row));
239
- onChange(nextRows);
240
- }
241
-
242
- function handleCellChange(rowIndex: number, cellField: string, value: unknown): void {
243
- replaceRow(rowIndex, (row) =>
244
- withRecomputedDerived({ ...row, [cellField]: value }, derived, cells),
245
- );
246
- }
247
-
248
- function handleAddRow(): void {
249
- onChange([...rows, withRecomputedDerived({}, derived, cells)]);
250
- }
251
-
252
- function handleRemoveRow(rowIndex: number): void {
253
- onChange(rows.filter((_, i) => i !== rowIndex));
254
- }
255
-
256
- function handleDuplicateRow(rowIndex: number): void {
257
- const source = rows[rowIndex];
258
- if (source === undefined) return;
259
- const next = [...rows];
260
- next.splice(rowIndex + 1, 0, { ...source });
261
- onChange(next);
262
- }
263
-
264
- function handleMoveRow(fromIndex: number, toIndex: number): void {
265
- if (toIndex < 0 || toIndex >= rows.length) return;
266
- const next = [...rows];
267
- const [moved] = next.splice(fromIndex, 1);
268
- if (moved === undefined) return;
269
- next.splice(toIndex, 0, moved);
270
- onChange(next);
271
- }
272
-
273
- function handlePasteCells(
274
- rowIndex: number,
275
- columnIndex: number,
276
- grid: readonly (readonly string[])[],
277
- ): void {
278
- const maxItems = field.embeddedListMaxItems;
279
- const nextRows = [...rows];
280
- const touchedIndices = new Set<number>();
281
- let droppedRows = 0;
282
- let unmatchedCells = 0;
283
-
284
- grid.forEach((gridRow, gridRowOffset) => {
285
- const targetRowIndex = rowIndex + gridRowOffset;
286
- if (targetRowIndex >= nextRows.length) {
287
- if (maxItems !== undefined && nextRows.length >= maxItems) {
288
- droppedRows += 1;
289
- return;
290
- }
291
- nextRows.push({});
292
- }
293
- const targetRow = nextRows[targetRowIndex];
294
- if (targetRow === undefined) return;
295
- let updatedRow: Record<string, unknown> = { ...targetRow };
296
- gridRow.forEach((text, gridColOffset) => {
297
- const column = columns[columnIndex + gridColOffset];
298
- if (column === undefined) return;
299
- const coerced = coerceCellValue(column, text, field.embeddedListCurrency ?? "EUR");
300
- // Also covers an unparseable money paste (e.g. mangled thousands
301
- // separators) — the cell must stay unchanged instead of silently
302
- // clearing to undefined (kumiko-framework#1838).
303
- const isUnmatchedOrUnparseable =
304
- (column.type === "select" || column.type === "reference" || column.type === "money") &&
305
- coerced === undefined &&
306
- text.trim() !== "";
307
- if (isUnmatchedOrUnparseable) {
308
- unmatchedCells += 1;
309
- return;
310
- }
311
- updatedRow = { ...updatedRow, [column.field]: coerced };
312
- });
313
- nextRows[targetRowIndex] = updatedRow;
314
- touchedIndices.add(targetRowIndex);
315
- });
316
-
317
- setPasteWarning(droppedRows > 0 || unmatchedCells > 0 ? { droppedRows, unmatchedCells } : null);
318
-
319
- const recomputed = nextRows.map((row, i) =>
320
- touchedIndices.has(i) ? withRecomputedDerived(row, derived, cells) : row,
321
- );
322
- onChange(recomputed);
323
- }
324
-
325
- return (
326
- <EmbeddedListInput
327
- id={id}
328
- columns={columns}
329
- rows={rows}
330
- totals={totals}
331
- currency={field.embeddedListCurrency}
332
- disabled={field.readOnly}
333
- minItems={field.embeddedListMinItems}
334
- maxItems={field.embeddedListMaxItems}
335
- listIssues={combinedListIssues}
336
- rowIssues={rowIssues}
337
- cellIssues={cellIssues}
338
- onCellChange={handleCellChange}
339
- onAddRow={handleAddRow}
340
- onRemoveRow={handleRemoveRow}
341
- onDuplicateRow={handleDuplicateRow}
342
- onMoveRow={handleMoveRow}
343
- onPasteCells={handlePasteCells}
344
- addLabel={t("kumiko.field.embedded-list.add-row")}
345
- removeLabel={t("kumiko.field.embedded-list.remove-row")}
346
- duplicateLabel={t("kumiko.field.embedded-list.duplicate-row")}
347
- moveUpLabel={t("kumiko.field.embedded-list.move-up")}
348
- moveDownLabel={t("kumiko.field.embedded-list.move-down")}
349
- emptyLabel={t("kumiko.field.embedded-list.empty")}
350
- emptyCtaLabel={t("kumiko.field.embedded-list.empty-cta")}
351
- testId={id}
352
- />
353
- );
354
- }
@@ -1,88 +0,0 @@
1
- import type { EditFieldViewModel, FieldIssue } from "@cosmicdrift/kumiko-headless";
2
- import type { ReactNode } from "react";
3
- import type { FieldCellWidth, usePrimitives } from "../primitives";
4
- import { RenderField } from "./render-field";
5
-
6
- // Extracted out of render-edit.tsx so write-form-section.tsx can reuse it
7
- // without a circular import between the two components.
8
- export type GridCellForFieldProps = {
9
- readonly field: EditFieldViewModel;
10
- readonly columns: number;
11
- readonly issues: readonly FieldIssue[] | undefined;
12
- readonly onChange: (value: unknown) => void;
13
- readonly GridCell: ReturnType<typeof usePrimitives>["GridCell"];
14
- /** Tier 2.7e-3: passed through so Reference fields can build the correct
15
- * lookup query QN (`<feature>:query:<refEntity>:list`). */
16
- readonly featureName: string;
17
- readonly labelAppendix?: ReactNode;
18
- readonly fieldAppendix?: ReactNode;
19
- /** Full issues-by-path map (FormSnapshot.errors) — passed through for
20
- * embedded-list fields, which bucket row-/cell-level issues themselves. */
21
- readonly allIssues: Readonly<Record<string, readonly FieldIssue[]>>;
22
- /** Passed through to RenderField unchanged — see RenderEditProps.valueDisplay. */
23
- readonly valueDisplay: "form" | "text";
24
- /** Passed through to RenderField as `row` — see RenderFieldProps.row. */
25
- readonly row: Readonly<Record<string, unknown>>;
26
- readonly changed?: boolean;
27
- readonly flow?: boolean;
28
- };
29
-
30
- const FIELD_CELL_WIDTH_BY_TYPE: Readonly<Record<string, FieldCellWidth>> = {
31
- number: "number",
32
- decimal: "number",
33
- bigInt: "number",
34
- money: "money",
35
- date: "date",
36
- select: "select",
37
- multiSelect: "select",
38
- boolean: "auto",
39
- longText: "full",
40
- embedded: "full",
41
- jsonb: "full",
42
- file: "full",
43
- files: "full",
44
- image: "full",
45
- images: "full",
46
- };
47
-
48
- function fieldCellWidth(field: EditFieldViewModel): FieldCellWidth {
49
- if (field.span !== undefined || (field.type === "text" && field.multiline)) return "full";
50
- return FIELD_CELL_WIDTH_BY_TYPE[field.type] ?? "text";
51
- }
52
-
53
- export function GridCellForField({
54
- field,
55
- columns,
56
- issues,
57
- onChange,
58
- GridCell,
59
- featureName,
60
- labelAppendix,
61
- fieldAppendix,
62
- allIssues,
63
- valueDisplay,
64
- row,
65
- changed,
66
- flow,
67
- }: GridCellForFieldProps): ReactNode {
68
- // RenderField renders nothing for a hidden field, but the GridCell around it still claims the row.
69
- if (!field.visible) return null;
70
-
71
- const effectiveSpan = field.span !== undefined ? Math.min(field.span, columns) : 1;
72
- return (
73
- <GridCell span={effectiveSpan} {...(flow === true && { width: fieldCellWidth(field) })}>
74
- <RenderField
75
- field={field}
76
- {...(issues !== undefined && { issues })}
77
- onChange={onChange}
78
- featureName={featureName}
79
- {...(labelAppendix !== undefined && { labelAppendix })}
80
- {...(fieldAppendix !== undefined && { fieldAppendix })}
81
- allIssues={allIssues}
82
- valueDisplay={valueDisplay}
83
- row={row}
84
- {...(changed === true && { changed })}
85
- />
86
- </GridCell>
87
- );
88
- }
@@ -1,93 +0,0 @@
1
- import type { EditFieldViewModel } from "@cosmicdrift/kumiko-headless";
2
- import type { ReactNode } from "react";
3
- import { useTranslation } from "../i18n";
4
- import { usePrimitives } from "../primitives";
5
-
6
- export type MultiSelectCheckboxOption = {
7
- readonly value: string;
8
- readonly label: string;
9
- };
10
-
11
- export type MultiSelectCheckboxesProps = {
12
- readonly field: EditFieldViewModel;
13
- readonly id: string;
14
- readonly options: readonly MultiSelectCheckboxOption[];
15
- readonly value: readonly string[];
16
- readonly onChange: (value: readonly string[]) => void;
17
- };
18
-
19
- // Renders MultiSelectFieldDef's `display: "checkboxes"` mode — every option
20
- // as a visible checkbox plus a select-all toggle, built purely from the
21
- // platform-neutral Primitives contract (Grid/GridCell/Field/Input kind
22
- // "boolean", the same primitive `case "boolean"` uses in render-field.tsx).
23
- // No renderer-web import here — this file must stay usable from Expo too.
24
- export function MultiSelectCheckboxes({
25
- field,
26
- id,
27
- options,
28
- value,
29
- onChange,
30
- }: MultiSelectCheckboxesProps): ReactNode {
31
- const { Grid, GridCell, Field, Input, Button } = usePrimitives();
32
- const t = useTranslation();
33
- const disabled = field.readOnly;
34
- const selected = new Set(value);
35
- const allSelected = options.length > 0 && options.every((opt) => selected.has(opt.value));
36
-
37
- // Always emits in `field.options` order (MultiSelectFieldDef's documented
38
- // ordering guarantee), never in click/selection order.
39
- const emitOrdered = (next: ReadonlySet<string>): void => {
40
- onChange(options.filter((opt) => next.has(opt.value)).map((opt) => opt.value));
41
- };
42
-
43
- const toggleOption = (optionValue: string, checked: boolean): void => {
44
- const next = new Set(selected);
45
- if (checked) {
46
- next.add(optionValue);
47
- } else {
48
- next.delete(optionValue);
49
- }
50
- emitOrdered(next);
51
- };
52
-
53
- const toggleAll = (): void => {
54
- emitOrdered(allSelected ? new Set() : new Set(options.map((opt) => opt.value)));
55
- };
56
-
57
- return (
58
- <>
59
- <Button
60
- type="button"
61
- variant="secondary"
62
- size="sm"
63
- disabled={disabled}
64
- onClick={toggleAll}
65
- testId={`${id}-select-all`}
66
- >
67
- {allSelected
68
- ? t("kumiko.field.multiSelect.deselect-all")
69
- : t("kumiko.field.multiSelect.select-all")}
70
- </Button>
71
- <Grid
72
- columns={field.columns ?? 2}
73
- testId={`${id}-checkboxes`}
74
- {...(field.maxRows !== undefined && { maxRows: field.maxRows })}
75
- >
76
- {options.map((opt) => (
77
- <GridCell key={opt.value}>
78
- <Field id={`${id}-${opt.value}`} label={opt.label} layout="inline">
79
- <Input
80
- kind="boolean"
81
- id={`${id}-${opt.value}`}
82
- name={`${id}-${opt.value}`}
83
- value={selected.has(opt.value)}
84
- onChange={(checked) => toggleOption(opt.value, checked)}
85
- disabled={disabled}
86
- />
87
- </Field>
88
- </GridCell>
89
- ))}
90
- </Grid>
91
- </>
92
- );
93
- }
@@ -1,91 +0,0 @@
1
- import type {
2
- EntityDefinition,
3
- EntityEditScreenDefinition,
4
- } from "@cosmicdrift/kumiko-framework/ui-types";
5
- import type { FormValues, SubmitResult } from "@cosmicdrift/kumiko-headless";
6
- import { type ReactNode, useMemo } from "react";
7
- import { buildInitialValues } from "../app/kumiko-screen";
8
- import { toKebab } from "../app/qn";
9
- import { useTranslation } from "../i18n";
10
- import { usePrimitives } from "../primitives";
11
- import { RenderEdit } from "./render-edit";
12
-
13
- // Hosts a target entity's create-form inside a bare Modal so a reference
14
- // field can create a missing record without leaving the current form
15
- // (kumiko-framework#1681) — same create wiring as EntityEditCreateBody
16
- // (kumiko-screen.tsx), but reports the new id back via a callback instead
17
- // of navigating to the entity's list screen.
18
-
19
- function entityWriteCommand(featureName: string, entity: string): string {
20
- return `${toKebab(featureName)}:write:${toKebab(entity)}:create`;
21
- }
22
-
23
- // The create handler's success payload is `{ kind: "save", id, ... }`
24
- // (see event-store-executor-write.ts) but RenderEdit's onSubmit only
25
- // types it as `unknown` — narrow defensively instead of casting through it.
26
- // `field` is author-declared (ActionFormRedirect.idFrom, fw#2670), never
27
- // user input; the own-property check keeps a prototype key like
28
- // "constructor" from resolving to anything.
29
- export function extractIdField(data: unknown, field: string): string | undefined {
30
- if (typeof data !== "object" || data === null) return undefined;
31
- if (!Object.hasOwn(data, field)) return undefined;
32
- const value = (data as Record<string, unknown>)[field];
33
- return typeof value === "string" ? value : undefined;
34
- }
35
-
36
- export function extractCreatedId(data: unknown): string | undefined {
37
- return extractIdField(data, "id");
38
- }
39
-
40
- export type ReferenceCreateDialogProps = {
41
- readonly open: boolean;
42
- readonly onClose: () => void;
43
- // id is undefined when the record was created but the write-handler's
44
- // success payload carried no `id` (custom create-handler variant) — the
45
- // record already exists server-side, so this is still a success signal
46
- // to the caller, just one it can't auto-select from (#1694).
47
- readonly onCreated: (id: string | undefined) => void;
48
- readonly featureName: string;
49
- readonly screen: EntityEditScreenDefinition;
50
- readonly entity: EntityDefinition;
51
- };
52
-
53
- export function ReferenceCreateDialog({
54
- open,
55
- onClose,
56
- onCreated,
57
- featureName,
58
- screen,
59
- entity,
60
- }: ReferenceCreateDialogProps): ReactNode {
61
- const { Modal } = usePrimitives();
62
- const t = useTranslation();
63
- const initial = useMemo(
64
- () => buildInitialValues(entity.fields, entity.defaultCurrency ?? "EUR") as FormValues,
65
- [entity.fields, entity.defaultCurrency],
66
- );
67
- const writeCommand = entityWriteCommand(featureName, screen.entity);
68
- const handleSubmitted = (result: SubmitResult<unknown>): void => {
69
- if (result.validationBlocked || !result.isSuccess) return;
70
- onCreated(extractCreatedId(result.data));
71
- };
72
- if (!open) return null;
73
- return (
74
- <Modal
75
- open={open}
76
- onOpenChange={(next) => !next && onClose()}
77
- title={t("kumiko.actions.create")}
78
- >
79
- <RenderEdit
80
- screen={screen}
81
- entity={entity}
82
- featureName={featureName}
83
- initial={initial}
84
- writeCommand={writeCommand}
85
- onSubmit={handleSubmitted}
86
- onCancel={onClose}
87
- {...(screen.submitLabel !== undefined && { submitLabel: screen.submitLabel })}
88
- />
89
- </Modal>
90
- );
91
- }