@cosmicdrift/kumiko-renderer 0.389.0 → 0.390.0

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 (230) hide show
  1. package/dist/app/projection-detail-body.js +59 -394
  2. package/dist/app/projection-detail-header-model.d.ts +11 -0
  3. package/dist/app/projection-detail-header-model.js +33 -0
  4. package/dist/app/projection-detail-header-region.d.ts +28 -0
  5. package/dist/app/projection-detail-header-region.js +81 -0
  6. package/dist/app/projection-detail-header.d.ts +29 -0
  7. package/dist/app/projection-detail-header.js +84 -0
  8. package/dist/app/projection-detail-navigation.d.ts +13 -0
  9. package/dist/app/projection-detail-navigation.js +91 -0
  10. package/dist/app/projection-detail-states.d.ts +22 -0
  11. package/dist/app/projection-detail-states.js +26 -0
  12. package/dist/app/projection-drawer-host.d.ts +12 -0
  13. package/dist/app/projection-drawer-host.js +10 -0
  14. package/dist/app/projection-list-body.d.ts +1 -1
  15. package/dist/app/projection-list-body.js +69 -189
  16. package/dist/app/projection-list-render-props.d.ts +56 -0
  17. package/dist/app/projection-list-render-props.js +47 -0
  18. package/dist/app/projection-list-render.d.ts +26 -0
  19. package/dist/app/projection-list-render.js +20 -0
  20. package/dist/app/projection-list-row-click.d.ts +12 -0
  21. package/dist/app/projection-list-row-click.js +14 -0
  22. package/dist/app/use-projection-detail-actions.d.ts +35 -0
  23. package/dist/app/use-projection-detail-actions.js +156 -0
  24. package/dist/app/use-projection-detail-data.d.ts +19 -0
  25. package/dist/app/use-projection-detail-data.js +47 -0
  26. package/dist/app/use-projection-detail-layout.d.ts +17 -0
  27. package/dist/app/use-projection-detail-layout.js +23 -0
  28. package/dist/app/use-projection-list-actions.d.ts +31 -0
  29. package/dist/app/use-projection-list-actions.js +73 -0
  30. package/dist/app/use-projection-list-data.d.ts +20 -0
  31. package/dist/app/use-projection-list-data.js +26 -0
  32. package/dist/app/use-projection-list-definitions.d.ts +11 -0
  33. package/dist/app/use-projection-list-definitions.js +20 -0
  34. package/dist/app/use-projection-list-query.d.ts +28 -0
  35. package/dist/app/use-projection-list-query.js +83 -0
  36. package/dist/components/render-edit-banners.d.ts +12 -0
  37. package/dist/components/render-edit-banners.js +90 -0
  38. package/dist/components/render-edit-draft-save-helpers.d.ts +19 -0
  39. package/dist/components/render-edit-draft-save-helpers.js +42 -0
  40. package/dist/components/render-edit-field-grid.d.ts +4 -0
  41. package/dist/components/render-edit-field-grid.js +40 -0
  42. package/dist/components/render-edit-fields-section.d.ts +4 -0
  43. package/dist/components/render-edit-fields-section.js +76 -0
  44. package/dist/components/render-edit-form-actions.d.ts +4 -0
  45. package/dist/components/render-edit-form-actions.js +66 -0
  46. package/dist/components/render-edit-header-actions.d.ts +11 -0
  47. package/dist/components/render-edit-header-actions.js +120 -0
  48. package/dist/components/render-edit-layout-flags.d.ts +29 -0
  49. package/dist/components/render-edit-layout-flags.js +106 -0
  50. package/dist/components/render-edit-layout.d.ts +4 -0
  51. package/dist/components/render-edit-layout.js +79 -0
  52. package/dist/components/render-edit-render-model.d.ts +63 -0
  53. package/dist/components/render-edit-render-model.js +14 -0
  54. package/dist/components/render-edit-section-kinds.d.ts +4 -0
  55. package/dist/components/render-edit-section-kinds.js +115 -0
  56. package/dist/components/render-edit-submit-result.d.ts +8 -0
  57. package/dist/components/render-edit-submit-result.js +77 -0
  58. package/dist/components/render-edit-submit.d.ts +7 -0
  59. package/dist/components/render-edit-submit.js +113 -0
  60. package/dist/components/render-edit-titles.d.ts +17 -0
  61. package/dist/components/render-edit-titles.js +99 -0
  62. package/dist/components/render-edit-wizard-navigation.d.ts +11 -0
  63. package/dist/components/render-edit-wizard-navigation.js +115 -0
  64. package/dist/components/render-edit.d.ts +2 -2
  65. package/dist/components/render-edit.js +63 -1128
  66. package/dist/components/render-field-choice-inputs.d.ts +7 -0
  67. package/dist/components/render-field-choice-inputs.js +52 -0
  68. package/dist/components/render-field-control.d.ts +12 -0
  69. package/dist/components/render-field-control.js +93 -0
  70. package/dist/components/render-field-display-text.d.ts +4 -0
  71. package/dist/components/render-field-display-text.js +48 -0
  72. package/dist/components/render-field-fill-action.d.ts +13 -0
  73. package/dist/components/render-field-fill-action.js +63 -0
  74. package/dist/components/render-field-input-shared.d.ts +39 -0
  75. package/dist/components/render-field-input-shared.js +20 -0
  76. package/dist/components/render-field-input.d.ts +3 -0
  77. package/dist/components/render-field-input.js +34 -0
  78. package/dist/components/render-field-meta.d.ts +6 -0
  79. package/dist/components/render-field-meta.js +51 -0
  80. package/dist/components/render-field-read-only.d.ts +11 -0
  81. package/dist/components/render-field-read-only.js +75 -0
  82. package/dist/components/render-field-reference.d.ts +15 -0
  83. package/dist/components/render-field-reference.js +103 -0
  84. package/dist/components/render-field-scalar-inputs.d.ts +10 -0
  85. package/dist/components/render-field-scalar-inputs.js +35 -0
  86. package/dist/components/render-field-text-inputs.d.ts +3 -0
  87. package/dist/components/render-field-text-inputs.js +34 -0
  88. package/dist/components/render-field-values.d.ts +18 -0
  89. package/dist/components/render-field-values.js +111 -0
  90. package/dist/components/render-field-write-only.d.ts +9 -0
  91. package/dist/components/render-field-write-only.js +29 -0
  92. package/dist/components/render-field.d.ts +2 -2
  93. package/dist/components/render-field.js +39 -670
  94. package/dist/components/render-list-header-slot.d.ts +5 -0
  95. package/dist/components/render-list-header-slot.js +30 -0
  96. package/dist/components/render-list-reference-bridge.d.ts +3 -0
  97. package/dist/components/render-list-reference-bridge.js +25 -0
  98. package/dist/components/render-list-reference-columns.d.ts +25 -0
  99. package/dist/components/render-list-reference-columns.js +137 -0
  100. package/dist/components/render-list-table-props.d.ts +52 -0
  101. package/dist/components/render-list-table-props.js +53 -0
  102. package/dist/components/render-list-toolbar-model.d.ts +42 -0
  103. package/dist/components/render-list-toolbar-model.js +82 -0
  104. package/dist/components/render-list-toolbar.d.ts +38 -0
  105. package/dist/components/render-list-toolbar.js +40 -0
  106. package/dist/components/render-list-types.d.ts +113 -0
  107. package/dist/components/render-list-types.js +1 -0
  108. package/dist/components/render-list.d.ts +4 -113
  109. package/dist/components/render-list.js +91 -307
  110. package/dist/components/use-reference-input-state.d.ts +20 -0
  111. package/dist/components/use-reference-input-state.js +85 -0
  112. package/dist/components/use-render-edit-controlled-sync.d.ts +3 -0
  113. package/dist/components/use-render-edit-controlled-sync.js +26 -0
  114. package/dist/components/use-render-edit-draft-save.d.ts +29 -0
  115. package/dist/components/use-render-edit-draft-save.js +141 -0
  116. package/dist/components/use-render-edit-form.d.ts +10 -0
  117. package/dist/components/use-render-edit-form.js +101 -0
  118. package/dist/components/use-render-edit-sections.d.ts +15 -0
  119. package/dist/components/use-render-edit-sections.js +100 -0
  120. package/dist/components/use-render-edit-state.d.ts +65 -0
  121. package/dist/components/use-render-edit-state.js +158 -0
  122. package/dist/components/use-render-edit-steps.d.ts +13 -0
  123. package/dist/components/use-render-edit-steps.js +52 -0
  124. package/dist/components/use-render-list-columns.d.ts +18 -0
  125. package/dist/components/use-render-list-columns.js +41 -0
  126. package/dist/components/use-render-list-search.d.ts +4 -0
  127. package/dist/components/use-render-list-search.js +50 -0
  128. package/dist/primitives.d.ts +1 -1
  129. package/dist/web/app/dashboard-body.d.ts +1 -1
  130. package/dist/web/app/dashboard-body.js +42 -246
  131. package/dist/web/app/dashboard-header.d.ts +34 -0
  132. package/dist/web/app/dashboard-header.js +58 -0
  133. package/dist/web/app/dashboard-screen-controls.d.ts +11 -0
  134. package/dist/web/app/dashboard-screen-controls.js +42 -0
  135. package/dist/web/app/dashboard-screen-tiles.d.ts +8 -0
  136. package/dist/web/app/dashboard-screen-tiles.js +42 -0
  137. package/dist/web/app/dashboard-visibility.d.ts +42 -0
  138. package/dist/web/app/dashboard-visibility.js +102 -0
  139. package/dist/web/app/use-dashboard-screen-state.d.ts +35 -0
  140. package/dist/web/app/use-dashboard-screen-state.js +69 -0
  141. package/dist/web/primitives/data-table/actions-column.js +6 -5
  142. package/dist/web/primitives/data-table/card-body.d.ts +6 -0
  143. package/dist/web/primitives/data-table/card-body.js +32 -0
  144. package/dist/web/primitives/data-table/card-row.d.ts +5 -0
  145. package/dist/web/primitives/data-table/card-row.js +31 -0
  146. package/dist/web/primitives/data-table/card-view.d.ts +3 -0
  147. package/dist/web/primitives/data-table/card-view.js +13 -0
  148. package/dist/web/primitives/data-table/column-resize-handle.js +3 -1
  149. package/dist/web/primitives/data-table/data-cell-props.d.ts +7 -0
  150. package/dist/web/primitives/data-table/data-cell-props.js +8 -0
  151. package/dist/web/primitives/data-table/row-render-parts.d.ts +8 -0
  152. package/dist/web/primitives/data-table/row-render-parts.js +35 -0
  153. package/dist/web/primitives/data-table/table-body.d.ts +3 -0
  154. package/dist/web/primitives/data-table/table-body.js +32 -0
  155. package/dist/web/primitives/data-table/table-content.d.ts +3 -0
  156. package/dist/web/primitives/data-table/table-content.js +41 -0
  157. package/dist/web/primitives/data-table/table-data-cell.d.ts +4 -0
  158. package/dist/web/primitives/data-table/table-data-cell.js +25 -0
  159. package/dist/web/primitives/data-table/table-header-row.d.ts +3 -0
  160. package/dist/web/primitives/data-table/table-header-row.js +21 -0
  161. package/dist/web/primitives/data-table/table-view.d.ts +3 -0
  162. package/dist/web/primitives/data-table/table-view.js +25 -0
  163. package/dist/web/primitives/data-table/toolbar.d.ts +6 -0
  164. package/dist/web/primitives/data-table/toolbar.js +53 -0
  165. package/dist/web/primitives/data-table/use-data-table-state.d.ts +44 -0
  166. package/dist/web/primitives/data-table/use-data-table-state.js +123 -0
  167. package/dist/web/primitives/data-table.d.ts +3 -4
  168. package/dist/web/primitives/data-table.js +32 -252
  169. package/dist/web/primitives/file-value-list.js +1 -1
  170. package/dist/web/primitives/form-footer-parts.d.ts +25 -0
  171. package/dist/web/primitives/form-footer-parts.js +55 -0
  172. package/dist/web/primitives/form.js +33 -43
  173. package/dist/web/primitives/input-choice-kinds.d.ts +4 -0
  174. package/dist/web/primitives/input-choice-kinds.js +67 -0
  175. package/dist/web/primitives/input-file-kinds.d.ts +3 -0
  176. package/dist/web/primitives/input-file-kinds.js +6 -0
  177. package/dist/web/primitives/input-number-kinds.d.ts +4 -0
  178. package/dist/web/primitives/input-number-kinds.js +16 -0
  179. package/dist/web/primitives/input-optional-props.d.ts +20 -0
  180. package/dist/web/primitives/input-optional-props.js +24 -0
  181. package/dist/web/primitives/input-select-presentation.d.ts +13 -0
  182. package/dist/web/primitives/input-select-presentation.js +19 -0
  183. package/dist/web/primitives/input-shared.d.ts +22 -0
  184. package/dist/web/primitives/input-shared.js +10 -0
  185. package/dist/web/primitives/input-temporal-kinds.d.ts +5 -0
  186. package/dist/web/primitives/input-temporal-kinds.js +10 -0
  187. package/dist/web/primitives/input-text-kinds.d.ts +7 -0
  188. package/dist/web/primitives/input-text-kinds.js +33 -0
  189. package/dist/web/primitives/input.js +35 -161
  190. package/dist/web/widgets/chart-data.d.ts +35 -0
  191. package/dist/web/widgets/chart-data.js +37 -0
  192. package/dist/web/widgets/chart-markers.d.ts +26 -0
  193. package/dist/web/widgets/chart-markers.js +64 -0
  194. package/dist/web/widgets/chart-parts.d.ts +20 -0
  195. package/dist/web/widgets/chart-parts.js +15 -0
  196. package/dist/web/widgets/charts.d.ts +5 -78
  197. package/dist/web/widgets/charts.js +7 -485
  198. package/dist/web/widgets/drawer-panel-class.d.ts +3 -0
  199. package/dist/web/widgets/drawer-panel-class.js +40 -0
  200. package/dist/web/widgets/drawer-resize-controls.d.ts +11 -0
  201. package/dist/web/widgets/drawer-resize-controls.js +12 -0
  202. package/dist/web/widgets/drawer-sizing.d.ts +19 -0
  203. package/dist/web/widgets/drawer-sizing.js +60 -0
  204. package/dist/web/widgets/drawer.js +17 -116
  205. package/dist/web/widgets/stacked-area-brush.d.ts +13 -0
  206. package/dist/web/widgets/stacked-area-brush.js +91 -0
  207. package/dist/web/widgets/stacked-area-chart.d.ts +6 -0
  208. package/dist/web/widgets/stacked-area-chart.js +45 -0
  209. package/dist/web/widgets/stacked-area-chrome.d.ts +7 -0
  210. package/dist/web/widgets/stacked-area-chrome.js +77 -0
  211. package/dist/web/widgets/stacked-area-geometry.d.ts +54 -0
  212. package/dist/web/widgets/stacked-area-geometry.js +83 -0
  213. package/dist/web/widgets/stacked-area-model.d.ts +11 -0
  214. package/dist/web/widgets/stacked-area-model.js +1 -0
  215. package/dist/web/widgets/stacked-area-plot.d.ts +3 -0
  216. package/dist/web/widgets/stacked-area-plot.js +51 -0
  217. package/dist/web/widgets/stacked-area-props.d.ts +83 -0
  218. package/dist/web/widgets/stacked-area-props.js +16 -0
  219. package/dist/web/widgets/stacked-area-svg.d.ts +3 -0
  220. package/dist/web/widgets/stacked-area-svg.js +38 -0
  221. package/dist/web/widgets/stacked-area-tooltip.d.ts +3 -0
  222. package/dist/web/widgets/stacked-area-tooltip.js +86 -0
  223. package/dist/web/widgets/use-drawer-resize.d.ts +22 -0
  224. package/dist/web/widgets/use-drawer-resize.js +52 -0
  225. package/dist/web/widgets/use-stacked-area-tooltip.d.ts +9 -0
  226. package/dist/web/widgets/use-stacked-area-tooltip.js +34 -0
  227. package/dist/web/widgets/use-stacked-area-window.d.ts +20 -0
  228. package/dist/web/widgets/use-stacked-area-window.js +76 -0
  229. package/package.json +9 -9
  230. package/src/changes.json +54 -0
@@ -1,32 +1,25 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { isFormatSpec, SYSTEM_REFERENCE_LABELS, UNRESOLVED_REFERENCE_LABEL_KEYS, } from "@cosmicdrift/kumiko-framework/ui-types";
3
- import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
4
- import { createContext, Fragment, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
5
- import { useAppFeatures } from "../app/app-features-context.js";
6
- import { useColumnRenderer } from "../app/column-renderers.js";
7
- import { extensionSectionName } from "../app/extension-sections.js";
8
- import { toKebab } from "../app/qn.js";
9
- import { useReferenceLinkResolver } from "../app/reference-link.js";
10
- import { evalRowExtractor } from "../app/row-actions.js";
11
- import { screenAccessAllows } from "../app/screen-access.js";
12
- import { useOptionalDispatcher } from "../context/dispatcher-context.js";
13
- import { useUserRoles } from "../context/user-roles-context.js";
14
- import { rendererError, rendererWarn } from "../dev-console.js";
15
- import { fileIdsOf } from "../file-ids-of.js";
16
- import { applyFormatSpec, currencyDecimals, EMPTY_VALUE_PLACEHOLDER, } from "../headless/index.js";
17
- import { REFERENCE_COMBOBOX_LIMIT } from "../hooks/reference-limits.js";
18
- import { useQuery } from "../hooks/use-query.js";
19
- import { isCompleteRowPage, referenceOptionSource } from "../hooks/use-reference-lookup.js";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from "react";
20
3
  import { useLocale, useTranslation } from "../i18n.js";
21
4
  import { usePrimitives } from "../primitives.js";
22
- import { AvailabilityOptionsMultiSelect, AvailabilityOptionsSelect, staticSelectOptions, } from "./availability-options-select.js";
23
- import { EmbeddedListField } from "./embedded-list-field.js";
24
- import { FileListField } from "./file-list-field.js";
25
- import { MultiSelectCheckboxes } from "./multi-select-checkboxes.js";
26
- import { QueryOptionsSelect } from "./query-options-select.js";
27
- import { ReferenceCreateDialog } from "./reference-create-dialog.js";
5
+ import { listPrintsOwnIssues, renderControl } from "./render-field-control.js";
6
+ import { inputId } from "./render-field-meta.js";
28
7
  const AllFieldsRequiredContext = createContext(false);
29
8
  export const AllFieldsRequiredProvider = AllFieldsRequiredContext.Provider;
9
+ function fieldOptionalProps({ issues, printsIssues, labelAppendix, fieldAppendix, appendixPlacement, description, status, changed, layout, accent, hideLabel, }) {
10
+ return {
11
+ ...(issues !== undefined && printsIssues && { issues }),
12
+ ...(labelAppendix !== undefined && { labelAppendix }),
13
+ ...(fieldAppendix !== undefined && { fieldAppendix }),
14
+ ...(appendixPlacement !== undefined && { appendixPlacement }),
15
+ ...(description !== undefined && { description }),
16
+ ...(status !== undefined && { status }),
17
+ ...(changed === true && { changed }),
18
+ ...(layout !== undefined && { layout }),
19
+ ...(accent === true && { accent }),
20
+ ...(hideLabel === true && { hideLabel }),
21
+ };
22
+ }
30
23
  export function RenderField({ field, issues, onChange, featureName, labelAppendix, fieldAppendix, appendixPlacement, description, status, allIssues, valueDisplay = "form", row, changed, layout, accent, fillDisabled, hideLabel, }) {
31
24
  const { Field, Input, Banner, Text, JsonView } = usePrimitives();
32
25
  // The app locale is required for money/date inputs; otherwise they fall back
@@ -39,40 +32,16 @@ export function RenderField({ field, issues, onChange, featureName, labelAppendi
39
32
  if (!field.visible)
40
33
  return null;
41
34
  const id = inputId(field);
42
- const hasError = issues !== undefined && issues.length > 0;
43
35
  // An author-declared renderer always wins over the input widget, same as
44
36
  // the list-column path (render-list.tsx). Only takes effect once the field
45
37
  // is actually readOnly — a FormatSpec/PlatformComponent renderer has no
46
38
  // editable widget of its own, so applying it to an editable field would
47
39
  // silently make that field un-editable.
48
40
  const readOnlyText = valueDisplay === "text" && field.readOnly && field.renderer === undefined;
49
- // The reference field renders its own component because it uses useQuery()
50
- // for the live lookup and must therefore be mounted as a React component,
51
- // not called as a pure render function.
52
- // The list input prints its own list-level issues (including the empty state),
53
- // so the Field wrapper must not print them a second time.
54
- const listPrintsOwnIssues = field.type === "embedded" &&
55
- field.embeddedListCells !== undefined &&
56
- !(field.readOnly && field.renderer !== undefined);
57
- const control = field.readOnly && field.renderer !== undefined ? (_jsx(FieldRendererOutput, { field: field, renderer: field.renderer, appLocale: appLocale, ...(row !== undefined && { row }) })) : field.type === "embedded" && field.embeddedListCells !== undefined ? (_jsx(EmbeddedListField, { field: field, id: id, onChange: onChange, allIssues: allIssues ?? {}, featureName: featureName ?? "" })) : field.type === "reference" ? (readOnlyText ? (_jsx(ReadOnlyReferenceValue, { field: field, featureName: featureName ?? "" })) : (_jsx(ReferenceInput, { field: field, id: id, hasError: hasError, onChange: onChange, Input: Input, featureName: featureName ?? "" }))) : field.type === "select" && field.selectOptionsQuery !== undefined && !readOnlyText ? (_jsx(QueryOptionsSelect, { field: field, query: field.selectOptionsQuery.query, payload: field.selectOptionsQuery.payload, row: row ?? { [field.field]: field.value }, id: id, hasError: hasError, onChange: onChange })) : field.type === "select" &&
58
- field.selectOptionsAvailabilityQuery !== undefined &&
59
- !readOnlyText ? (_jsx(AvailabilityOptionsSelect, { field: field, query: field.selectOptionsAvailabilityQuery.query, payload: field.selectOptionsAvailabilityQuery.payload, row: row ?? { [field.field]: field.value }, id: id, hasError: hasError, onChange: onChange })) : readOnlyText && !isComplexFieldType(field.type) && !isFileFieldType(field.type) ? (_jsx(Text, { testId: `field-value-${field.field}`, children: readOnlyDisplayText(field, appLocale, t) })) : field.type === "text" && field.writeOnly === true && !readOnlyText ? (_jsx(WriteOnlyTextInput, { field: field, id: id, hasError: hasError, onChange: onChange, changed: changed === true })) : field.fillAction !== undefined ? (_jsx(FillActionControl, { field: field, fillAction: field.fillAction, row: row ?? { [field.field]: field.value }, onChange: onChange, disabled: fillDisabled === true, Banner: Banner, renderControl: (fill) => renderInput({
60
- field,
61
- id,
62
- hasError,
63
- onChange,
64
- Input,
65
- appLocale,
66
- Banner,
67
- Text,
68
- JsonView,
69
- t,
70
- row,
71
- fill,
72
- }) })) : (renderInput({
41
+ const control = renderControl({
73
42
  field,
74
43
  id,
75
- hasError,
44
+ hasError: issues !== undefined && issues.length > 0,
76
45
  onChange,
77
46
  Input,
78
47
  appLocale,
@@ -80,624 +49,24 @@ export function RenderField({ field, issues, onChange, featureName, labelAppendi
80
49
  Text,
81
50
  JsonView,
82
51
  t,
83
- row,
84
- }));
85
- return (_jsx(Field, { id: id, label: field.label, required: field.required && field.type !== "boolean" && !allFieldsRequired, ...(issues !== undefined && !listPrintsOwnIssues && { issues }), ...(labelAppendix !== undefined && { labelAppendix }), ...(fieldAppendix !== undefined && { fieldAppendix }), ...(appendixPlacement !== undefined && { appendixPlacement }), ...(description !== undefined && { description }), ...(status !== undefined && { status }), ...(changed === true && { changed }), ...(layout !== undefined && { layout }), ...(accent === true && { accent }), ...(hideLabel === true && { hideLabel }), testId: `field-${field.field}`, children: control }));
86
- }
87
- const NUMERIC_FIELD_TYPES = new Set(["number", "bigInt", "decimal"]);
88
- // A text field takes a number as its string form; a numeric field takes only a
89
- // number — a string would be sent as a wrong-typed value.
90
- function fillValueFor(fieldType, result) {
91
- if (NUMERIC_FIELD_TYPES.has(fieldType))
92
- return typeof result === "number" ? result : undefined;
93
- if (typeof result === "string")
94
- return result;
95
- return typeof result === "number" ? String(result) : undefined;
96
- }
97
- // Owns the query state of a fill button. A separate component because RenderField
98
- // returns early for hidden fields and the dispatcher hook must not be conditional.
99
- function FillActionControl({ field, fillAction, row, onChange, disabled, Banner, renderControl, }) {
100
- const dispatcher = useOptionalDispatcher();
101
- const t = useTranslation();
102
- const [busy, setBusy] = useState(false);
103
- const [failed, setFailed] = useState(false);
104
- // A newer click or an unmount cancels the in-flight query so a late answer
105
- // cannot overwrite a value the user has since typed.
106
- const activeRun = useRef(null);
107
- useEffect(() => () => activeRun.current?.abort(), []);
108
- const run = async () => {
109
- if (dispatcher === undefined) {
110
- setFailed(true);
111
- return;
112
- }
113
- activeRun.current?.abort();
114
- const ctrl = new AbortController();
115
- activeRun.current = ctrl;
116
- setBusy(true);
117
- setFailed(false);
118
- const payload = fillAction.params === undefined ? {} : evalRowExtractor(fillAction.params, row);
119
- const result = await dispatcher.query(fillAction.query, payload, {
120
- signal: ctrl.signal,
121
- });
122
- if (ctrl.signal.aborted)
123
- return;
124
- setBusy(false);
125
- if (!result.isSuccess) {
126
- setFailed(true);
127
- return;
128
- }
129
- const value = fillValueFor(field.type, propertyOf(result.data, fillAction.field));
130
- if (value === undefined) {
131
- setFailed(true);
132
- return;
133
- }
134
- onChange(value);
135
- };
136
- return (_jsxs(_Fragment, { children: [renderControl({
137
- label: fillAction.label,
138
- onPress: () => void run(),
139
- busy,
140
- disabled,
141
- testId: `field-fill-${field.field}`,
142
- }), failed && (_jsx(Banner, { variant: "error", testId: `field-fill-error-${field.field}`, children: t("kumiko.field.fillAction.failed") }))] }));
143
- }
144
- // Wire value of a writeOnly field is `true` (set) / `null` (empty), never the
145
- // secret. Typing sets a new value; an untouched or emptied input submits
146
- // nothing / "" which the server reads as "keep". `null` after a loaded `true`
147
- // (changed) means "remove on save".
148
- function WriteOnlyTextInput({ field, id, hasError, onChange, changed, }) {
149
- const { Input } = usePrimitives();
150
- const t = useTranslation();
151
- const isSet = field.value === true;
152
- const isMarkedForRemoval = field.value === null && changed;
153
- if (isMarkedForRemoval) {
154
- return (_jsx(Input, { kind: "password", id: id, name: field.field, disabled: true, hasError: hasError, value: "", onChange: () => { }, autoComplete: "new-password", placeholder: t("kumiko.field.writeOnly.willRemove"), testId: `field-write-only-removal-${field.field}`, trailingAction: {
155
- icon: "undo",
156
- label: t("kumiko.field.writeOnly.undo"),
157
- onPress: () => onChange(true),
158
- } }));
159
- }
160
- return (_jsx(Input, { kind: "password", id: id, name: field.field, disabled: field.readOnly, required: field.required && !isSet, hasError: hasError, value: typeof field.value === "string" ? field.value : "", onChange: (v) => onChange(v), autoComplete: "new-password", ...(isSet && { placeholder: t("kumiko.field.writeOnly.setPlaceholder") }), ...(isSet &&
161
- !field.required &&
162
- !field.readOnly && {
163
- trailingAction: {
164
- icon: "clear",
165
- label: t("kumiko.field.writeOnly.remove"),
166
- onPress: () => onChange(null),
167
- },
168
- }) }));
169
- }
170
- // ReferenceInput renders a searchable combobox populated by a live query for
171
- // the referenced entity. With the default limit of 200, larger datasets miss
172
- // the tail in the dropdown; a server-side debounced search query comes later.
173
- // - Display = row[refLabelField], default labelField "id".
174
- // - During loading, the dropdown is empty and the field is disabled.
175
- // - In multiple mode (field.refMultiple), value is a UUID array and the
176
- // combobox renders tags.
177
- //
178
- // Storage: the UI value is a UUID (row.id), or a UUID array in multiple mode.
179
- // Server schema: z.uuid() or z.array(z.uuid()).
180
- // REFERENCE_COMBOBOX_LIMIT is centralized in hooks/reference-limits.ts
181
- // (see there for the rationale behind the default values).
182
- function ReferenceInput({ field, id, hasError, onChange, Input, featureName, }) {
183
- const { Banner } = usePrimitives();
184
- const refEntity = field.refEntity ?? "";
185
- const refFeature = field.refFeature ?? featureName;
186
- const isMultiple = field.refMultiple === true;
187
- // refFeature may differ from featureName (for example,
188
- // items.assignee → users:query:user:list). The default is the same feature,
189
- // as provided by the ViewModel (parseRefTarget).
190
- // refOptionsQuery replaces that source with an author-supplied handler whose
191
- // rows already carry a composed `label`.
192
- const { queryQn, labelKey } = referenceOptionSource(field, refFeature, refEntity);
193
- // "+ Neu" in the combobox opens the referenced entity's create screen in a
194
- // dialog instead of leaving the current form.
195
- // refFeature may differ from the currently rendered feature; appFeatures
196
- // (createKumikoApp) knows all feature schemas, not only the active screen.
197
- // If the feature, screen, or entity is not registered, or allowCreate:false,
198
- // onCreate remains undefined and the combobox renders without the footer.
199
- const appFeatures = useAppFeatures();
200
- const userRoles = useUserRoles();
201
- const t = useTranslation();
202
- const [createOpen, setCreateOpen] = useState(false);
203
- const refTargetSchema = appFeatures.find((f) => f.featureName === refFeature);
204
- const refCreateScreen = refTargetSchema?.screens.find((s) => s.type === "entityEdit" &&
205
- s.entity === refEntity &&
206
- s.allowCreate !== false &&
207
- screenAccessAllows(s.access, userRoles));
208
- const refEntityDef = refTargetSchema?.entities[refEntity];
209
- // When the user types in the combobox, the server filters through the
210
- // existing list-payload `search` parameter. The combobox debounces the
211
- // keystroke itself (300ms) and calls onSearchChange.
212
- // An empty initial state requests the first 50 items in default order.
213
- const [searchTerm, setSearchTerm] = useState("");
214
- const queryPayload = useMemo(() => searchTerm === ""
215
- ? { limit: REFERENCE_COMBOBOX_LIMIT }
216
- : { limit: REFERENCE_COMBOBOX_LIMIT, search: searchTerm }, [searchTerm]);
217
- const queryResult = useQuery(queryQn, queryPayload);
218
- const handleSearchChange = useCallback((q) => setSearchTerm(q), []);
219
- const canCreate = !field.readOnly && refCreateScreen !== undefined && refEntityDef !== undefined;
220
- const [createdWithoutIdWarning, setCreatedWithoutIdWarning] = useState(false);
221
- const handleCreated = useCallback((newId) => {
222
- setCreateOpen(false);
223
- // Clear the search filter before refetching — the just-created record
224
- // won't match whatever the user had typed pre-create, and would then
225
- // be missing from `options` (shown as a raw id instead of a label).
226
- setSearchTerm("");
227
- queryResult.refetch().catch((err) => {
228
- rendererError("render-field: refetch after create failed", err);
229
- });
230
- if (newId === undefined) {
231
- // Record was created server-side but the payload carried no id —
232
- // can't auto-select it, surface that instead of failing silently.
233
- setCreatedWithoutIdWarning(true);
234
- return;
235
- }
236
- setCreatedWithoutIdWarning(false);
237
- if (isMultiple) {
238
- const current = stringArrayOf(field.value);
239
- onChange([...current, newId]);
240
- }
241
- else {
242
- onChange(newId);
243
- }
244
- }, [isMultiple, field.value, onChange, queryResult.refetch]);
245
- const options = useMemo(() => {
246
- const rows = queryResult.data?.rows ?? [];
247
- return rows.map((row) => {
248
- const idVal = String(row["id"] ?? "");
249
- const label = String(row[labelKey] ?? idVal);
250
- return { value: idVal, label };
251
- });
252
- }, [queryResult.data, labelKey]);
253
- // Single mode uses a String/null value; multiple mode uses an array. Coerce
254
- // to the shape expected by the combobox so storage drift (for example, an
255
- // old String from the server where an array is now expected) cannot crash it.
256
- const baseInputProps = {
257
- id,
258
- name: field.field,
259
- // Disable the input during the initial load; subsequent loads show a
260
- // loading indicator in the popover.
261
- disabled: field.readOnly || (queryResult.loading && options.length === 0),
262
- required: field.required,
263
- hasError,
264
- options,
265
- onSearchChange: handleSearchChange,
266
- loading: queryResult.loading,
267
- ...(canCreate && {
268
- onCreate: () => setCreateOpen(true),
269
- createLabel: t("kumiko.actions.create"),
270
- }),
271
- };
272
- const createDialog = (_jsxs(_Fragment, { children: [canCreate && refCreateScreen && refEntityDef && (_jsx(ReferenceCreateDialog, { open: createOpen, onClose: () => setCreateOpen(false), onCreated: handleCreated, featureName: refFeature, screen: refCreateScreen, entity: refEntityDef })), createdWithoutIdWarning && (_jsx(Banner, { variant: "error", children: t("kumiko.field.reference-created-no-id") }))] }));
273
- if (isMultiple) {
274
- const arrayValue = stringArrayOf(field.value);
275
- return (_jsxs(_Fragment, { children: [_jsx(Input, { kind: "combobox", ...baseInputProps, multiple: true, value: arrayValue, onChange: (v) => onChange(v) }), createDialog] }));
276
- }
277
- const stringValue = field.value === undefined || field.value === null ? "" : String(field.value);
278
- return (_jsxs(_Fragment, { children: [_jsx(Input, { kind: "combobox", ...baseInputProps, value: stringValue, onChange: (v) => onChange(v === "" ? null : v) }), createDialog] }));
279
- }
280
- function inputId(field) {
281
- return `kumiko-edit-${field.field}`;
282
- }
283
- // Doesn't resolve the `string` (cross-feature QN) FieldRenderer variant — the
284
- // list-column path (DataTableCell) doesn't either, so this stays in parity.
285
- function FieldRendererOutput({ field, renderer, row, appLocale, }) {
286
- const { Text, JsonView, FileValueList } = usePrimitives();
287
- const t = useTranslation();
288
- const valueTestId = `field-value-${field.field}`;
289
- const componentName = !isFormatSpec(renderer) && typeof renderer === "object" && renderer !== null
290
- ? extensionSectionName(renderer)
291
- : undefined;
292
- const Component = useColumnRenderer(componentName);
293
- if (isFormatSpec(renderer)) {
294
- // format:"json" wants the raw value (JsonView stringifies + highlights
295
- // itself) — applyFormatSpec's already-indented string is only the
296
- // fallback for primitives-providers without JsonView.
297
- if (renderer.format === "json" && JsonView !== undefined) {
298
- return _jsx(JsonView, { value: field.value, indent: renderer.indent, testId: valueTestId });
299
- }
300
- if (renderer.format === "file" && FileValueList !== undefined) {
301
- const fileIds = fileIdsOf(field.value);
302
- if (fileIds.length === 0)
303
- return _jsx(Text, { testId: valueTestId, children: EMPTY_VALUE_PLACEHOLDER });
304
- return (_jsx(FileValueList, { fileIds: fileIds, previewOpenByDefault: renderer.previewOpenByDefault === true, testId: valueTestId }));
305
- }
306
- // App locale as default when the FormatSpec declares none of its own —
307
- // otherwise locale-sensitive formats (timestamp/date/number/decimal/
308
- // bigInt/unit) fell back to Intl's runtime default instead of the app
309
- // language chosen via LocaleProvider. Prefer renderer.locale
310
- // when set; coalesce undefined (spread override) back to appLocale.
311
- return (_jsx(Text, { testId: valueTestId, children: applyFormatSpec({
312
- ...renderer,
313
- locale: stringPropertyOf(renderer, "locale") ?? appLocale,
314
- }, field.value, t) }));
315
- }
316
- if (componentName !== undefined) {
317
- if (Component !== undefined) {
318
- return (_jsx(Component, { value: field.value, row: row ?? { [field.field]: field.value }, column: { field: field.field } }));
319
- }
320
- rendererWarn(`[kumiko] fieldRenderer "${componentName}" not registered`);
321
- }
322
- return _jsx(Text, { testId: valueTestId, children: stringValue(field.value) });
323
- }
324
- // Read-only text for `type: "reference"` — resolves the referenced row's
325
- // label via the same lookup query as ReferenceInput's combobox, but shows
326
- // plain text instead of mounting an (unusable, disabled) combobox.
327
- function ReadOnlyReferenceValue({ field, featureName, }) {
328
- const { Text, Link } = usePrimitives();
329
- const t = useTranslation();
330
- const resolveReferenceLink = useReferenceLinkResolver();
331
- const refEntity = field.refEntity ?? "";
332
- const refFeature = field.refFeature ?? featureName;
333
- const isMultiple = field.refMultiple === true;
334
- const { queryQn, labelKey } = referenceOptionSource(field, refFeature, refEntity);
335
- const queryResult = useQuery(queryQn, {
336
- limit: REFERENCE_COMBOBOX_LIMIT,
52
+ readOnlyText,
53
+ featureName,
54
+ allIssues,
55
+ changed,
56
+ fillDisabled,
57
+ ...(row !== undefined && { row }),
337
58
  });
338
- const ids = isMultiple
339
- ? stringArrayOf(field.value)
340
- : typeof field.value === "string" && field.value !== ""
341
- ? [field.value]
342
- : [];
343
- if (ids.length === 0)
344
- return _jsx(Text, { testId: `field-value-${field.field}`, children: EMPTY_VALUE_PLACEHOLDER });
345
- const rows = queryResult.data?.rows ?? [];
346
- // System-scope ids (e.g. SYSTEM_TENANT_ID) never have a backing row —
347
- // same central fallback as useReferenceLookup (render-list.tsx), so a
348
- // reference field showing one on the detail path also gets a label
349
- // instead of the raw id.
350
- const systemLabel = SYSTEM_REFERENCE_LABELS[`${refFeature}:${refEntity}`];
351
- const parts = ids.map((id) => {
352
- if (systemLabel !== undefined && (id === systemLabel.id || systemLabel.aliases?.includes(id))) {
353
- return { id, label: t(systemLabel.labelKey), link: undefined };
354
- }
355
- const row = rows.find((r) => String(r["id"] ?? "") === id);
356
- if (row === undefined) {
357
- const unknownKey = UNRESOLVED_REFERENCE_LABEL_KEYS[`${refFeature}:${refEntity}`];
358
- const label = unknownKey !== undefined &&
359
- !queryResult.loading &&
360
- queryResult.error === null &&
361
- isCompleteRowPage(queryResult.data)
362
- ? t(unknownKey)
363
- : id;
364
- return { id, label, link: undefined };
365
- }
366
- return { id, label: String(row[labelKey] ?? id), link: resolveReferenceLink(refEntity, id) };
367
- });
368
- return (_jsx(Text, { testId: `field-value-${field.field}`, children: parts.map(({ id, label, link }, index) => (_jsxs(Fragment, { children: [index > 0 && ", ", link === undefined ? (label) : (_jsx(Link, { href: link.href, onPress: link.onPress, testId: `field-link-${field.field}-${id}`, children: label }))] }, id))) }));
369
- }
370
- // Field types whose read display already isn't a boxed "disabled input" look
371
- // (embedded/jsonb render as an info Banner) — `readOnlyDisplayText` doesn't
372
- // cover these, they keep going through `renderInput`'s existing Banner branch
373
- // in text-display mode too.
374
- function isComplexFieldType(type) {
375
- return type === "embedded" || type === "jsonb";
376
- }
377
- // Read-only file fields keep their (disabled) widget: it shows a thumbnail or
378
- // the file name, where plain text could only show the FileRef UUID.
379
- function isFileFieldType(type) {
380
- return type === "file" || type === "image" || type === "files" || type === "images";
381
- }
382
- // Mirrors ACTION_ICON_BY_ID (kumiko-screen.tsx) for fields: only names with
383
- // an unambiguous matching FieldIconKey are listed here, everything else
384
- // stays iconless rather than guessing.
385
- const FIELD_ICON_BY_NAME = {
386
- email: "mail",
387
- phone: "phone",
388
- tel: "phone",
389
- mobile: "phone",
390
- url: "link",
391
- website: "link",
392
- link: "link",
393
- password: "lock",
394
- search: "search",
395
- city: "map-pin",
396
- address: "map-pin",
397
- street: "map-pin",
398
- user: "user",
399
- owner: "user",
400
- assignee: "user",
401
- };
402
- function kebabLastSegment(value) {
403
- const kebab = toKebab(value);
404
- const lastDash = kebab.lastIndexOf("-");
405
- return lastDash === -1 ? kebab : kebab.slice(lastDash + 1);
406
- }
407
- // Derives a prefix icon when the field declares none: field.icon wins, then
408
- // the field name. Deliberately no type-based fallback — a hash in front of a
409
- // number field restates what the field already shows and reads as noise.
410
- function resolveFieldIcon(field) {
411
- if (field.icon !== undefined)
412
- return field.icon;
413
- const byName = FIELD_ICON_BY_NAME[field.field.toLowerCase()] ??
414
- FIELD_ICON_BY_NAME[kebabLastSegment(field.field)];
415
- return byName;
416
- }
417
- // Dispatches field.type → Input-kind. Select threads options through
418
- // from the EditFieldViewModel (computeEditViewModel pulls them from
419
- // SelectFieldDef.options). Structural types without a widget (embedded,
420
- // jsonb, multiSelect) render read-only instead of a data-destroying text
421
- // fallback; truly unknown scalar types still fall back to text.
422
- function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner, Text, JsonView, t, row, fill, }) {
423
- const common = {
424
- id,
425
- name: field.field,
426
- disabled: field.readOnly,
427
- required: field.required,
428
- hasError,
429
- };
430
- const placeholderProp = field.placeholder !== undefined ? { placeholder: field.placeholder } : {};
431
- const prefix = resolveAffix(field.prefix, row);
432
- const suffix = resolveAffix(field.suffix, row);
433
- const affixProps = {
434
- ...(prefix !== undefined && { prefix }),
435
- ...(suffix !== undefined && { suffix }),
436
- ...(fill !== undefined && { fill }),
437
- };
438
- switch (field.type) {
439
- // decimal/bigInt are both plain numbers on the wire (fieldToZod:
440
- // z.number() / z.number().int().safe()) — the number input's onChange
441
- // already emits `number | undefined`, so no extra coercion is needed
442
- // beyond what "number" already does.
443
- case "number":
444
- case "bigInt": {
445
- const icon = resolveFieldIcon(field);
446
- const unit = resolveNumberUnit(field.unit, row);
447
- return (_jsx(Input, { kind: "number", ...common, value: numberValue(field.value), onChange: (v) => onChange(v), locale: appLocale, ...(field.grouping !== undefined && { grouping: field.grouping }), ...(field.type === "bigInt" && { integer: true }), ...placeholderProp, ...affixProps, ...(icon !== undefined && { icon }), ...(unit !== undefined && { unit }) }));
448
- }
449
- case "decimal": {
450
- const icon = resolveFieldIcon(field);
451
- return (_jsx(Input, { kind: "number", ...common, value: numberValue(field.value), onChange: (v) => onChange(v), locale: appLocale, ...(field.grouping !== undefined && { grouping: field.grouping }), ...placeholderProp, ...affixProps, ...(icon !== undefined && { icon }) }));
452
- }
453
- case "tz":
454
- return (_jsx(Input, { kind: "tz", ...common, value: stringValue(field.value), onChange: (v) => onChange(v) }));
455
- case "multiSelect": {
456
- const arrayValue = stringArrayOf(field.value);
457
- const renderMultiSelect = (options) => field.display === "checkboxes" ? (_jsx(MultiSelectCheckboxes, { field: field, id: id, options: options, value: arrayValue, onChange: (v) => onChange(v) })) : (_jsx(Input, { kind: "combobox", ...common, multiple: true, value: arrayValue, onChange: (v) => onChange(withPinnedValues(v, options, arrayValue)), options: options }));
458
- if (field.selectOptionsAvailabilityQuery !== undefined && field.readOnly !== true) {
459
- return (_jsx(AvailabilityOptionsMultiSelect, { field: field, query: field.selectOptionsAvailabilityQuery.query, initialValues: arrayValue, children: renderMultiSelect }));
460
- }
461
- return renderMultiSelect(staticSelectOptions(field));
462
- }
463
- case "money": {
464
- const currency = resolveMoneyCurrency(field.value, field.currency);
465
- return (_jsx(Input, { kind: "money", ...common, value: moneyMinorValue(field.value, currency), onChange: (v) => onChange(moneyPayload(v, currency)), currency: currency, locale: appLocale }));
466
- }
467
- case "boolean":
468
- return (_jsx(Input, { kind: "boolean", ...common, value: field.value === true, onChange: (v) => onChange(v) }));
469
- case "date":
470
- return (_jsx(Input, { kind: "date", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), locale: field.dateLocale ?? appLocale, ...(field.min !== undefined && { min: field.min }), ...(field.max !== undefined && { max: field.max }) }));
471
- case "timestamp":
472
- return (_jsx(Input, { kind: "timestamp", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), locale: field.dateLocale ?? appLocale, ...(field.wallClock !== undefined && { wallClock: field.wallClock }), ...(field.min !== undefined && { min: field.min }), ...(field.max !== undefined && { max: field.max }) }));
473
- case "locatedTimestamp":
474
- return (_jsx(Input, { kind: "locatedTimestamp", ...common, value: locatedValue(field.value), onChange: (v) => onChange(v), locale: field.dateLocale ?? appLocale, ...(field.min !== undefined && { min: field.min }), ...(field.max !== undefined && { max: field.max }) }));
475
- case "select": {
476
- // Translated option labels come from the ViewModel builder
477
- // (computeEditViewModel, convention key
478
- // `<feature>:entity:<entity>:field:<field>:option:<value>`).
479
- // If no translations are registered, fall back to the raw value as the
480
- // label, which ComboboxInput then shows unchanged.
481
- const selectOptions = field.optionLabels !== undefined ? staticSelectOptions(field) : (field.options ?? []);
482
- // `display` is shared with multiSelect's "checkboxes", which is not a
483
- // select presentation — narrow to the two a select field can request.
484
- const display = field.display === "radio" || field.display === "dropdown" ? field.display : undefined;
485
- return (_jsx(Input, { kind: "select", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), options: selectOptions, ...(display !== undefined && { display }) }));
486
- }
487
- case "file":
488
- case "image": {
489
- const kind = field.type === "image" ? "image" : "file";
490
- const fileId = typeof field.value === "string" && field.value !== "" ? field.value : null;
491
- return (_jsx(Input, { kind: kind, ...common, value: fileId, onChange: (v) => onChange(v), ...(field.accept !== undefined && { accept: field.accept }), ...(field.maxSize !== undefined && { maxSize: field.maxSize }), ...(field.entityType !== undefined && { entityType: field.entityType }), ...(field.uploadActionForm !== undefined && {
492
- uploadActionForm: field.uploadActionForm,
493
- }), ...(field.entityId !== undefined && { entityId: field.entityId }), ...(field.fieldName !== undefined && { fieldName: field.fieldName }), ...(field.imageVariant !== undefined && { imageVariant: field.imageVariant }), ...(field.capture !== undefined && { capture: field.capture }) }));
494
- }
495
- case "files":
496
- case "images":
497
- return (_jsx(FileListField, { field: field, id: id, kind: field.type === "images" ? "image" : "file", hasError: hasError, onChange: (ids) => onChange(ids) }));
498
- // embedded (without embeddedListCells — that's embeddedList, which has
499
- // had its own EmbeddedListField widget) and jsonb carry
500
- // arbitrary objects. Without a dedicated widget these must NOT fall
501
- // through to a text input: stringValue() turns them into
502
- // "[object Object]" and saving that overwrites the real data with the
503
- // mangled string. A `required: true` on either is caught loudly
504
- // at boot (validateNoWidgetRequiredField in the framework package)
505
- // instead of silently failing here.
506
- case "embedded":
507
- case "jsonb": {
508
- const hasValue = field.value !== undefined && field.value !== null && field.value !== "";
509
- return (_jsxs(Banner, { id: id, variant: "info", children: [t("kumiko.field.unsupported"), hasValue &&
510
- (JsonView !== undefined ? (_jsx(JsonView, { value: field.value, testId: `field-value-${field.field}` })) : (_jsx(Text, { variant: "code", children: JSON.stringify(field.value) })))] }));
511
- }
512
- default: {
513
- if (field.type === "text" && field.format === "password") {
514
- return (_jsx(Input, { kind: "password", ...common, value: stringValue(field.value), onChange: (v) => onChange(v) }));
515
- }
516
- if (field.type === "text" && field.format === "month") {
517
- return (_jsx(Input, { kind: "month", ...common, value: stringValue(field.value), onChange: (v) => onChange(v) }));
518
- }
519
- // text + unknown scalar type → text input. If TextFieldDef.multiline
520
- // is set (the view-model carries it), the renderer switches to
521
- // textarea. longText always renders a textarea — that's the point of
522
- // the type — regardless of whether `multiline` is set; `multiline`
523
- // only supplies an optional `{ rows }` override for it.
524
- if (field.type === "longText" || (field.type === "text" && field.multiline)) {
525
- const rows = typeof field.multiline === "object" ? field.multiline.rows : undefined;
526
- return (_jsx(Input, { kind: "textarea", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), ...(rows !== undefined && { rows }), ...placeholderProp }));
527
- }
528
- const icon = resolveFieldIcon(field);
529
- return (_jsx(Input, { kind: "text", ...common, value: stringValue(field.value), onChange: (v) => onChange(v), ...placeholderProp, ...affixProps, ...(icon !== undefined && { icon }), ...(field.autoComplete !== undefined && { autoComplete: field.autoComplete }) }));
530
- }
531
- }
532
- }
533
- function stringValue(v) {
534
- if (v === undefined || v === null)
535
- return "";
536
- return typeof v === "string" ? v : String(v);
537
- }
538
- function numberValue(v) {
539
- if (v === undefined || v === null || v === "")
540
- return "";
541
- return typeof v === "number" ? v : Number(v);
542
- }
543
- // Read/initial value → MoneyInput's minor-units contract. The server's
544
- // read shape is `{amount, currency, amountMinor}` in MAJOR units — deliberately
545
- // NOT reading `amountMinor` here: the server derives it via a flat
546
- // MINOR_UNIT_SCALE=100 (money.ts), which disagrees with currencyDecimals for
547
- // zero-decimal currencies like JPY, so it would double-scale JPY amounts.
548
- // Deriving from `amount * 10**currencyDecimals(currency)` keeps this the
549
- // exact inverse of moneyPayload below. A bare number (e.g. ConfigEditBody's
550
- // stored-config coercion) is MAJOR units too — every producer in this repo
551
- // hands rehydrateMoney's `{amount,…}` shape or a raw major-unit number, never
552
- // pre-scaled minor units.
553
- // A stored row can carry a legacy/imported currency that is not an ISO-4217
554
- // shape; Intl.NumberFormat throws a RangeError on it and would crash the screen.
555
- const ISO_CURRENCY_CODE = /^[A-Z]{3}$/;
556
- function resolveMoneyCurrency(value, fieldCurrency) {
557
- if (typeof value === "object" && value !== null && "currency" in value) {
558
- const { currency } = value;
559
- if (typeof currency === "string") {
560
- const normalized = currency.toUpperCase();
561
- if (ISO_CURRENCY_CODE.test(normalized))
562
- return normalized;
563
- }
564
- }
565
- return fieldCurrency ?? "EUR";
566
- }
567
- // The suffix span is absolutely positioned inside a fixed right padding sized
568
- // for short symbols; a free-text sibling value longer than any real unit
569
- // symbol would overflow the number.
570
- const MAX_SIBLING_UNIT_LENGTH = 8;
571
- // Static unit → used as-is. Sibling-field reference → read the live row
572
- // value; missing/empty/non-string/overlong sibling means no suffix, never a guess.
573
- function resolveNumberUnit(unit, row) {
574
- if (unit === undefined)
575
- return undefined;
576
- if (typeof unit === "string")
577
- return unit;
578
- const sibling = row?.[unit.field];
579
- if (typeof sibling !== "string")
580
- return undefined;
581
- const trimmed = sibling.trim();
582
- return trimmed.length > 0 && trimmed.length <= MAX_SIBLING_UNIT_LENGTH ? trimmed : undefined;
583
- }
584
- // String affix → used as-is (the view model already translated it). Field
585
- // affix → the live form value; missing, empty or non-scalar means no affix.
586
- function resolveAffix(affix, row) {
587
- if (affix === undefined)
588
- return undefined;
589
- if (typeof affix === "string")
590
- return affix;
591
- const value = row?.[affix.field];
592
- const text = typeof value === "number" ? String(value) : value;
593
- return typeof text === "string" && text.trim().length > 0 ? text.trim() : undefined;
594
- }
595
- function stringArrayOf(value) {
596
- return Array.isArray(value)
597
- ? value.filter((item) => typeof item === "string")
598
- : [];
599
- }
600
- // The combobox cannot disable a single option, so removals of disabled options
601
- // the form already holds are merged back.
602
- function withPinnedValues(next, options, current) {
603
- const missingPinned = options
604
- .filter((opt) => opt.disabled === true && current.includes(opt.value) && !next.includes(opt.value))
605
- .map((opt) => opt.value);
606
- return missingPinned.length === 0 ? next : [...next, ...missingPinned];
607
- }
608
- function stringPropertyOf(value, key) {
609
- const property = propertyOf(value, key);
610
- return typeof property === "string" ? property : undefined;
611
- }
612
- function moneyMinorValue(v, currency) {
613
- if (v === undefined || v === null || v === "")
614
- return "";
615
- if (typeof v === "number")
616
- return Math.round(v * 10 ** currencyDecimals(currency));
617
- if (typeof v === "object") {
618
- const amount = propertyOf(v, "amount");
619
- if (typeof amount === "number")
620
- return Math.round(amount * 10 ** currencyDecimals(currency));
621
- }
622
- return "";
623
- }
624
- // MoneyInput's minor-units onChange → the server payload shape
625
- // (`z.object({amount, currency})`, schema-builder.ts) — MAJOR units, no
626
- // `amountMinor` (the server derives that itself on write).
627
- function moneyPayload(minorUnits, currency) {
628
- if (minorUnits === undefined)
629
- return undefined;
630
- return { amount: minorUnits / 10 ** currencyDecimals(currency), currency };
631
- }
632
- // A locatedTimestamp field value is supplied by the read wrapper as
633
- // `{ at, tz, utc }`; an unset value becomes "" as the empty sentinel,
634
- // consistent with money and timestamp fields.
635
- function locatedValue(v) {
636
- if (v !== null && typeof v === "object" && "at" in v && "tz" in v) {
637
- const utc = propertyOf(v, "utc");
638
- return {
639
- at: typeof v.at === "string" ? v.at : "",
640
- tz: typeof v.tz === "string" ? v.tz : "",
641
- ...(typeof utc === "string" && { utc }),
642
- };
643
- }
644
- return "";
645
- }
646
- // Read-only text for a `field.readOnly` field without its own `renderer` —
647
- // per-type formatting that reuses the same value-shaping helpers as the
648
- // editable widgets above, so a text value and its would-be Input widget stay
649
- // derived from the identical parse. `type: "reference"` isn't
650
- // covered here — it needs a live label lookup, see ReadOnlyReferenceValue.
651
- // `isComplexFieldType` types aren't covered either — callers keep those on
652
- // `renderInput`'s existing Banner fallback.
653
- function readOnlyDisplayText(field, appLocale, t) {
654
- const { type, value } = field;
655
- if (value === undefined || value === null || value === "")
656
- return EMPTY_VALUE_PLACEHOLDER;
657
- if (type === "text" && field.writeOnly === true)
658
- return t("kumiko.field.writeOnly.set");
659
- if (type === "text" && field.format === "password")
660
- return "••••••••";
661
- switch (type) {
662
- case "boolean":
663
- return applyFormatSpec({ format: "boolean" }, value);
664
- case "date":
665
- return applyFormatSpec({ format: "date", locale: field.dateLocale ?? appLocale }, value);
666
- case "timestamp":
667
- return applyFormatSpec({ format: "timestamp", locale: field.dateLocale ?? appLocale }, value);
668
- case "locatedTimestamp": {
669
- const located = locatedValue(value);
670
- if (located === "" || located.at === "")
671
- return EMPTY_VALUE_PLACEHOLDER;
672
- return applyFormatSpec({ format: "timestamp", locale: field.dateLocale ?? appLocale }, located.utc ?? located.at);
673
- }
674
- case "number":
675
- case "bigInt":
676
- case "decimal": {
677
- const n = numberValue(value);
678
- return n === ""
679
- ? EMPTY_VALUE_PLACEHOLDER
680
- : new Intl.NumberFormat(appLocale, { useGrouping: field.grouping ?? true }).format(n);
681
- }
682
- case "money": {
683
- const currency = resolveMoneyCurrency(value, field.currency);
684
- const minor = moneyMinorValue(value, currency);
685
- if (minor === "")
686
- return EMPTY_VALUE_PLACEHOLDER;
687
- const major = minor / 10 ** currencyDecimals(currency);
688
- return new Intl.NumberFormat(appLocale, { style: "currency", currency }).format(major);
689
- }
690
- case "select":
691
- case "multiSelect": {
692
- const labels = field.optionLabels;
693
- const values = Array.isArray(value) ? value : [value];
694
- if (values.length === 0)
695
- return EMPTY_VALUE_PLACEHOLDER;
696
- return values
697
- .map((v) => (typeof v === "string" ? (labels?.[v] ?? v) : stringValue(v)))
698
- .join(", ");
699
- }
700
- default:
701
- return stringValue(value);
702
- }
59
+ return (_jsx(Field, { id: id, label: field.label, required: field.required && field.type !== "boolean" && !allFieldsRequired, ...fieldOptionalProps({
60
+ issues,
61
+ printsIssues: !listPrintsOwnIssues(field),
62
+ labelAppendix,
63
+ fieldAppendix,
64
+ appendixPlacement,
65
+ description,
66
+ status,
67
+ changed,
68
+ layout,
69
+ accent,
70
+ hideLabel,
71
+ }), testId: `field-${field.field}`, children: control }));
703
72
  }