@freighttech/ui 0.13.18

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 (1477) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/dist/backend/AppShell.js +1289 -0
  4. package/dist/backend/AppShell.js.map +7 -0
  5. package/dist/backend/BackendChromeProvider.js +83 -0
  6. package/dist/backend/BackendChromeProvider.js.map +7 -0
  7. package/dist/backend/BackendPageSurface.js +19 -0
  8. package/dist/backend/BackendPageSurface.js.map +7 -0
  9. package/dist/backend/CollapsibleNavSection.js +149 -0
  10. package/dist/backend/CollapsibleNavSection.js.map +7 -0
  11. package/dist/backend/ContextHelp.js +33 -0
  12. package/dist/backend/ContextHelp.js.map +7 -0
  13. package/dist/backend/CrudForm.js +3163 -0
  14. package/dist/backend/CrudForm.js.map +7 -0
  15. package/dist/backend/DataTable.js +2091 -0
  16. package/dist/backend/DataTable.js.map +7 -0
  17. package/dist/backend/EmptyState.js +52 -0
  18. package/dist/backend/EmptyState.js.map +7 -0
  19. package/dist/backend/FilterBar.js +147 -0
  20. package/dist/backend/FilterBar.js.map +7 -0
  21. package/dist/backend/FilterOverlay.js +337 -0
  22. package/dist/backend/FilterOverlay.js.map +7 -0
  23. package/dist/backend/FlashMessages.js +158 -0
  24. package/dist/backend/FlashMessages.js.map +7 -0
  25. package/dist/backend/HtmlRichTextEditor.js +129 -0
  26. package/dist/backend/HtmlRichTextEditor.js.map +7 -0
  27. package/dist/backend/IntegrationsButton.js +25 -0
  28. package/dist/backend/IntegrationsButton.js.map +7 -0
  29. package/dist/backend/JsonBuilder.js +323 -0
  30. package/dist/backend/JsonBuilder.js.map +7 -0
  31. package/dist/backend/JsonDisplay.js +207 -0
  32. package/dist/backend/JsonDisplay.js.map +7 -0
  33. package/dist/backend/NextStepCallout.js +79 -0
  34. package/dist/backend/NextStepCallout.js.map +7 -0
  35. package/dist/backend/Page.js +27 -0
  36. package/dist/backend/Page.js.map +7 -0
  37. package/dist/backend/PerspectiveSidebar.js +456 -0
  38. package/dist/backend/PerspectiveSidebar.js.map +7 -0
  39. package/dist/backend/ProfileDropdown.js +341 -0
  40. package/dist/backend/ProfileDropdown.js.map +7 -0
  41. package/dist/backend/RowActions.js +155 -0
  42. package/dist/backend/RowActions.js.map +7 -0
  43. package/dist/backend/SectionHeader.js +74 -0
  44. package/dist/backend/SectionHeader.js.map +7 -0
  45. package/dist/backend/SettingsButton.js +25 -0
  46. package/dist/backend/SettingsButton.js.map +7 -0
  47. package/dist/backend/Sidebar.js +510 -0
  48. package/dist/backend/Sidebar.js.map +7 -0
  49. package/dist/backend/Sidebar.stories.js +166 -0
  50. package/dist/backend/Sidebar.stories.js.map +7 -0
  51. package/dist/backend/TruncatedCell.js +92 -0
  52. package/dist/backend/TruncatedCell.js.map +7 -0
  53. package/dist/backend/UserMenu.js +146 -0
  54. package/dist/backend/UserMenu.js.map +7 -0
  55. package/dist/backend/ValueIcons.js +34 -0
  56. package/dist/backend/ValueIcons.js.map +7 -0
  57. package/dist/backend/WebhookSetupGuide.js +79 -0
  58. package/dist/backend/WebhookSetupGuide.js.map +7 -0
  59. package/dist/backend/charts/BarChart.js +154 -0
  60. package/dist/backend/charts/BarChart.js.map +7 -0
  61. package/dist/backend/charts/ChartUtils.js +141 -0
  62. package/dist/backend/charts/ChartUtils.js.map +7 -0
  63. package/dist/backend/charts/ComboChart.js +261 -0
  64. package/dist/backend/charts/ComboChart.js.map +7 -0
  65. package/dist/backend/charts/ComboChart.stories.js +88 -0
  66. package/dist/backend/charts/ComboChart.stories.js.map +7 -0
  67. package/dist/backend/charts/KpiCard.js +96 -0
  68. package/dist/backend/charts/KpiCard.js.map +7 -0
  69. package/dist/backend/charts/LineChart.js +165 -0
  70. package/dist/backend/charts/LineChart.js.map +7 -0
  71. package/dist/backend/charts/PieChart.js +121 -0
  72. package/dist/backend/charts/PieChart.js.map +7 -0
  73. package/dist/backend/charts/ScatterChart.js +320 -0
  74. package/dist/backend/charts/ScatterChart.js.map +7 -0
  75. package/dist/backend/charts/TopNTable.js +91 -0
  76. package/dist/backend/charts/TopNTable.js.map +7 -0
  77. package/dist/backend/charts/WaterfallChart.js +136 -0
  78. package/dist/backend/charts/WaterfallChart.js.map +7 -0
  79. package/dist/backend/charts/index.js +30 -0
  80. package/dist/backend/charts/index.js.map +7 -0
  81. package/dist/backend/columns/ColumnChooserPanel.js +244 -0
  82. package/dist/backend/columns/ColumnChooserPanel.js.map +7 -0
  83. package/dist/backend/confirm-dialog/ConfirmDialog.js +214 -0
  84. package/dist/backend/confirm-dialog/ConfirmDialog.js.map +7 -0
  85. package/dist/backend/confirm-dialog/index.js +7 -0
  86. package/dist/backend/confirm-dialog/index.js.map +7 -0
  87. package/dist/backend/confirm-dialog/useConfirmDialog.js +113 -0
  88. package/dist/backend/confirm-dialog/useConfirmDialog.js.map +7 -0
  89. package/dist/backend/custom-fields/FieldDefinitionsEditor.js +1352 -0
  90. package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +7 -0
  91. package/dist/backend/custom-fields/FieldDefinitionsManager.js +332 -0
  92. package/dist/backend/custom-fields/FieldDefinitionsManager.js.map +7 -0
  93. package/dist/backend/dashboard/DashboardScreen.js +598 -0
  94. package/dist/backend/dashboard/DashboardScreen.js.map +7 -0
  95. package/dist/backend/dashboard/index.js +5 -0
  96. package/dist/backend/dashboard/index.js.map +7 -0
  97. package/dist/backend/dashboard/widgetRegistry.js +55 -0
  98. package/dist/backend/dashboard/widgetRegistry.js.map +7 -0
  99. package/dist/backend/date-range/DateRangeSelect.js +39 -0
  100. package/dist/backend/date-range/DateRangeSelect.js.map +7 -0
  101. package/dist/backend/date-range/InlineDateRangeSelect.js +42 -0
  102. package/dist/backend/date-range/InlineDateRangeSelect.js.map +7 -0
  103. package/dist/backend/date-range/InlineGranularitySelect.js +40 -0
  104. package/dist/backend/date-range/InlineGranularitySelect.js.map +7 -0
  105. package/dist/backend/date-range/dateRanges.js +176 -0
  106. package/dist/backend/date-range/dateRanges.js.map +7 -0
  107. package/dist/backend/date-range/index.js +25 -0
  108. package/dist/backend/date-range/index.js.map +7 -0
  109. package/dist/backend/detail/AccessDeniedMessage.js +28 -0
  110. package/dist/backend/detail/AccessDeniedMessage.js.map +7 -0
  111. package/dist/backend/detail/ActivitiesSection.js +947 -0
  112. package/dist/backend/detail/ActivitiesSection.js.map +7 -0
  113. package/dist/backend/detail/AddressEditor.js +417 -0
  114. package/dist/backend/detail/AddressEditor.js.map +7 -0
  115. package/dist/backend/detail/AddressTiles.js +441 -0
  116. package/dist/backend/detail/AddressTiles.js.map +7 -0
  117. package/dist/backend/detail/AddressesSection.js +274 -0
  118. package/dist/backend/detail/AddressesSection.js.map +7 -0
  119. package/dist/backend/detail/AttachmentDeleteDialog.js +41 -0
  120. package/dist/backend/detail/AttachmentDeleteDialog.js.map +7 -0
  121. package/dist/backend/detail/AttachmentMetadataDialog.js +519 -0
  122. package/dist/backend/detail/AttachmentMetadataDialog.js.map +7 -0
  123. package/dist/backend/detail/AttachmentVisualPreview.js +132 -0
  124. package/dist/backend/detail/AttachmentVisualPreview.js.map +7 -0
  125. package/dist/backend/detail/AttachmentsSection.js +281 -0
  126. package/dist/backend/detail/AttachmentsSection.js.map +7 -0
  127. package/dist/backend/detail/CustomDataSection.js +575 -0
  128. package/dist/backend/detail/CustomDataSection.js.map +7 -0
  129. package/dist/backend/detail/DetailFieldsSection.js +85 -0
  130. package/dist/backend/detail/DetailFieldsSection.js.map +7 -0
  131. package/dist/backend/detail/ErrorMessage.js +28 -0
  132. package/dist/backend/detail/ErrorMessage.js.map +7 -0
  133. package/dist/backend/detail/InlineEditors.js +697 -0
  134. package/dist/backend/detail/InlineEditors.js.map +7 -0
  135. package/dist/backend/detail/LoadingMessage.js +14 -0
  136. package/dist/backend/detail/LoadingMessage.js.map +7 -0
  137. package/dist/backend/detail/NotesSection.js +1022 -0
  138. package/dist/backend/detail/NotesSection.js.map +7 -0
  139. package/dist/backend/detail/TabEmptyState.js +25 -0
  140. package/dist/backend/detail/TabEmptyState.js.map +7 -0
  141. package/dist/backend/detail/TagsSection.js +375 -0
  142. package/dist/backend/detail/TagsSection.js.map +7 -0
  143. package/dist/backend/detail/addressFormat.js +77 -0
  144. package/dist/backend/detail/addressFormat.js.map +7 -0
  145. package/dist/backend/detail/index.js +36 -0
  146. package/dist/backend/detail/index.js.map +7 -0
  147. package/dist/backend/devtools/UmesDevToolsPanel.js +109 -0
  148. package/dist/backend/devtools/UmesDevToolsPanel.js.map +7 -0
  149. package/dist/backend/devtools/components/ConflictWarnings.js +32 -0
  150. package/dist/backend/devtools/components/ConflictWarnings.js.map +7 -0
  151. package/dist/backend/devtools/components/EnricherTiming.js +46 -0
  152. package/dist/backend/devtools/components/EnricherTiming.js.map +7 -0
  153. package/dist/backend/devtools/components/EventFlow.js +34 -0
  154. package/dist/backend/devtools/components/EventFlow.js.map +7 -0
  155. package/dist/backend/devtools/components/ExtensionPointList.js +58 -0
  156. package/dist/backend/devtools/components/ExtensionPointList.js.map +7 -0
  157. package/dist/backend/devtools/components/InterceptorActivity.js +36 -0
  158. package/dist/backend/devtools/components/InterceptorActivity.js.map +7 -0
  159. package/dist/backend/devtools/index.js +7 -0
  160. package/dist/backend/devtools/index.js.map +7 -0
  161. package/dist/backend/devtools/useUmesDevTools.js +125 -0
  162. package/dist/backend/devtools/useUmesDevTools.js.map +7 -0
  163. package/dist/backend/document-viewer/RegionOverlay.js +92 -0
  164. package/dist/backend/document-viewer/RegionOverlay.js.map +7 -0
  165. package/dist/backend/document-viewer/index.js +6 -0
  166. package/dist/backend/document-viewer/index.js.map +7 -0
  167. package/dist/backend/dynamic-grid/DynamicGrid.js +130 -0
  168. package/dist/backend/dynamic-grid/DynamicGrid.js.map +7 -0
  169. package/dist/backend/dynamic-grid/adapters.js +34 -0
  170. package/dist/backend/dynamic-grid/adapters.js.map +7 -0
  171. package/dist/backend/dynamic-grid/components/GridCell.js +149 -0
  172. package/dist/backend/dynamic-grid/components/GridCell.js.map +7 -0
  173. package/dist/backend/dynamic-grid/components/GridRow.js +32 -0
  174. package/dist/backend/dynamic-grid/components/GridRow.js.map +7 -0
  175. package/dist/backend/dynamic-grid/hooks/useGridKeyboardNav.js +115 -0
  176. package/dist/backend/dynamic-grid/hooks/useGridKeyboardNav.js.map +7 -0
  177. package/dist/backend/dynamic-grid/index.js +9 -0
  178. package/dist/backend/dynamic-grid/index.js.map +7 -0
  179. package/dist/backend/dynamic-grid/types.js +1 -0
  180. package/dist/backend/dynamic-grid/types.js.map +7 -0
  181. package/dist/backend/dynamic-table/DynamicTable.js +2607 -0
  182. package/dist/backend/dynamic-table/DynamicTable.js.map +7 -0
  183. package/dist/backend/dynamic-table/DynamicTable.stories.js +233 -0
  184. package/dist/backend/dynamic-table/DynamicTable.stories.js.map +7 -0
  185. package/dist/backend/dynamic-table/components/ActiveFilterChips.js +60 -0
  186. package/dist/backend/dynamic-table/components/ActiveFilterChips.js.map +7 -0
  187. package/dist/backend/dynamic-table/components/BulkActionsBar.js +58 -0
  188. package/dist/backend/dynamic-table/components/BulkActionsBar.js.map +7 -0
  189. package/dist/backend/dynamic-table/components/Cell.js +155 -0
  190. package/dist/backend/dynamic-table/components/Cell.js.map +7 -0
  191. package/dist/backend/dynamic-table/components/CellCommentDialog.js +726 -0
  192. package/dist/backend/dynamic-table/components/CellCommentDialog.js.map +7 -0
  193. package/dist/backend/dynamic-table/components/CellCommentHoverPopup.js +77 -0
  194. package/dist/backend/dynamic-table/components/CellCommentHoverPopup.js.map +7 -0
  195. package/dist/backend/dynamic-table/components/ColumnFilterPopover.js +485 -0
  196. package/dist/backend/dynamic-table/components/ColumnFilterPopover.js.map +7 -0
  197. package/dist/backend/dynamic-table/components/ColumnHeaderMenu.js +173 -0
  198. package/dist/backend/dynamic-table/components/ColumnHeaderMenu.js.map +7 -0
  199. package/dist/backend/dynamic-table/components/ColumnHeaders.js +528 -0
  200. package/dist/backend/dynamic-table/components/ColumnHeaders.js.map +7 -0
  201. package/dist/backend/dynamic-table/components/CompactLabelCell.js +64 -0
  202. package/dist/backend/dynamic-table/components/CompactLabelCell.js.map +7 -0
  203. package/dist/backend/dynamic-table/components/ConfigureViewFields.js +139 -0
  204. package/dist/backend/dynamic-table/components/ConfigureViewFields.js.map +7 -0
  205. package/dist/backend/dynamic-table/components/ConfigureViewFilters.js +305 -0
  206. package/dist/backend/dynamic-table/components/ConfigureViewFilters.js.map +7 -0
  207. package/dist/backend/dynamic-table/components/ConfigureViewFormatting.js +169 -0
  208. package/dist/backend/dynamic-table/components/ConfigureViewFormatting.js.map +7 -0
  209. package/dist/backend/dynamic-table/components/ConfigureViewFormulas.js +298 -0
  210. package/dist/backend/dynamic-table/components/ConfigureViewFormulas.js.map +7 -0
  211. package/dist/backend/dynamic-table/components/ConfigureViewGrouping.js +224 -0
  212. package/dist/backend/dynamic-table/components/ConfigureViewGrouping.js.map +7 -0
  213. package/dist/backend/dynamic-table/components/ConfigureViewLinkedColumns.js +167 -0
  214. package/dist/backend/dynamic-table/components/ConfigureViewLinkedColumns.js.map +7 -0
  215. package/dist/backend/dynamic-table/components/ConfigureViewPanel.js +592 -0
  216. package/dist/backend/dynamic-table/components/ConfigureViewPanel.js.map +7 -0
  217. package/dist/backend/dynamic-table/components/ConfigureViewRollups.js +269 -0
  218. package/dist/backend/dynamic-table/components/ConfigureViewRollups.js.map +7 -0
  219. package/dist/backend/dynamic-table/components/ConfigureViewSorting.js +107 -0
  220. package/dist/backend/dynamic-table/components/ConfigureViewSorting.js.map +7 -0
  221. package/dist/backend/dynamic-table/components/ContextMenu.js +91 -0
  222. package/dist/backend/dynamic-table/components/ContextMenu.js.map +7 -0
  223. package/dist/backend/dynamic-table/components/DateCell.js +80 -0
  224. package/dist/backend/dynamic-table/components/DateCell.js.map +7 -0
  225. package/dist/backend/dynamic-table/components/Debugger.js +311 -0
  226. package/dist/backend/dynamic-table/components/Debugger.js.map +7 -0
  227. package/dist/backend/dynamic-table/components/DensityControl.js +289 -0
  228. package/dist/backend/dynamic-table/components/DensityControl.js.map +7 -0
  229. package/dist/backend/dynamic-table/components/EmptyState.js +42 -0
  230. package/dist/backend/dynamic-table/components/EmptyState.js.map +7 -0
  231. package/dist/backend/dynamic-table/components/EntitySearchEditor.js +369 -0
  232. package/dist/backend/dynamic-table/components/EntitySearchEditor.js.map +7 -0
  233. package/dist/backend/dynamic-table/components/ExportMenu.js +63 -0
  234. package/dist/backend/dynamic-table/components/ExportMenu.js.map +7 -0
  235. package/dist/backend/dynamic-table/components/FilterDatePicker.js +165 -0
  236. package/dist/backend/dynamic-table/components/FilterDatePicker.js.map +7 -0
  237. package/dist/backend/dynamic-table/components/FilterValueInput.js +200 -0
  238. package/dist/backend/dynamic-table/components/FilterValueInput.js.map +7 -0
  239. package/dist/backend/dynamic-table/components/FooterTotalsRow.js +165 -0
  240. package/dist/backend/dynamic-table/components/FooterTotalsRow.js.map +7 -0
  241. package/dist/backend/dynamic-table/components/FullscreenOverlay.js +51 -0
  242. package/dist/backend/dynamic-table/components/FullscreenOverlay.js.map +7 -0
  243. package/dist/backend/dynamic-table/components/GroupHeaderRow.js +108 -0
  244. package/dist/backend/dynamic-table/components/GroupHeaderRow.js.map +7 -0
  245. package/dist/backend/dynamic-table/components/GroupSummaryRow.js +134 -0
  246. package/dist/backend/dynamic-table/components/GroupSummaryRow.js.map +7 -0
  247. package/dist/backend/dynamic-table/components/ImportPanel.js +341 -0
  248. package/dist/backend/dynamic-table/components/ImportPanel.js.map +7 -0
  249. package/dist/backend/dynamic-table/components/ImportPreviewGrid.js +114 -0
  250. package/dist/backend/dynamic-table/components/ImportPreviewGrid.js.map +7 -0
  251. package/dist/backend/dynamic-table/components/LoadErrorState.js +23 -0
  252. package/dist/backend/dynamic-table/components/LoadErrorState.js.map +7 -0
  253. package/dist/backend/dynamic-table/components/MentionPopup.js +149 -0
  254. package/dist/backend/dynamic-table/components/MentionPopup.js.map +7 -0
  255. package/dist/backend/dynamic-table/components/PerspectiveDeleteDialog.js +57 -0
  256. package/dist/backend/dynamic-table/components/PerspectiveDeleteDialog.js.map +7 -0
  257. package/dist/backend/dynamic-table/components/PerspectivePublishDialog.js +100 -0
  258. package/dist/backend/dynamic-table/components/PerspectivePublishDialog.js.map +7 -0
  259. package/dist/backend/dynamic-table/components/PerspectiveTabMenu.js +159 -0
  260. package/dist/backend/dynamic-table/components/PerspectiveTabMenu.js.map +7 -0
  261. package/dist/backend/dynamic-table/components/PerspectiveTabs.js +319 -0
  262. package/dist/backend/dynamic-table/components/PerspectiveTabs.js.map +7 -0
  263. package/dist/backend/dynamic-table/components/PerspectiveTemplateMenu.js +90 -0
  264. package/dist/backend/dynamic-table/components/PerspectiveTemplateMenu.js.map +7 -0
  265. package/dist/backend/dynamic-table/components/RowHeaderCell.js +58 -0
  266. package/dist/backend/dynamic-table/components/RowHeaderCell.js.map +7 -0
  267. package/dist/backend/dynamic-table/components/SearchBar.js +221 -0
  268. package/dist/backend/dynamic-table/components/SearchBar.js.map +7 -0
  269. package/dist/backend/dynamic-table/components/SelectMenu.js +186 -0
  270. package/dist/backend/dynamic-table/components/SelectMenu.js.map +7 -0
  271. package/dist/backend/dynamic-table/components/StaleDataBar.js +26 -0
  272. package/dist/backend/dynamic-table/components/StaleDataBar.js.map +7 -0
  273. package/dist/backend/dynamic-table/components/TableDeleteDialog.js +48 -0
  274. package/dist/backend/dynamic-table/components/TableDeleteDialog.js.map +7 -0
  275. package/dist/backend/dynamic-table/components/TablePagination.js +69 -0
  276. package/dist/backend/dynamic-table/components/TablePagination.js.map +7 -0
  277. package/dist/backend/dynamic-table/components/TableSkeleton.js +43 -0
  278. package/dist/backend/dynamic-table/components/TableSkeleton.js.map +7 -0
  279. package/dist/backend/dynamic-table/components/ToolbarOverflow.js +204 -0
  280. package/dist/backend/dynamic-table/components/ToolbarOverflow.js.map +7 -0
  281. package/dist/backend/dynamic-table/components/ViewModeSwitch.js +50 -0
  282. package/dist/backend/dynamic-table/components/ViewModeSwitch.js.map +7 -0
  283. package/dist/backend/dynamic-table/components/VirtualRow.js +214 -0
  284. package/dist/backend/dynamic-table/components/VirtualRow.js.map +7 -0
  285. package/dist/backend/dynamic-table/components/editors.js +1483 -0
  286. package/dist/backend/dynamic-table/components/editors.js.map +7 -0
  287. package/dist/backend/dynamic-table/components/renderers.js +131 -0
  288. package/dist/backend/dynamic-table/components/renderers.js.map +7 -0
  289. package/dist/backend/dynamic-table/events/events.js +73 -0
  290. package/dist/backend/dynamic-table/events/events.js.map +7 -0
  291. package/dist/backend/dynamic-table/formula/check.js +243 -0
  292. package/dist/backend/dynamic-table/formula/check.js.map +7 -0
  293. package/dist/backend/dynamic-table/formula/evaluate.js +302 -0
  294. package/dist/backend/dynamic-table/formula/evaluate.js.map +7 -0
  295. package/dist/backend/dynamic-table/formula/functions.js +602 -0
  296. package/dist/backend/dynamic-table/formula/functions.js.map +7 -0
  297. package/dist/backend/dynamic-table/formula/index.js +47 -0
  298. package/dist/backend/dynamic-table/formula/index.js.map +7 -0
  299. package/dist/backend/dynamic-table/formula/parse.js +293 -0
  300. package/dist/backend/dynamic-table/formula/parse.js.map +7 -0
  301. package/dist/backend/dynamic-table/formula/types.js +24 -0
  302. package/dist/backend/dynamic-table/formula/types.js.map +7 -0
  303. package/dist/backend/dynamic-table/handlers/cellWrites.js +143 -0
  304. package/dist/backend/dynamic-table/handlers/cellWrites.js.map +7 -0
  305. package/dist/backend/dynamic-table/handlers/index.js +504 -0
  306. package/dist/backend/dynamic-table/handlers/index.js.map +7 -0
  307. package/dist/backend/dynamic-table/handlers/perspectiveHandlers.js +377 -0
  308. package/dist/backend/dynamic-table/handlers/perspectiveHandlers.js.map +7 -0
  309. package/dist/backend/dynamic-table/hooks/index.js +524 -0
  310. package/dist/backend/dynamic-table/hooks/index.js.map +7 -0
  311. package/dist/backend/dynamic-table/hooks/useAbbreviations.js +19 -0
  312. package/dist/backend/dynamic-table/hooks/useAbbreviations.js.map +7 -0
  313. package/dist/backend/dynamic-table/hooks/useAnnotations.js +115 -0
  314. package/dist/backend/dynamic-table/hooks/useAnnotations.js.map +7 -0
  315. package/dist/backend/dynamic-table/hooks/useColumnVirtualizer.js +155 -0
  316. package/dist/backend/dynamic-table/hooks/useColumnVirtualizer.js.map +7 -0
  317. package/dist/backend/dynamic-table/hooks/useColumnWidthPersistence.js +135 -0
  318. package/dist/backend/dynamic-table/hooks/useColumnWidthPersistence.js.map +7 -0
  319. package/dist/backend/dynamic-table/hooks/useDensityPreference.js +203 -0
  320. package/dist/backend/dynamic-table/hooks/useDensityPreference.js.map +7 -0
  321. package/dist/backend/dynamic-table/hooks/useDynamicTablePage.js +1005 -0
  322. package/dist/backend/dynamic-table/hooks/useDynamicTablePage.js.map +7 -0
  323. package/dist/backend/dynamic-table/hooks/useEscapeLayer.js +32 -0
  324. package/dist/backend/dynamic-table/hooks/useEscapeLayer.js.map +7 -0
  325. package/dist/backend/dynamic-table/hooks/useGrouping.js +256 -0
  326. package/dist/backend/dynamic-table/hooks/useGrouping.js.map +7 -0
  327. package/dist/backend/dynamic-table/hooks/useSuggestionFetch.js +63 -0
  328. package/dist/backend/dynamic-table/hooks/useSuggestionFetch.js.map +7 -0
  329. package/dist/backend/dynamic-table/hooks/useTableDateFormat.js +11 -0
  330. package/dist/backend/dynamic-table/hooks/useTableDateFormat.js.map +7 -0
  331. package/dist/backend/dynamic-table/index.js +476 -0
  332. package/dist/backend/dynamic-table/index.js.map +7 -0
  333. package/dist/backend/dynamic-table/registry/ContentRegistryContext.js +126 -0
  334. package/dist/backend/dynamic-table/registry/ContentRegistryContext.js.map +7 -0
  335. package/dist/backend/dynamic-table/registry/TableRegistryContext.js +43 -0
  336. package/dist/backend/dynamic-table/registry/TableRegistryContext.js.map +7 -0
  337. package/dist/backend/dynamic-table/registry/filterAccessibleTables.js +13 -0
  338. package/dist/backend/dynamic-table/registry/filterAccessibleTables.js.map +7 -0
  339. package/dist/backend/dynamic-table/registry/index.js +44 -0
  340. package/dist/backend/dynamic-table/registry/index.js.map +7 -0
  341. package/dist/backend/dynamic-table/registry/lazyTable.js +55 -0
  342. package/dist/backend/dynamic-table/registry/lazyTable.js.map +7 -0
  343. package/dist/backend/dynamic-table/registry/types.js +1 -0
  344. package/dist/backend/dynamic-table/registry/types.js.map +7 -0
  345. package/dist/backend/dynamic-table/registry/widgetCatalog.js +96 -0
  346. package/dist/backend/dynamic-table/registry/widgetCatalog.js.map +7 -0
  347. package/dist/backend/dynamic-table/server/aggregate.js +261 -0
  348. package/dist/backend/dynamic-table/server/aggregate.js.map +7 -0
  349. package/dist/backend/dynamic-table/server/dateWindows.js +105 -0
  350. package/dist/backend/dynamic-table/server/dateWindows.js.map +7 -0
  351. package/dist/backend/dynamic-table/server/filterParser.js +97 -0
  352. package/dist/backend/dynamic-table/server/filterParser.js.map +7 -0
  353. package/dist/backend/dynamic-table/server/filterSuggestions.js +70 -0
  354. package/dist/backend/dynamic-table/server/filterSuggestions.js.map +7 -0
  355. package/dist/backend/dynamic-table/server/index.js +47 -0
  356. package/dist/backend/dynamic-table/server/index.js.map +7 -0
  357. package/dist/backend/dynamic-table/server/makeDynamicTableRoute.js +270 -0
  358. package/dist/backend/dynamic-table/server/makeDynamicTableRoute.js.map +7 -0
  359. package/dist/backend/dynamic-table/split-view/ContentPicker.js +171 -0
  360. package/dist/backend/dynamic-table/split-view/ContentPicker.js.map +7 -0
  361. package/dist/backend/dynamic-table/split-view/EmptySlot.js +47 -0
  362. package/dist/backend/dynamic-table/split-view/EmptySlot.js.map +7 -0
  363. package/dist/backend/dynamic-table/split-view/SplitViewHost.js +863 -0
  364. package/dist/backend/dynamic-table/split-view/SplitViewHost.js.map +7 -0
  365. package/dist/backend/dynamic-table/split-view/WidgetPane.js +280 -0
  366. package/dist/backend/dynamic-table/split-view/WidgetPane.js.map +7 -0
  367. package/dist/backend/dynamic-table/split-view/WorkspaceFilterBar.js +524 -0
  368. package/dist/backend/dynamic-table/split-view/WorkspaceFilterBar.js.map +7 -0
  369. package/dist/backend/dynamic-table/split-view/index.js +61 -0
  370. package/dist/backend/dynamic-table/split-view/index.js.map +7 -0
  371. package/dist/backend/dynamic-table/split-view/layoutPersistence.js +91 -0
  372. package/dist/backend/dynamic-table/split-view/layoutPersistence.js.map +7 -0
  373. package/dist/backend/dynamic-table/split-view/sharedCriteria.js +62 -0
  374. package/dist/backend/dynamic-table/split-view/sharedCriteria.js.map +7 -0
  375. package/dist/backend/dynamic-table/split-view/types.js +426 -0
  376. package/dist/backend/dynamic-table/split-view/types.js.map +7 -0
  377. package/dist/backend/dynamic-table/split-view/useSplitViewLayouts.js +79 -0
  378. package/dist/backend/dynamic-table/split-view/useSplitViewLayouts.js.map +7 -0
  379. package/dist/backend/dynamic-table/store/index.js +611 -0
  380. package/dist/backend/dynamic-table/store/index.js.map +7 -0
  381. package/dist/backend/dynamic-table/styles/ContextMenu.css.js +2 -0
  382. package/dist/backend/dynamic-table/styles/DynamicTable.css.js +2 -0
  383. package/dist/backend/dynamic-table/styles/DynamicTable.v2.css.js +2 -0
  384. package/dist/backend/dynamic-table/styles/SearchBar.css.js +2 -0
  385. package/dist/backend/dynamic-table/styles/dateCell.css.js +2 -0
  386. package/dist/backend/dynamic-table/styles/density.css.js +2 -0
  387. package/dist/backend/dynamic-table/types/density.js +29 -0
  388. package/dist/backend/dynamic-table/types/density.js.map +7 -0
  389. package/dist/backend/dynamic-table/types/filters.js +124 -0
  390. package/dist/backend/dynamic-table/types/filters.js.map +7 -0
  391. package/dist/backend/dynamic-table/types/grouping.js +34 -0
  392. package/dist/backend/dynamic-table/types/grouping.js.map +7 -0
  393. package/dist/backend/dynamic-table/types/import.js +7 -0
  394. package/dist/backend/dynamic-table/types/import.js.map +7 -0
  395. package/dist/backend/dynamic-table/types/index.js +46 -0
  396. package/dist/backend/dynamic-table/types/index.js.map +7 -0
  397. package/dist/backend/dynamic-table/types/perspective.js +79 -0
  398. package/dist/backend/dynamic-table/types/perspective.js.map +7 -0
  399. package/dist/backend/dynamic-table/types/rollup.js +13 -0
  400. package/dist/backend/dynamic-table/types/rollup.js.map +7 -0
  401. package/dist/backend/dynamic-table/utils/abbreviations.js +228 -0
  402. package/dist/backend/dynamic-table/utils/abbreviations.js.map +7 -0
  403. package/dist/backend/dynamic-table/utils/anchoredPosition.js +21 -0
  404. package/dist/backend/dynamic-table/utils/anchoredPosition.js.map +7 -0
  405. package/dist/backend/dynamic-table/utils/badgeAutoFit.js +35 -0
  406. package/dist/backend/dynamic-table/utils/badgeAutoFit.js.map +7 -0
  407. package/dist/backend/dynamic-table/utils/clipboard.js +172 -0
  408. package/dist/backend/dynamic-table/utils/clipboard.js.map +7 -0
  409. package/dist/backend/dynamic-table/utils/coerceCellValue.js +52 -0
  410. package/dist/backend/dynamic-table/utils/coerceCellValue.js.map +7 -0
  411. package/dist/backend/dynamic-table/utils/colorAdjacency.js +27 -0
  412. package/dist/backend/dynamic-table/utils/colorAdjacency.js.map +7 -0
  413. package/dist/backend/dynamic-table/utils/columnFill.js +36 -0
  414. package/dist/backend/dynamic-table/utils/columnFill.js.map +7 -0
  415. package/dist/backend/dynamic-table/utils/columnSpacerStyle.js +21 -0
  416. package/dist/backend/dynamic-table/utils/columnSpacerStyle.js.map +7 -0
  417. package/dist/backend/dynamic-table/utils/columnWindow.js +293 -0
  418. package/dist/backend/dynamic-table/utils/columnWindow.js.map +7 -0
  419. package/dist/backend/dynamic-table/utils/conditionalFormat.js +188 -0
  420. package/dist/backend/dynamic-table/utils/conditionalFormat.js.map +7 -0
  421. package/dist/backend/dynamic-table/utils/exportTable.js +82 -0
  422. package/dist/backend/dynamic-table/utils/exportTable.js.map +7 -0
  423. package/dist/backend/dynamic-table/utils/fillGeometry.js +92 -0
  424. package/dist/backend/dynamic-table/utils/fillGeometry.js.map +7 -0
  425. package/dist/backend/dynamic-table/utils/fillPatterns.js +241 -0
  426. package/dist/backend/dynamic-table/utils/fillPatterns.js.map +7 -0
  427. package/dist/backend/dynamic-table/utils/formatAggregate.js +82 -0
  428. package/dist/backend/dynamic-table/utils/formatAggregate.js.map +7 -0
  429. package/dist/backend/dynamic-table/utils/formatDateCompact.js +508 -0
  430. package/dist/backend/dynamic-table/utils/formatDateCompact.js.map +7 -0
  431. package/dist/backend/dynamic-table/utils/formatLabelCompact.js +312 -0
  432. package/dist/backend/dynamic-table/utils/formatLabelCompact.js.map +7 -0
  433. package/dist/backend/dynamic-table/utils/formulaColumns.js +158 -0
  434. package/dist/backend/dynamic-table/utils/formulaColumns.js.map +7 -0
  435. package/dist/backend/dynamic-table/utils/importMapping.js +118 -0
  436. package/dist/backend/dynamic-table/utils/importMapping.js.map +7 -0
  437. package/dist/backend/dynamic-table/utils/importParse.js +121 -0
  438. package/dist/backend/dynamic-table/utils/importParse.js.map +7 -0
  439. package/dist/backend/dynamic-table/utils/lookupColumns.js +28 -0
  440. package/dist/backend/dynamic-table/utils/lookupColumns.js.map +7 -0
  441. package/dist/backend/dynamic-table/utils/perspectiveTransforms.js +191 -0
  442. package/dist/backend/dynamic-table/utils/perspectiveTransforms.js.map +7 -0
  443. package/dist/backend/dynamic-table/utils/rollupColumns.js +81 -0
  444. package/dist/backend/dynamic-table/utils/rollupColumns.js.map +7 -0
  445. package/dist/backend/dynamic-table/validators/index.js +36 -0
  446. package/dist/backend/dynamic-table/validators/index.js.map +7 -0
  447. package/dist/backend/fields/registry.generated.js +8 -0
  448. package/dist/backend/fields/registry.generated.js.map +7 -0
  449. package/dist/backend/fields/registry.js +29 -0
  450. package/dist/backend/fields/registry.js.map +7 -0
  451. package/dist/backend/filters/AdvancedFilterBuilder.js +280 -0
  452. package/dist/backend/filters/AdvancedFilterBuilder.js.map +7 -0
  453. package/dist/backend/forms/ActionsDropdown.js +160 -0
  454. package/dist/backend/forms/ActionsDropdown.js.map +7 -0
  455. package/dist/backend/forms/FormActionButtons.js +59 -0
  456. package/dist/backend/forms/FormActionButtons.js.map +7 -0
  457. package/dist/backend/forms/FormFooter.js +18 -0
  458. package/dist/backend/forms/FormFooter.js.map +7 -0
  459. package/dist/backend/forms/FormHeader.js +125 -0
  460. package/dist/backend/forms/FormHeader.js.map +7 -0
  461. package/dist/backend/forms/index.js +11 -0
  462. package/dist/backend/forms/index.js.map +7 -0
  463. package/dist/backend/hooks/useAdvancedFilter.js +64 -0
  464. package/dist/backend/hooks/useAdvancedFilter.js.map +7 -0
  465. package/dist/backend/indexes/PartialIndexBanner.js +74 -0
  466. package/dist/backend/indexes/PartialIndexBanner.js.map +7 -0
  467. package/dist/backend/indexes/store.js +62 -0
  468. package/dist/backend/indexes/store.js.map +7 -0
  469. package/dist/backend/injection/ComponentOverrideProvider.js +55 -0
  470. package/dist/backend/injection/ComponentOverrideProvider.js.map +7 -0
  471. package/dist/backend/injection/InjectedField.js +166 -0
  472. package/dist/backend/injection/InjectedField.js.map +7 -0
  473. package/dist/backend/injection/InjectionSpot.js +323 -0
  474. package/dist/backend/injection/InjectionSpot.js.map +7 -0
  475. package/dist/backend/injection/InjectionWizard.js +256 -0
  476. package/dist/backend/injection/InjectionWizard.js.map +7 -0
  477. package/dist/backend/injection/PageInjectionBoundary.js +26 -0
  478. package/dist/backend/injection/PageInjectionBoundary.js.map +7 -0
  479. package/dist/backend/injection/SseEventIndicator.js +8 -0
  480. package/dist/backend/injection/SseEventIndicator.js.map +7 -0
  481. package/dist/backend/injection/StatusBadgeInjectionSpot.js +34 -0
  482. package/dist/backend/injection/StatusBadgeInjectionSpot.js.map +7 -0
  483. package/dist/backend/injection/StatusBadgeRenderer.js +58 -0
  484. package/dist/backend/injection/StatusBadgeRenderer.js.map +7 -0
  485. package/dist/backend/injection/WidgetSharedState.js +49 -0
  486. package/dist/backend/injection/WidgetSharedState.js.map +7 -0
  487. package/dist/backend/injection/WizardInjectionSpot.js +30 -0
  488. package/dist/backend/injection/WizardInjectionSpot.js.map +7 -0
  489. package/dist/backend/injection/eventBridge.js +129 -0
  490. package/dist/backend/injection/eventBridge.js.map +7 -0
  491. package/dist/backend/injection/helpers.js +26 -0
  492. package/dist/backend/injection/helpers.js.map +7 -0
  493. package/dist/backend/injection/mergeMenuItems.js +49 -0
  494. package/dist/backend/injection/mergeMenuItems.js.map +7 -0
  495. package/dist/backend/injection/mutationEvents.js +20 -0
  496. package/dist/backend/injection/mutationEvents.js.map +7 -0
  497. package/dist/backend/injection/resolveInjectedIcon.js +23 -0
  498. package/dist/backend/injection/resolveInjectedIcon.js.map +7 -0
  499. package/dist/backend/injection/spotIds.js +72 -0
  500. package/dist/backend/injection/spotIds.js.map +7 -0
  501. package/dist/backend/injection/useAppEvent.js +35 -0
  502. package/dist/backend/injection/useAppEvent.js.map +7 -0
  503. package/dist/backend/injection/useGuardedMutation.js +71 -0
  504. package/dist/backend/injection/useGuardedMutation.js.map +7 -0
  505. package/dist/backend/injection/useInjectedMenuItems.js +49 -0
  506. package/dist/backend/injection/useInjectedMenuItems.js.map +7 -0
  507. package/dist/backend/injection/useInjectionDataWidgets.js +36 -0
  508. package/dist/backend/injection/useInjectionDataWidgets.js.map +7 -0
  509. package/dist/backend/injection/useOperationProgress.js +64 -0
  510. package/dist/backend/injection/useOperationProgress.js.map +7 -0
  511. package/dist/backend/injection/useRegisteredComponent.js +89 -0
  512. package/dist/backend/injection/useRegisteredComponent.js.map +7 -0
  513. package/dist/backend/injection/useWidgetSharedState.js +26 -0
  514. package/dist/backend/injection/useWidgetSharedState.js.map +7 -0
  515. package/dist/backend/injection/visibility-utils.js +29 -0
  516. package/dist/backend/injection/visibility-utils.js.map +7 -0
  517. package/dist/backend/injection/widgetRegistry.js +55 -0
  518. package/dist/backend/injection/widgetRegistry.js.map +7 -0
  519. package/dist/backend/inputs/ComboboxInput.js +228 -0
  520. package/dist/backend/inputs/ComboboxInput.js.map +7 -0
  521. package/dist/backend/inputs/DatePicker.js +148 -0
  522. package/dist/backend/inputs/DatePicker.js.map +7 -0
  523. package/dist/backend/inputs/DateTimePicker.js +188 -0
  524. package/dist/backend/inputs/DateTimePicker.js.map +7 -0
  525. package/dist/backend/inputs/EventPatternInput.js +38 -0
  526. package/dist/backend/inputs/EventPatternInput.js.map +7 -0
  527. package/dist/backend/inputs/EventSelect.js +115 -0
  528. package/dist/backend/inputs/EventSelect.js.map +7 -0
  529. package/dist/backend/inputs/LookupSelect.js +197 -0
  530. package/dist/backend/inputs/LookupSelect.js.map +7 -0
  531. package/dist/backend/inputs/PhoneNumberField.js +133 -0
  532. package/dist/backend/inputs/PhoneNumberField.js.map +7 -0
  533. package/dist/backend/inputs/SwitchableMarkdownInput.js +73 -0
  534. package/dist/backend/inputs/SwitchableMarkdownInput.js.map +7 -0
  535. package/dist/backend/inputs/TagsInput.js +258 -0
  536. package/dist/backend/inputs/TagsInput.js.map +7 -0
  537. package/dist/backend/inputs/TimeInput.js +132 -0
  538. package/dist/backend/inputs/TimeInput.js.map +7 -0
  539. package/dist/backend/inputs/TimePicker.js +105 -0
  540. package/dist/backend/inputs/TimePicker.js.map +7 -0
  541. package/dist/backend/inputs/index.js +12 -0
  542. package/dist/backend/inputs/index.js.map +7 -0
  543. package/dist/backend/markdown/MarkdownContent.js +20 -0
  544. package/dist/backend/markdown/MarkdownContent.js.map +7 -0
  545. package/dist/backend/markdown/index.js +3 -0
  546. package/dist/backend/markdown/index.js.map +7 -0
  547. package/dist/backend/markdown/useMarkdownRemarkPlugins.js +33 -0
  548. package/dist/backend/markdown/useMarkdownRemarkPlugins.js.map +7 -0
  549. package/dist/backend/messages/MessageComposer.js +158 -0
  550. package/dist/backend/messages/MessageComposer.js.map +7 -0
  551. package/dist/backend/messages/MessageObjectDetail.js +81 -0
  552. package/dist/backend/messages/MessageObjectDetail.js.map +7 -0
  553. package/dist/backend/messages/MessageObjectPreview.js +48 -0
  554. package/dist/backend/messages/MessageObjectPreview.js.map +7 -0
  555. package/dist/backend/messages/MessageObjectRecordPicker.js +89 -0
  556. package/dist/backend/messages/MessageObjectRecordPicker.js.map +7 -0
  557. package/dist/backend/messages/MessagePrioritySelector.js +74 -0
  558. package/dist/backend/messages/MessagePrioritySelector.js.map +7 -0
  559. package/dist/backend/messages/MessagesIcon.js +22 -0
  560. package/dist/backend/messages/MessagesIcon.js.map +7 -0
  561. package/dist/backend/messages/SendObjectMessageDialog.js +65 -0
  562. package/dist/backend/messages/SendObjectMessageDialog.js.map +7 -0
  563. package/dist/backend/messages/index.js +17 -0
  564. package/dist/backend/messages/index.js.map +7 -0
  565. package/dist/backend/messages/message-compose-form-groups.js +333 -0
  566. package/dist/backend/messages/message-compose-form-groups.js.map +7 -0
  567. package/dist/backend/messages/message-composer.types.js +1 -0
  568. package/dist/backend/messages/message-composer.types.js.map +7 -0
  569. package/dist/backend/messages/message-priority.js +40 -0
  570. package/dist/backend/messages/message-priority.js.map +7 -0
  571. package/dist/backend/messages/useMessageCompose.js +488 -0
  572. package/dist/backend/messages/useMessageCompose.js.map +7 -0
  573. package/dist/backend/messages/useMessageComposeOperations.js +163 -0
  574. package/dist/backend/messages/useMessageComposeOperations.js.map +7 -0
  575. package/dist/backend/messages/useMessagesPoll.js +79 -0
  576. package/dist/backend/messages/useMessagesPoll.js.map +7 -0
  577. package/dist/backend/notifications/NotificationBell.js +83 -0
  578. package/dist/backend/notifications/NotificationBell.js.map +7 -0
  579. package/dist/backend/notifications/NotificationCountBadge.js +9 -0
  580. package/dist/backend/notifications/NotificationCountBadge.js.map +7 -0
  581. package/dist/backend/notifications/NotificationDispatcher.js +165 -0
  582. package/dist/backend/notifications/NotificationDispatcher.js.map +7 -0
  583. package/dist/backend/notifications/NotificationItem.js +187 -0
  584. package/dist/backend/notifications/NotificationItem.js.map +7 -0
  585. package/dist/backend/notifications/NotificationPanel.js +146 -0
  586. package/dist/backend/notifications/NotificationPanel.js.map +7 -0
  587. package/dist/backend/notifications/index.js +19 -0
  588. package/dist/backend/notifications/index.js.map +7 -0
  589. package/dist/backend/notifications/useNotificationActions.js +124 -0
  590. package/dist/backend/notifications/useNotificationActions.js.map +7 -0
  591. package/dist/backend/notifications/useNotificationEffect.js +13 -0
  592. package/dist/backend/notifications/useNotificationEffect.js.map +7 -0
  593. package/dist/backend/notifications/useNotifications.js +11 -0
  594. package/dist/backend/notifications/useNotifications.js.map +7 -0
  595. package/dist/backend/notifications/useNotificationsPoll.js +135 -0
  596. package/dist/backend/notifications/useNotificationsPoll.js.map +7 -0
  597. package/dist/backend/notifications/useNotificationsSse.js +173 -0
  598. package/dist/backend/notifications/useNotificationsSse.js.map +7 -0
  599. package/dist/backend/operations/LastOperationBanner.js +80 -0
  600. package/dist/backend/operations/LastOperationBanner.js.map +7 -0
  601. package/dist/backend/operations/store.js +183 -0
  602. package/dist/backend/operations/store.js.map +7 -0
  603. package/dist/backend/progress/ProgressTopBar.js +129 -0
  604. package/dist/backend/progress/ProgressTopBar.js.map +7 -0
  605. package/dist/backend/progress/useProgress.js +11 -0
  606. package/dist/backend/progress/useProgress.js.map +7 -0
  607. package/dist/backend/progress/useProgressPoll.js +69 -0
  608. package/dist/backend/progress/useProgressPoll.js.map +7 -0
  609. package/dist/backend/progress/useProgressSse.js +104 -0
  610. package/dist/backend/progress/useProgressSse.js.map +7 -0
  611. package/dist/backend/schedule/ScheduleAgenda.js +107 -0
  612. package/dist/backend/schedule/ScheduleAgenda.js.map +7 -0
  613. package/dist/backend/schedule/ScheduleGrid.js +107 -0
  614. package/dist/backend/schedule/ScheduleGrid.js.map +7 -0
  615. package/dist/backend/schedule/ScheduleToolbar.js +166 -0
  616. package/dist/backend/schedule/ScheduleToolbar.js.map +7 -0
  617. package/dist/backend/schedule/ScheduleView.js +168 -0
  618. package/dist/backend/schedule/ScheduleView.js.map +7 -0
  619. package/dist/backend/schedule/index.js +6 -0
  620. package/dist/backend/schedule/index.js.map +7 -0
  621. package/dist/backend/schedule/recurrence.js +83 -0
  622. package/dist/backend/schedule/recurrence.js.map +7 -0
  623. package/dist/backend/schedule/types.js +1 -0
  624. package/dist/backend/schedule/types.js.map +7 -0
  625. package/dist/backend/section-page/SectionNav.js +104 -0
  626. package/dist/backend/section-page/SectionNav.js.map +7 -0
  627. package/dist/backend/section-page/SectionPage.js +33 -0
  628. package/dist/backend/section-page/SectionPage.js.map +7 -0
  629. package/dist/backend/section-page/index.js +7 -0
  630. package/dist/backend/section-page/index.js.map +7 -0
  631. package/dist/backend/section-page/types.js +1 -0
  632. package/dist/backend/section-page/types.js.map +7 -0
  633. package/dist/backend/settings/SettingsNavigation.js +43 -0
  634. package/dist/backend/settings/SettingsNavigation.js.map +7 -0
  635. package/dist/backend/settings/SettingsPageWrapper.js +66 -0
  636. package/dist/backend/settings/SettingsPageWrapper.js.map +7 -0
  637. package/dist/backend/settings/index.js +6 -0
  638. package/dist/backend/settings/index.js.map +7 -0
  639. package/dist/backend/upgrades/UpgradeActionBanner.js +91 -0
  640. package/dist/backend/upgrades/UpgradeActionBanner.js.map +7 -0
  641. package/dist/backend/utils/api.js +178 -0
  642. package/dist/backend/utils/api.js.map +7 -0
  643. package/dist/backend/utils/apiCall.js +68 -0
  644. package/dist/backend/utils/apiCall.js.map +7 -0
  645. package/dist/backend/utils/crud.js +126 -0
  646. package/dist/backend/utils/crud.js.map +7 -0
  647. package/dist/backend/utils/customFieldColumns.js +100 -0
  648. package/dist/backend/utils/customFieldColumns.js.map +7 -0
  649. package/dist/backend/utils/customFieldDefs.js +143 -0
  650. package/dist/backend/utils/customFieldDefs.js.map +7 -0
  651. package/dist/backend/utils/customFieldFilters.js +151 -0
  652. package/dist/backend/utils/customFieldFilters.js.map +7 -0
  653. package/dist/backend/utils/customFieldForms.js +169 -0
  654. package/dist/backend/utils/customFieldForms.js.map +7 -0
  655. package/dist/backend/utils/customFieldRelationDisplay.js +239 -0
  656. package/dist/backend/utils/customFieldRelationDisplay.js.map +7 -0
  657. package/dist/backend/utils/customFieldValues.js +26 -0
  658. package/dist/backend/utils/customFieldValues.js.map +7 -0
  659. package/dist/backend/utils/flash.js +16 -0
  660. package/dist/backend/utils/flash.js.map +7 -0
  661. package/dist/backend/utils/nav.js +280 -0
  662. package/dist/backend/utils/nav.js.map +7 -0
  663. package/dist/backend/utils/richTextSanitizer.js +11 -0
  664. package/dist/backend/utils/richTextSanitizer.js.map +7 -0
  665. package/dist/backend/utils/scopedHeaderStack.js +44 -0
  666. package/dist/backend/utils/scopedHeaderStack.js.map +7 -0
  667. package/dist/backend/utils/serverErrors.js +237 -0
  668. package/dist/backend/utils/serverErrors.js.map +7 -0
  669. package/dist/backend/utils/useAutoDiscoveredFields.js +96 -0
  670. package/dist/backend/utils/useAutoDiscoveredFields.js.map +7 -0
  671. package/dist/backend/version-history/VersionHistoryAction.js +67 -0
  672. package/dist/backend/version-history/VersionHistoryAction.js.map +7 -0
  673. package/dist/backend/version-history/VersionHistoryDetail.js +57 -0
  674. package/dist/backend/version-history/VersionHistoryDetail.js.map +7 -0
  675. package/dist/backend/version-history/VersionHistoryPanel.js +270 -0
  676. package/dist/backend/version-history/VersionHistoryPanel.js.map +7 -0
  677. package/dist/backend/version-history/index.js +13 -0
  678. package/dist/backend/version-history/index.js.map +7 -0
  679. package/dist/backend/version-history/labels.js +52 -0
  680. package/dist/backend/version-history/labels.js.map +7 -0
  681. package/dist/backend/version-history/types.js +1 -0
  682. package/dist/backend/version-history/types.js.map +7 -0
  683. package/dist/backend/version-history/useAuditPermissions.js +72 -0
  684. package/dist/backend/version-history/useAuditPermissions.js.map +7 -0
  685. package/dist/backend/version-history/useVersionHistory.js +141 -0
  686. package/dist/backend/version-history/useVersionHistory.js.map +7 -0
  687. package/dist/backend/views/NewViewForm.js +56 -0
  688. package/dist/backend/views/NewViewForm.js.map +7 -0
  689. package/dist/backend/views/ShareForm.js +49 -0
  690. package/dist/backend/views/ShareForm.js.map +7 -0
  691. package/dist/backend/views/ViewChip.js +112 -0
  692. package/dist/backend/views/ViewChip.js.map +7 -0
  693. package/dist/backend/views/ViewChipMenu.js +89 -0
  694. package/dist/backend/views/ViewChipMenu.js.map +7 -0
  695. package/dist/backend/views/types.js +1 -0
  696. package/dist/backend/views/types.js.map +7 -0
  697. package/dist/frontend/AuthFooter.js +23 -0
  698. package/dist/frontend/AuthFooter.js.map +7 -0
  699. package/dist/frontend/LanguageSwitcher.js +57 -0
  700. package/dist/frontend/LanguageSwitcher.js.map +7 -0
  701. package/dist/frontend/Layout.js +14 -0
  702. package/dist/frontend/Layout.js.map +7 -0
  703. package/dist/hooks/useIsMobile.js +21 -0
  704. package/dist/hooks/useIsMobile.js.map +7 -0
  705. package/dist/index.js +40 -0
  706. package/dist/index.js.map +7 -0
  707. package/dist/portal/PortalContext.js +113 -0
  708. package/dist/portal/PortalContext.js.map +7 -0
  709. package/dist/portal/PortalLayoutProvider.js +5 -0
  710. package/dist/portal/PortalLayoutProvider.js.map +7 -0
  711. package/dist/portal/PortalLayoutShell.js +44 -0
  712. package/dist/portal/PortalLayoutShell.js.map +7 -0
  713. package/dist/portal/PortalShell.js +229 -0
  714. package/dist/portal/PortalShell.js.map +7 -0
  715. package/dist/portal/components/PortalCard.js +31 -0
  716. package/dist/portal/components/PortalCard.js.map +7 -0
  717. package/dist/portal/components/PortalEmptyState.js +16 -0
  718. package/dist/portal/components/PortalEmptyState.js.map +7 -0
  719. package/dist/portal/components/PortalFeatureCard.js +21 -0
  720. package/dist/portal/components/PortalFeatureCard.js.map +7 -0
  721. package/dist/portal/components/PortalNotificationBell.js +67 -0
  722. package/dist/portal/components/PortalNotificationBell.js.map +7 -0
  723. package/dist/portal/components/PortalNotificationPanel.js +158 -0
  724. package/dist/portal/components/PortalNotificationPanel.js.map +7 -0
  725. package/dist/portal/components/PortalPageHeader.js +16 -0
  726. package/dist/portal/components/PortalPageHeader.js.map +7 -0
  727. package/dist/portal/components/index.js +18 -0
  728. package/dist/portal/components/index.js.map +7 -0
  729. package/dist/portal/hooks/useCustomerAuth.js +69 -0
  730. package/dist/portal/hooks/useCustomerAuth.js.map +7 -0
  731. package/dist/portal/hooks/usePortalAppEvent.js +26 -0
  732. package/dist/portal/hooks/usePortalAppEvent.js.map +7 -0
  733. package/dist/portal/hooks/usePortalDashboardWidgets.js +37 -0
  734. package/dist/portal/hooks/usePortalDashboardWidgets.js.map +7 -0
  735. package/dist/portal/hooks/usePortalEventBridge.js +129 -0
  736. package/dist/portal/hooks/usePortalEventBridge.js.map +7 -0
  737. package/dist/portal/hooks/usePortalInjectedMenuItems.js +88 -0
  738. package/dist/portal/hooks/usePortalInjectedMenuItems.js.map +7 -0
  739. package/dist/portal/hooks/usePortalNotifications.js +85 -0
  740. package/dist/portal/hooks/usePortalNotifications.js.map +7 -0
  741. package/dist/portal/hooks/useTenantContext.js +53 -0
  742. package/dist/portal/hooks/useTenantContext.js.map +7 -0
  743. package/dist/primitives/DataLoader.js +68 -0
  744. package/dist/primitives/DataLoader.js.map +7 -0
  745. package/dist/primitives/ErrorNotice.js +14 -0
  746. package/dist/primitives/ErrorNotice.js.map +7 -0
  747. package/dist/primitives/Notice.js +55 -0
  748. package/dist/primitives/Notice.js.map +7 -0
  749. package/dist/primitives/alert.js +38 -0
  750. package/dist/primitives/alert.js.map +7 -0
  751. package/dist/primitives/badge.js +33 -0
  752. package/dist/primitives/badge.js.map +7 -0
  753. package/dist/primitives/button.js +45 -0
  754. package/dist/primitives/button.js.map +7 -0
  755. package/dist/primitives/calendar.js +62 -0
  756. package/dist/primitives/calendar.js.map +7 -0
  757. package/dist/primitives/card.js +91 -0
  758. package/dist/primitives/card.js.map +7 -0
  759. package/dist/primitives/checkbox.js +28 -0
  760. package/dist/primitives/checkbox.js.map +7 -0
  761. package/dist/primitives/dialog.js +101 -0
  762. package/dist/primitives/dialog.js.map +7 -0
  763. package/dist/primitives/form-field.js +78 -0
  764. package/dist/primitives/form-field.js.map +7 -0
  765. package/dist/primitives/icon-button.js +40 -0
  766. package/dist/primitives/icon-button.js.map +7 -0
  767. package/dist/primitives/input.js +22 -0
  768. package/dist/primitives/input.js.map +7 -0
  769. package/dist/primitives/label.js +21 -0
  770. package/dist/primitives/label.js.map +7 -0
  771. package/dist/primitives/popover.js +36 -0
  772. package/dist/primitives/popover.js.map +7 -0
  773. package/dist/primitives/primitives.stories.js +187 -0
  774. package/dist/primitives/primitives.stories.js.map +7 -0
  775. package/dist/primitives/progress.js +28 -0
  776. package/dist/primitives/progress.js.map +7 -0
  777. package/dist/primitives/separator.js +9 -0
  778. package/dist/primitives/separator.js.map +7 -0
  779. package/dist/primitives/sheet.js +103 -0
  780. package/dist/primitives/sheet.js.map +7 -0
  781. package/dist/primitives/spinner.js +24 -0
  782. package/dist/primitives/spinner.js.map +7 -0
  783. package/dist/primitives/status-badge.js +38 -0
  784. package/dist/primitives/status-badge.js.map +7 -0
  785. package/dist/primitives/switch.js +80 -0
  786. package/dist/primitives/switch.js.map +7 -0
  787. package/dist/primitives/table.js +29 -0
  788. package/dist/primitives/table.js.map +7 -0
  789. package/dist/primitives/tabs.js +90 -0
  790. package/dist/primitives/tabs.js.map +7 -0
  791. package/dist/primitives/textarea.js +21 -0
  792. package/dist/primitives/textarea.js.map +7 -0
  793. package/dist/primitives/tooltip.js +60 -0
  794. package/dist/primitives/tooltip.js.map +7 -0
  795. package/dist/primitives-v2/Alert.js +69 -0
  796. package/dist/primitives-v2/Alert.js.map +7 -0
  797. package/dist/primitives-v2/Alert.stories.js +42 -0
  798. package/dist/primitives-v2/Alert.stories.js.map +7 -0
  799. package/dist/primitives-v2/Avatar.js +93 -0
  800. package/dist/primitives-v2/Avatar.js.map +7 -0
  801. package/dist/primitives-v2/Avatar.stories.js +57 -0
  802. package/dist/primitives-v2/Avatar.stories.js.map +7 -0
  803. package/dist/primitives-v2/Badge.js +50 -0
  804. package/dist/primitives-v2/Badge.js.map +7 -0
  805. package/dist/primitives-v2/Badge.stories.js +66 -0
  806. package/dist/primitives-v2/Badge.stories.js.map +7 -0
  807. package/dist/primitives-v2/Breadcrumb.js +53 -0
  808. package/dist/primitives-v2/Breadcrumb.js.map +7 -0
  809. package/dist/primitives-v2/Breadcrumb.stories.js +70 -0
  810. package/dist/primitives-v2/Breadcrumb.stories.js.map +7 -0
  811. package/dist/primitives-v2/Button.js +85 -0
  812. package/dist/primitives-v2/Button.js.map +7 -0
  813. package/dist/primitives-v2/Button.stories.js +48 -0
  814. package/dist/primitives-v2/Button.stories.js.map +7 -0
  815. package/dist/primitives-v2/Checkbox.js +65 -0
  816. package/dist/primitives-v2/Checkbox.js.map +7 -0
  817. package/dist/primitives-v2/Checkbox.stories.js +31 -0
  818. package/dist/primitives-v2/Checkbox.stories.js.map +7 -0
  819. package/dist/primitives-v2/IconButton.js +13 -0
  820. package/dist/primitives-v2/IconButton.js.map +7 -0
  821. package/dist/primitives-v2/IconButton.stories.js +37 -0
  822. package/dist/primitives-v2/IconButton.stories.js.map +7 -0
  823. package/dist/primitives-v2/Input.js +74 -0
  824. package/dist/primitives-v2/Input.js.map +7 -0
  825. package/dist/primitives-v2/Input.stories.js +56 -0
  826. package/dist/primitives-v2/Input.stories.js.map +7 -0
  827. package/dist/primitives-v2/NavGroup.js +54 -0
  828. package/dist/primitives-v2/NavGroup.js.map +7 -0
  829. package/dist/primitives-v2/NavGroup.stories.js +39 -0
  830. package/dist/primitives-v2/NavGroup.stories.js.map +7 -0
  831. package/dist/primitives-v2/NavItem.js +61 -0
  832. package/dist/primitives-v2/NavItem.js.map +7 -0
  833. package/dist/primitives-v2/NavItem.stories.js +56 -0
  834. package/dist/primitives-v2/NavItem.stories.js.map +7 -0
  835. package/dist/primitives-v2/SearchInput.js +93 -0
  836. package/dist/primitives-v2/SearchInput.js.map +7 -0
  837. package/dist/primitives-v2/SearchInput.stories.js +41 -0
  838. package/dist/primitives-v2/SearchInput.stories.js.map +7 -0
  839. package/dist/primitives-v2/Sidebar.js +48 -0
  840. package/dist/primitives-v2/Sidebar.js.map +7 -0
  841. package/dist/primitives-v2/Sidebar.stories.js +76 -0
  842. package/dist/primitives-v2/Sidebar.stories.js.map +7 -0
  843. package/dist/primitives-v2/Tab.js +78 -0
  844. package/dist/primitives-v2/Tab.js.map +7 -0
  845. package/dist/primitives-v2/Tab.stories.js +51 -0
  846. package/dist/primitives-v2/Tab.stories.js.map +7 -0
  847. package/dist/primitives-v2/Tag.js +61 -0
  848. package/dist/primitives-v2/Tag.js.map +7 -0
  849. package/dist/primitives-v2/Tag.stories.js +121 -0
  850. package/dist/primitives-v2/Tag.stories.js.map +7 -0
  851. package/dist/primitives-v2/Toggle.js +72 -0
  852. package/dist/primitives-v2/Toggle.js.map +7 -0
  853. package/dist/primitives-v2/Toggle.stories.js +34 -0
  854. package/dist/primitives-v2/Toggle.stories.js.map +7 -0
  855. package/dist/primitives-v2/Topbar.js +24 -0
  856. package/dist/primitives-v2/Topbar.js.map +7 -0
  857. package/dist/primitives-v2/Topbar.stories.js +58 -0
  858. package/dist/primitives-v2/Topbar.stories.js.map +7 -0
  859. package/dist/primitives-v2/index.js +41 -0
  860. package/dist/primitives-v2/index.js.map +7 -0
  861. package/dist/primitives-v2/utils.js +7 -0
  862. package/dist/primitives-v2/utils.js.map +7 -0
  863. package/dist/theme/BrandThemeProvider.js +49 -0
  864. package/dist/theme/BrandThemeProvider.js.map +7 -0
  865. package/dist/theme/QueryProvider.js +44 -0
  866. package/dist/theme/QueryProvider.js.map +7 -0
  867. package/dist/theme/ThemeProvider.js +96 -0
  868. package/dist/theme/ThemeProvider.js.map +7 -0
  869. package/dist/theme/ThemeToggle.js +88 -0
  870. package/dist/theme/ThemeToggle.js.map +7 -0
  871. package/dist/theme/index.js +12 -0
  872. package/dist/theme/index.js.map +7 -0
  873. package/dist/theme/m3.css.js +2 -0
  874. package/dist/theme/tokens.v2.css.js +2 -0
  875. package/dist/theme/typography.css.js +2 -0
  876. package/dist/theme/typography.stories.js +123 -0
  877. package/dist/theme/typography.stories.js.map +7 -0
  878. package/dist/theme/typography.tokens.js +281 -0
  879. package/dist/theme/typography.tokens.js.map +7 -0
  880. package/dist/types/react-big-calendar.d.js +1 -0
  881. package/dist/types/react-big-calendar.d.js.map +7 -0
  882. package/package.json +411 -0
  883. package/src/backend/AppShell.tsx +1606 -0
  884. package/src/backend/BackendChromeProvider.tsx +99 -0
  885. package/src/backend/BackendPageSurface.tsx +57 -0
  886. package/src/backend/CollapsibleNavSection.tsx +201 -0
  887. package/src/backend/ContextHelp.tsx +40 -0
  888. package/src/backend/CrudForm.tsx +3796 -0
  889. package/src/backend/DataTable.tsx +2584 -0
  890. package/src/backend/EmptyState.tsx +65 -0
  891. package/src/backend/FilterBar.tsx +171 -0
  892. package/src/backend/FilterOverlay.tsx +388 -0
  893. package/src/backend/FlashMessages.tsx +189 -0
  894. package/src/backend/HtmlRichTextEditor.tsx +124 -0
  895. package/src/backend/IntegrationsButton.tsx +29 -0
  896. package/src/backend/JsonBuilder.tsx +359 -0
  897. package/src/backend/JsonDisplay.tsx +258 -0
  898. package/src/backend/NextStepCallout.tsx +134 -0
  899. package/src/backend/Page.tsx +30 -0
  900. package/src/backend/PerspectiveSidebar.tsx +506 -0
  901. package/src/backend/ProfileDropdown.tsx +369 -0
  902. package/src/backend/RowActions.tsx +158 -0
  903. package/src/backend/SectionHeader.tsx +119 -0
  904. package/src/backend/SettingsButton.tsx +28 -0
  905. package/src/backend/Sidebar.stories.tsx +183 -0
  906. package/src/backend/Sidebar.tsx +737 -0
  907. package/src/backend/TruncatedCell.tsx +133 -0
  908. package/src/backend/UserMenu.tsx +153 -0
  909. package/src/backend/ValueIcons.tsx +48 -0
  910. package/src/backend/WebhookSetupGuide.tsx +127 -0
  911. package/src/backend/__tests__/AppShell.test.tsx +392 -0
  912. package/src/backend/__tests__/CrudForm.custom-fields.test.tsx +180 -0
  913. package/src/backend/__tests__/CrudForm.datetime.test.tsx +95 -0
  914. package/src/backend/__tests__/CrudForm.defaults.test.tsx +195 -0
  915. package/src/backend/__tests__/CrudForm.navigation.test.tsx +156 -0
  916. package/src/backend/__tests__/CrudForm.render.test.tsx +146 -0
  917. package/src/backend/__tests__/CrudForm.transformData.test.tsx +231 -0
  918. package/src/backend/__tests__/CrudForm.unsavedNavigation.test.tsx +145 -0
  919. package/src/backend/__tests__/CrudForm.validation.test.tsx +178 -0
  920. package/src/backend/__tests__/CustomDataSection.test.tsx +465 -0
  921. package/src/backend/__tests__/DataTable.extensions.test.tsx +339 -0
  922. package/src/backend/__tests__/DataTable.namespaces.test.ts +32 -0
  923. package/src/backend/__tests__/DataTable.render.test.tsx +48 -0
  924. package/src/backend/__tests__/FieldDefinitionsEditor.test.tsx +64 -0
  925. package/src/backend/__tests__/FlashMessages.test.tsx +105 -0
  926. package/src/backend/__tests__/FormHeader.test.tsx +81 -0
  927. package/src/backend/__tests__/NotificationDispatcher.test.ts +172 -0
  928. package/src/backend/__tests__/VersionHistoryPanel.test.tsx +109 -0
  929. package/src/backend/__tests__/component-replacement.test.tsx +270 -0
  930. package/src/backend/__tests__/custom-field-filters.test.ts +72 -0
  931. package/src/backend/__tests__/custom-field-forms.test.ts +54 -0
  932. package/src/backend/__tests__/nav-utils.test.ts +175 -0
  933. package/src/backend/__tests__/serverErrors.test.ts +83 -0
  934. package/src/backend/__tests__/useAuditPermissions.test.tsx +51 -0
  935. package/src/backend/__tests__/useNotifications.strategy.test.ts +58 -0
  936. package/src/backend/__tests__/useProgress.strategy.test.ts +62 -0
  937. package/src/backend/charts/BarChart.tsx +208 -0
  938. package/src/backend/charts/ChartUtils.tsx +196 -0
  939. package/src/backend/charts/ComboChart.stories.tsx +103 -0
  940. package/src/backend/charts/ComboChart.tsx +350 -0
  941. package/src/backend/charts/KpiCard.tsx +155 -0
  942. package/src/backend/charts/LineChart.tsx +207 -0
  943. package/src/backend/charts/PieChart.tsx +157 -0
  944. package/src/backend/charts/ScatterChart.tsx +498 -0
  945. package/src/backend/charts/TopNTable.tsx +141 -0
  946. package/src/backend/charts/WaterfallChart.tsx +213 -0
  947. package/src/backend/charts/index.ts +21 -0
  948. package/src/backend/columns/ColumnChooserPanel.tsx +293 -0
  949. package/src/backend/confirm-dialog/ConfirmDialog.tsx +275 -0
  950. package/src/backend/confirm-dialog/index.ts +7 -0
  951. package/src/backend/confirm-dialog/useConfirmDialog.tsx +146 -0
  952. package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +1413 -0
  953. package/src/backend/custom-fields/FieldDefinitionsManager.tsx +381 -0
  954. package/src/backend/dashboard/DashboardScreen.tsx +706 -0
  955. package/src/backend/dashboard/__tests__/DashboardScreen.test.tsx +114 -0
  956. package/src/backend/dashboard/index.ts +1 -0
  957. package/src/backend/dashboard/widgetRegistry.ts +68 -0
  958. package/src/backend/date-range/DateRangeSelect.tsx +50 -0
  959. package/src/backend/date-range/InlineDateRangeSelect.tsx +52 -0
  960. package/src/backend/date-range/InlineGranularitySelect.tsx +63 -0
  961. package/src/backend/date-range/__tests__/dateRanges.test.ts +224 -0
  962. package/src/backend/date-range/dateRanges.ts +218 -0
  963. package/src/backend/date-range/index.ts +15 -0
  964. package/src/backend/detail/AccessDeniedMessage.tsx +32 -0
  965. package/src/backend/detail/ActivitiesSection.tsx +1261 -0
  966. package/src/backend/detail/AddressEditor.tsx +479 -0
  967. package/src/backend/detail/AddressTiles.tsx +591 -0
  968. package/src/backend/detail/AddressesSection.tsx +362 -0
  969. package/src/backend/detail/AttachmentDeleteDialog.tsx +56 -0
  970. package/src/backend/detail/AttachmentMetadataDialog.tsx +674 -0
  971. package/src/backend/detail/AttachmentVisualPreview.tsx +155 -0
  972. package/src/backend/detail/AttachmentsSection.tsx +327 -0
  973. package/src/backend/detail/CustomDataSection.tsx +700 -0
  974. package/src/backend/detail/DetailFieldsSection.tsx +163 -0
  975. package/src/backend/detail/ErrorMessage.tsx +32 -0
  976. package/src/backend/detail/InlineEditors.tsx +894 -0
  977. package/src/backend/detail/LoadingMessage.tsx +14 -0
  978. package/src/backend/detail/NotesSection.tsx +1266 -0
  979. package/src/backend/detail/TabEmptyState.tsx +48 -0
  980. package/src/backend/detail/TagsSection.tsx +470 -0
  981. package/src/backend/detail/__tests__/InlineEditors.test.tsx +59 -0
  982. package/src/backend/detail/addressFormat.tsx +121 -0
  983. package/src/backend/detail/index.ts +46 -0
  984. package/src/backend/devtools/UmesDevToolsPanel.tsx +122 -0
  985. package/src/backend/devtools/components/ConflictWarnings.tsx +36 -0
  986. package/src/backend/devtools/components/EnricherTiming.tsx +50 -0
  987. package/src/backend/devtools/components/EventFlow.tsx +45 -0
  988. package/src/backend/devtools/components/ExtensionPointList.tsx +59 -0
  989. package/src/backend/devtools/components/InterceptorActivity.tsx +48 -0
  990. package/src/backend/devtools/index.ts +2 -0
  991. package/src/backend/devtools/useUmesDevTools.ts +188 -0
  992. package/src/backend/document-viewer/RegionOverlay.tsx +150 -0
  993. package/src/backend/document-viewer/index.ts +2 -0
  994. package/src/backend/dynamic-grid/DynamicGrid.tsx +161 -0
  995. package/src/backend/dynamic-grid/adapters.ts +57 -0
  996. package/src/backend/dynamic-grid/components/GridCell.tsx +194 -0
  997. package/src/backend/dynamic-grid/components/GridRow.tsx +40 -0
  998. package/src/backend/dynamic-grid/hooks/useGridKeyboardNav.ts +134 -0
  999. package/src/backend/dynamic-grid/index.ts +4 -0
  1000. package/src/backend/dynamic-grid/types.ts +55 -0
  1001. package/src/backend/dynamic-table/DynamicTable.stories.tsx +274 -0
  1002. package/src/backend/dynamic-table/DynamicTable.tsx +4419 -0
  1003. package/src/backend/dynamic-table/README.md +1199 -0
  1004. package/src/backend/dynamic-table/URL_FILTERING.md +217 -0
  1005. package/src/backend/dynamic-table/__e2e__/GridHarness.ts +1155 -0
  1006. package/src/backend/dynamic-table/__e2e__/SplitViewHarness.ts +366 -0
  1007. package/src/backend/dynamic-table/__tests__/BulkActionsBar.test.tsx +143 -0
  1008. package/src/backend/dynamic-table/__tests__/ColumnQuickFilter.test.tsx +607 -0
  1009. package/src/backend/dynamic-table/__tests__/ConfigureViewFilters.suggestions.test.tsx +177 -0
  1010. package/src/backend/dynamic-table/__tests__/ConfigureViewFormatting.test.tsx +125 -0
  1011. package/src/backend/dynamic-table/__tests__/ConfigureViewFormulas.test.tsx +228 -0
  1012. package/src/backend/dynamic-table/__tests__/ConfigureViewGrouping.aggregations.test.tsx +131 -0
  1013. package/src/backend/dynamic-table/__tests__/ConfigureViewPanel.baseView.test.tsx +162 -0
  1014. package/src/backend/dynamic-table/__tests__/ConfigureViewPanel.focus.test.tsx +54 -0
  1015. package/src/backend/dynamic-table/__tests__/ConfigureViewPanel.sections.test.tsx +142 -0
  1016. package/src/backend/dynamic-table/__tests__/ConfigureViewRollups.test.tsx +228 -0
  1017. package/src/backend/dynamic-table/__tests__/ContextMenu.test.tsx +89 -0
  1018. package/src/backend/dynamic-table/__tests__/DynamicTable.density.test.tsx +299 -0
  1019. package/src/backend/dynamic-table/__tests__/DynamicTable.exportTruncation.test.tsx +145 -0
  1020. package/src/backend/dynamic-table/__tests__/DynamicTable.loadError.test.tsx +173 -0
  1021. package/src/backend/dynamic-table/__tests__/DynamicTable.wiring.test.tsx +357 -0
  1022. package/src/backend/dynamic-table/__tests__/EmptyState.test.tsx +70 -0
  1023. package/src/backend/dynamic-table/__tests__/ExportMenu.test.tsx +82 -0
  1024. package/src/backend/dynamic-table/__tests__/FooterTotalsRow.test.tsx +154 -0
  1025. package/src/backend/dynamic-table/__tests__/GroupSummaryRow.render.test.tsx +131 -0
  1026. package/src/backend/dynamic-table/__tests__/LoadErrorState.test.tsx +74 -0
  1027. package/src/backend/dynamic-table/__tests__/MentionPopup.search.test.tsx +127 -0
  1028. package/src/backend/dynamic-table/__tests__/PerspectiveTabs.test.tsx +170 -0
  1029. package/src/backend/dynamic-table/__tests__/RowHeaderCell.selection.test.tsx +80 -0
  1030. package/src/backend/dynamic-table/__tests__/abbreviationsSubscription.test.ts +171 -0
  1031. package/src/backend/dynamic-table/__tests__/aggregate.spec.test.ts +224 -0
  1032. package/src/backend/dynamic-table/__tests__/aggregateDimension.test.tsx +191 -0
  1033. package/src/backend/dynamic-table/__tests__/anchoredPosition.test.ts +88 -0
  1034. package/src/backend/dynamic-table/__tests__/badgeAutoFit.test.ts +121 -0
  1035. package/src/backend/dynamic-table/__tests__/cellHandlers.save.test.ts +282 -0
  1036. package/src/backend/dynamic-table/__tests__/cellWrites.test.ts +274 -0
  1037. package/src/backend/dynamic-table/__tests__/clipboard.test.ts +230 -0
  1038. package/src/backend/dynamic-table/__tests__/coerceCellValue.test.ts +117 -0
  1039. package/src/backend/dynamic-table/__tests__/colorAdjacency.test.ts +151 -0
  1040. package/src/backend/dynamic-table/__tests__/columnFill.test.ts +66 -0
  1041. package/src/backend/dynamic-table/__tests__/columnHeaderHandlers.sort.test.ts +100 -0
  1042. package/src/backend/dynamic-table/__tests__/columnHeaderReorder.test.tsx +209 -0
  1043. package/src/backend/dynamic-table/__tests__/columnReorderPersistence.test.tsx +127 -0
  1044. package/src/backend/dynamic-table/__tests__/columnVirtualization.render.test.tsx +420 -0
  1045. package/src/backend/dynamic-table/__tests__/columnVisibilityPersistence.test.tsx +136 -0
  1046. package/src/backend/dynamic-table/__tests__/columnWindow.test.ts +643 -0
  1047. package/src/backend/dynamic-table/__tests__/commentAudience.test.tsx +137 -0
  1048. package/src/backend/dynamic-table/__tests__/commentScope.test.tsx +214 -0
  1049. package/src/backend/dynamic-table/__tests__/conditionalFormat.test.ts +175 -0
  1050. package/src/backend/dynamic-table/__tests__/contentRegistry.test.ts +267 -0
  1051. package/src/backend/dynamic-table/__tests__/dateWindows.test.ts +149 -0
  1052. package/src/backend/dynamic-table/__tests__/density.test.ts +204 -0
  1053. package/src/backend/dynamic-table/__tests__/exportTable.test.ts +146 -0
  1054. package/src/backend/dynamic-table/__tests__/fillGeometry.test.ts +201 -0
  1055. package/src/backend/dynamic-table/__tests__/fillPatterns.test.ts +272 -0
  1056. package/src/backend/dynamic-table/__tests__/filterParser.presets.test.ts +64 -0
  1057. package/src/backend/dynamic-table/__tests__/filterParser.relative.test.ts +109 -0
  1058. package/src/backend/dynamic-table/__tests__/filterSuggestions.test.ts +152 -0
  1059. package/src/backend/dynamic-table/__tests__/formatAggregate.test.ts +99 -0
  1060. package/src/backend/dynamic-table/__tests__/formatDateCompact.test.ts +489 -0
  1061. package/src/backend/dynamic-table/__tests__/formatLabelCompact.test.ts +717 -0
  1062. package/src/backend/dynamic-table/__tests__/formula.check.test.ts +257 -0
  1063. package/src/backend/dynamic-table/__tests__/formula.evaluate.test.ts +274 -0
  1064. package/src/backend/dynamic-table/__tests__/formula.functions.test.ts +226 -0
  1065. package/src/backend/dynamic-table/__tests__/formula.noEval.test.ts +92 -0
  1066. package/src/backend/dynamic-table/__tests__/formula.parse.test.ts +233 -0
  1067. package/src/backend/dynamic-table/__tests__/formulaColumns.test.tsx +213 -0
  1068. package/src/backend/dynamic-table/__tests__/handlerIdentity.test.ts +77 -0
  1069. package/src/backend/dynamic-table/__tests__/importMapping.test.ts +174 -0
  1070. package/src/backend/dynamic-table/__tests__/importParse.test.ts +130 -0
  1071. package/src/backend/dynamic-table/__tests__/importServerOnly.test.ts +37 -0
  1072. package/src/backend/dynamic-table/__tests__/layoutPersistence.test.ts +372 -0
  1073. package/src/backend/dynamic-table/__tests__/mouseHandlers.doubleClick.test.ts +155 -0
  1074. package/src/backend/dynamic-table/__tests__/perspectivePersonalization.test.ts +188 -0
  1075. package/src/backend/dynamic-table/__tests__/perspectiveResolver.test.ts +133 -0
  1076. package/src/backend/dynamic-table/__tests__/perspectiveState.defaultGrouping.test.ts +306 -0
  1077. package/src/backend/dynamic-table/__tests__/perspectiveState.defaults.test.ts +107 -0
  1078. package/src/backend/dynamic-table/__tests__/perspectiveTemplates.test.tsx +269 -0
  1079. package/src/backend/dynamic-table/__tests__/perspectiveTransforms.aggregations.test.ts +105 -0
  1080. package/src/backend/dynamic-table/__tests__/perspectiveTransforms.conditionalFormats.test.ts +107 -0
  1081. package/src/backend/dynamic-table/__tests__/perspectiveTransforms.dateFormat.test.ts +61 -0
  1082. package/src/backend/dynamic-table/__tests__/perspectiveTransforms.formulas.test.ts +85 -0
  1083. package/src/backend/dynamic-table/__tests__/perspectiveTransforms.frozenColumns.test.ts +124 -0
  1084. package/src/backend/dynamic-table/__tests__/rollupColumns.test.ts +149 -0
  1085. package/src/backend/dynamic-table/__tests__/sharedCriteria.test.ts +255 -0
  1086. package/src/backend/dynamic-table/__tests__/splitViewTree.test.ts +324 -0
  1087. package/src/backend/dynamic-table/__tests__/store.clipboardMarker.test.ts +143 -0
  1088. package/src/backend/dynamic-table/__tests__/store.fillPreview.test.ts +168 -0
  1089. package/src/backend/dynamic-table/__tests__/store.mutations.test.ts +345 -0
  1090. package/src/backend/dynamic-table/__tests__/store.selection.test.ts +157 -0
  1091. package/src/backend/dynamic-table/__tests__/store.undo.test.ts +225 -0
  1092. package/src/backend/dynamic-table/__tests__/store.valueSource.test.tsx +204 -0
  1093. package/src/backend/dynamic-table/__tests__/useAnnotations.legacyOptions.test.tsx +58 -0
  1094. package/src/backend/dynamic-table/__tests__/useColumnVirtualizer.test.ts +290 -0
  1095. package/src/backend/dynamic-table/__tests__/useDensityPreference.test.ts +435 -0
  1096. package/src/backend/dynamic-table/__tests__/useDynamicTablePage.duplicate.test.tsx +99 -0
  1097. package/src/backend/dynamic-table/__tests__/useDynamicTablePage.export.test.tsx +281 -0
  1098. package/src/backend/dynamic-table/__tests__/useDynamicTablePage.personalization.test.tsx +313 -0
  1099. package/src/backend/dynamic-table/__tests__/useDynamicTablePage.perspectiveRestore.test.tsx +126 -0
  1100. package/src/backend/dynamic-table/__tests__/useGrouping.aggregations.test.ts +158 -0
  1101. package/src/backend/dynamic-table/__tests__/useGrouping.groupKey.test.ts +58 -0
  1102. package/src/backend/dynamic-table/__tests__/useKeyboardNavigation.test.ts +594 -0
  1103. package/src/backend/dynamic-table/__tests__/useStickyOffsets.memo.test.tsx +75 -0
  1104. package/src/backend/dynamic-table/__tests__/useStickyOffsets.test.ts +106 -0
  1105. package/src/backend/dynamic-table/__tests__/widgetPane.test.tsx +338 -0
  1106. package/src/backend/dynamic-table/components/ActiveFilterChips.tsx +109 -0
  1107. package/src/backend/dynamic-table/components/BulkActionsBar.tsx +89 -0
  1108. package/src/backend/dynamic-table/components/Cell.tsx +292 -0
  1109. package/src/backend/dynamic-table/components/CellCommentDialog.tsx +887 -0
  1110. package/src/backend/dynamic-table/components/CellCommentHoverPopup.tsx +94 -0
  1111. package/src/backend/dynamic-table/components/ColumnFilterPopover.tsx +775 -0
  1112. package/src/backend/dynamic-table/components/ColumnHeaderMenu.tsx +189 -0
  1113. package/src/backend/dynamic-table/components/ColumnHeaders.tsx +867 -0
  1114. package/src/backend/dynamic-table/components/CompactLabelCell.tsx +293 -0
  1115. package/src/backend/dynamic-table/components/ConfigureViewFields.tsx +173 -0
  1116. package/src/backend/dynamic-table/components/ConfigureViewFilters.tsx +346 -0
  1117. package/src/backend/dynamic-table/components/ConfigureViewFormatting.tsx +205 -0
  1118. package/src/backend/dynamic-table/components/ConfigureViewFormulas.tsx +423 -0
  1119. package/src/backend/dynamic-table/components/ConfigureViewGrouping.tsx +264 -0
  1120. package/src/backend/dynamic-table/components/ConfigureViewLinkedColumns.tsx +249 -0
  1121. package/src/backend/dynamic-table/components/ConfigureViewPanel.tsx +857 -0
  1122. package/src/backend/dynamic-table/components/ConfigureViewRollups.tsx +364 -0
  1123. package/src/backend/dynamic-table/components/ConfigureViewSorting.tsx +121 -0
  1124. package/src/backend/dynamic-table/components/ContextMenu.tsx +140 -0
  1125. package/src/backend/dynamic-table/components/DateCell.tsx +205 -0
  1126. package/src/backend/dynamic-table/components/Debugger.tsx +340 -0
  1127. package/src/backend/dynamic-table/components/DensityControl.tsx +441 -0
  1128. package/src/backend/dynamic-table/components/EmptyState.tsx +103 -0
  1129. package/src/backend/dynamic-table/components/EntitySearchEditor.tsx +528 -0
  1130. package/src/backend/dynamic-table/components/ExportMenu.tsx +90 -0
  1131. package/src/backend/dynamic-table/components/FilterDatePicker.tsx +194 -0
  1132. package/src/backend/dynamic-table/components/FilterValueInput.tsx +294 -0
  1133. package/src/backend/dynamic-table/components/FooterTotalsRow.tsx +291 -0
  1134. package/src/backend/dynamic-table/components/FullscreenOverlay.tsx +66 -0
  1135. package/src/backend/dynamic-table/components/GroupHeaderRow.tsx +136 -0
  1136. package/src/backend/dynamic-table/components/GroupSummaryRow.tsx +184 -0
  1137. package/src/backend/dynamic-table/components/ImportPanel.tsx +445 -0
  1138. package/src/backend/dynamic-table/components/ImportPreviewGrid.tsx +155 -0
  1139. package/src/backend/dynamic-table/components/LoadErrorState.tsx +70 -0
  1140. package/src/backend/dynamic-table/components/MentionPopup.tsx +224 -0
  1141. package/src/backend/dynamic-table/components/PerspectiveDeleteDialog.tsx +96 -0
  1142. package/src/backend/dynamic-table/components/PerspectivePublishDialog.tsx +152 -0
  1143. package/src/backend/dynamic-table/components/PerspectiveTabMenu.tsx +215 -0
  1144. package/src/backend/dynamic-table/components/PerspectiveTabs.tsx +426 -0
  1145. package/src/backend/dynamic-table/components/PerspectiveTemplateMenu.tsx +135 -0
  1146. package/src/backend/dynamic-table/components/RowHeaderCell.tsx +73 -0
  1147. package/src/backend/dynamic-table/components/SearchBar.tsx +309 -0
  1148. package/src/backend/dynamic-table/components/SelectMenu.tsx +237 -0
  1149. package/src/backend/dynamic-table/components/StaleDataBar.tsx +74 -0
  1150. package/src/backend/dynamic-table/components/TableDeleteDialog.tsx +74 -0
  1151. package/src/backend/dynamic-table/components/TablePagination.tsx +96 -0
  1152. package/src/backend/dynamic-table/components/TableSkeleton.tsx +123 -0
  1153. package/src/backend/dynamic-table/components/ToolbarOverflow.tsx +284 -0
  1154. package/src/backend/dynamic-table/components/ViewModeSwitch.tsx +92 -0
  1155. package/src/backend/dynamic-table/components/VirtualRow.tsx +346 -0
  1156. package/src/backend/dynamic-table/components/editors.tsx +1727 -0
  1157. package/src/backend/dynamic-table/components/renderers.tsx +162 -0
  1158. package/src/backend/dynamic-table/events/events.ts +107 -0
  1159. package/src/backend/dynamic-table/formula/check.ts +359 -0
  1160. package/src/backend/dynamic-table/formula/evaluate.ts +473 -0
  1161. package/src/backend/dynamic-table/formula/functions.ts +737 -0
  1162. package/src/backend/dynamic-table/formula/index.ts +57 -0
  1163. package/src/backend/dynamic-table/formula/parse.ts +414 -0
  1164. package/src/backend/dynamic-table/formula/types.ts +151 -0
  1165. package/src/backend/dynamic-table/handlers/cellWrites.ts +362 -0
  1166. package/src/backend/dynamic-table/handlers/index.ts +789 -0
  1167. package/src/backend/dynamic-table/handlers/perspectiveHandlers.ts +684 -0
  1168. package/src/backend/dynamic-table/hooks/index.ts +888 -0
  1169. package/src/backend/dynamic-table/hooks/useAbbreviations.ts +67 -0
  1170. package/src/backend/dynamic-table/hooks/useAnnotations.ts +215 -0
  1171. package/src/backend/dynamic-table/hooks/useColumnVirtualizer.ts +303 -0
  1172. package/src/backend/dynamic-table/hooks/useColumnWidthPersistence.ts +214 -0
  1173. package/src/backend/dynamic-table/hooks/useDensityPreference.ts +451 -0
  1174. package/src/backend/dynamic-table/hooks/useDynamicTablePage.tsx +1681 -0
  1175. package/src/backend/dynamic-table/hooks/useEscapeLayer.ts +72 -0
  1176. package/src/backend/dynamic-table/hooks/useGrouping.ts +428 -0
  1177. package/src/backend/dynamic-table/hooks/useSuggestionFetch.ts +109 -0
  1178. package/src/backend/dynamic-table/hooks/useTableDateFormat.tsx +19 -0
  1179. package/src/backend/dynamic-table/index.ts +497 -0
  1180. package/src/backend/dynamic-table/registry/ContentRegistryContext.tsx +244 -0
  1181. package/src/backend/dynamic-table/registry/TableRegistryContext.tsx +84 -0
  1182. package/src/backend/dynamic-table/registry/filterAccessibleTables.ts +36 -0
  1183. package/src/backend/dynamic-table/registry/index.ts +38 -0
  1184. package/src/backend/dynamic-table/registry/lazyTable.tsx +80 -0
  1185. package/src/backend/dynamic-table/registry/types.ts +181 -0
  1186. package/src/backend/dynamic-table/registry/widgetCatalog.ts +226 -0
  1187. package/src/backend/dynamic-table/server/aggregate.ts +474 -0
  1188. package/src/backend/dynamic-table/server/dateWindows.ts +201 -0
  1189. package/src/backend/dynamic-table/server/filterParser.ts +132 -0
  1190. package/src/backend/dynamic-table/server/filterSuggestions.ts +150 -0
  1191. package/src/backend/dynamic-table/server/index.ts +47 -0
  1192. package/src/backend/dynamic-table/server/makeDynamicTableRoute.ts +426 -0
  1193. package/src/backend/dynamic-table/split-view/ContentPicker.tsx +251 -0
  1194. package/src/backend/dynamic-table/split-view/EmptySlot.tsx +69 -0
  1195. package/src/backend/dynamic-table/split-view/SplitViewHost.tsx +1336 -0
  1196. package/src/backend/dynamic-table/split-view/WidgetPane.tsx +390 -0
  1197. package/src/backend/dynamic-table/split-view/WorkspaceFilterBar.tsx +728 -0
  1198. package/src/backend/dynamic-table/split-view/index.ts +48 -0
  1199. package/src/backend/dynamic-table/split-view/layoutPersistence.ts +185 -0
  1200. package/src/backend/dynamic-table/split-view/sharedCriteria.ts +196 -0
  1201. package/src/backend/dynamic-table/split-view/types.ts +778 -0
  1202. package/src/backend/dynamic-table/split-view/useSplitViewLayouts.ts +97 -0
  1203. package/src/backend/dynamic-table/store/index.ts +1011 -0
  1204. package/src/backend/dynamic-table/styles/ContextMenu.css +387 -0
  1205. package/src/backend/dynamic-table/styles/DynamicTable.css +3659 -0
  1206. package/src/backend/dynamic-table/styles/DynamicTable.v2.css +2651 -0
  1207. package/src/backend/dynamic-table/styles/SearchBar.css +426 -0
  1208. package/src/backend/dynamic-table/styles/dateCell.css +146 -0
  1209. package/src/backend/dynamic-table/styles/density.css +690 -0
  1210. package/src/backend/dynamic-table/types/density.ts +179 -0
  1211. package/src/backend/dynamic-table/types/filters.ts +156 -0
  1212. package/src/backend/dynamic-table/types/grouping.ts +189 -0
  1213. package/src/backend/dynamic-table/types/import.ts +132 -0
  1214. package/src/backend/dynamic-table/types/index.ts +1198 -0
  1215. package/src/backend/dynamic-table/types/perspective.ts +436 -0
  1216. package/src/backend/dynamic-table/types/rollup.ts +111 -0
  1217. package/src/backend/dynamic-table/utils/abbreviations.ts +556 -0
  1218. package/src/backend/dynamic-table/utils/anchoredPosition.ts +106 -0
  1219. package/src/backend/dynamic-table/utils/badgeAutoFit.ts +108 -0
  1220. package/src/backend/dynamic-table/utils/clipboard.ts +294 -0
  1221. package/src/backend/dynamic-table/utils/coerceCellValue.ts +96 -0
  1222. package/src/backend/dynamic-table/utils/colorAdjacency.ts +87 -0
  1223. package/src/backend/dynamic-table/utils/columnFill.ts +93 -0
  1224. package/src/backend/dynamic-table/utils/columnSpacerStyle.ts +53 -0
  1225. package/src/backend/dynamic-table/utils/columnWindow.ts +668 -0
  1226. package/src/backend/dynamic-table/utils/conditionalFormat.ts +300 -0
  1227. package/src/backend/dynamic-table/utils/exportTable.ts +140 -0
  1228. package/src/backend/dynamic-table/utils/fillGeometry.ts +187 -0
  1229. package/src/backend/dynamic-table/utils/fillPatterns.ts +422 -0
  1230. package/src/backend/dynamic-table/utils/formatAggregate.ts +145 -0
  1231. package/src/backend/dynamic-table/utils/formatDateCompact.ts +879 -0
  1232. package/src/backend/dynamic-table/utils/formatLabelCompact.ts +640 -0
  1233. package/src/backend/dynamic-table/utils/formulaColumns.tsx +237 -0
  1234. package/src/backend/dynamic-table/utils/importMapping.ts +202 -0
  1235. package/src/backend/dynamic-table/utils/importParse.ts +220 -0
  1236. package/src/backend/dynamic-table/utils/lookupColumns.tsx +47 -0
  1237. package/src/backend/dynamic-table/utils/perspectiveTransforms.ts +368 -0
  1238. package/src/backend/dynamic-table/utils/rollupColumns.ts +162 -0
  1239. package/src/backend/dynamic-table/validators/index.ts +40 -0
  1240. package/src/backend/fields/registry.generated.ts +8 -0
  1241. package/src/backend/fields/registry.ts +38 -0
  1242. package/src/backend/filters/AdvancedFilterBuilder.tsx +339 -0
  1243. package/src/backend/forms/ActionsDropdown.tsx +198 -0
  1244. package/src/backend/forms/FormActionButtons.tsx +98 -0
  1245. package/src/backend/forms/FormFooter.tsx +27 -0
  1246. package/src/backend/forms/FormHeader.tsx +204 -0
  1247. package/src/backend/forms/index.ts +4 -0
  1248. package/src/backend/hooks/useAdvancedFilter.ts +77 -0
  1249. package/src/backend/indexes/PartialIndexBanner.tsx +86 -0
  1250. package/src/backend/indexes/store.ts +88 -0
  1251. package/src/backend/injection/ComponentOverrideProvider.tsx +66 -0
  1252. package/src/backend/injection/InjectedField.tsx +196 -0
  1253. package/src/backend/injection/InjectionSpot.tsx +457 -0
  1254. package/src/backend/injection/InjectionWizard.tsx +335 -0
  1255. package/src/backend/injection/PageInjectionBoundary.tsx +31 -0
  1256. package/src/backend/injection/SseEventIndicator.tsx +11 -0
  1257. package/src/backend/injection/StatusBadgeInjectionSpot.tsx +50 -0
  1258. package/src/backend/injection/StatusBadgeRenderer.tsx +100 -0
  1259. package/src/backend/injection/WidgetSharedState.ts +58 -0
  1260. package/src/backend/injection/WizardInjectionSpot.tsx +46 -0
  1261. package/src/backend/injection/eventBridge.ts +158 -0
  1262. package/src/backend/injection/helpers.ts +35 -0
  1263. package/src/backend/injection/mergeMenuItems.ts +80 -0
  1264. package/src/backend/injection/mutationEvents.ts +22 -0
  1265. package/src/backend/injection/resolveInjectedIcon.tsx +30 -0
  1266. package/src/backend/injection/spotIds.ts +64 -0
  1267. package/src/backend/injection/useAppEvent.ts +76 -0
  1268. package/src/backend/injection/useGuardedMutation.ts +101 -0
  1269. package/src/backend/injection/useInjectedMenuItems.ts +67 -0
  1270. package/src/backend/injection/useInjectionDataWidgets.ts +41 -0
  1271. package/src/backend/injection/useOperationProgress.ts +105 -0
  1272. package/src/backend/injection/useRegisteredComponent.tsx +106 -0
  1273. package/src/backend/injection/useWidgetSharedState.ts +28 -0
  1274. package/src/backend/injection/visibility-utils.ts +31 -0
  1275. package/src/backend/injection/widgetRegistry.ts +68 -0
  1276. package/src/backend/inputs/ComboboxInput.tsx +272 -0
  1277. package/src/backend/inputs/DatePicker.tsx +162 -0
  1278. package/src/backend/inputs/DateTimePicker.tsx +199 -0
  1279. package/src/backend/inputs/EventPatternInput.tsx +46 -0
  1280. package/src/backend/inputs/EventSelect.tsx +183 -0
  1281. package/src/backend/inputs/LookupSelect.tsx +253 -0
  1282. package/src/backend/inputs/PhoneNumberField.tsx +166 -0
  1283. package/src/backend/inputs/SwitchableMarkdownInput.tsx +106 -0
  1284. package/src/backend/inputs/TagsInput.tsx +297 -0
  1285. package/src/backend/inputs/TimeInput.tsx +146 -0
  1286. package/src/backend/inputs/TimePicker.tsx +120 -0
  1287. package/src/backend/inputs/__tests__/DatePicker.test.tsx +76 -0
  1288. package/src/backend/inputs/__tests__/DateTimePicker.logic.test.ts +126 -0
  1289. package/src/backend/inputs/__tests__/PhoneNumberField.test.tsx +73 -0
  1290. package/src/backend/inputs/__tests__/TagsInput.test.tsx +78 -0
  1291. package/src/backend/inputs/__tests__/TimeInput.test.tsx +134 -0
  1292. package/src/backend/inputs/__tests__/TimePicker.test.tsx +50 -0
  1293. package/src/backend/inputs/index.ts +11 -0
  1294. package/src/backend/markdown/MarkdownContent.tsx +33 -0
  1295. package/src/backend/markdown/index.ts +2 -0
  1296. package/src/backend/markdown/useMarkdownRemarkPlugins.ts +39 -0
  1297. package/src/backend/messages/MessageComposer.tsx +197 -0
  1298. package/src/backend/messages/MessageObjectDetail.tsx +95 -0
  1299. package/src/backend/messages/MessageObjectPreview.tsx +64 -0
  1300. package/src/backend/messages/MessageObjectRecordPicker.tsx +149 -0
  1301. package/src/backend/messages/MessagePrioritySelector.tsx +78 -0
  1302. package/src/backend/messages/MessagesIcon.tsx +32 -0
  1303. package/src/backend/messages/SendObjectMessageDialog.tsx +74 -0
  1304. package/src/backend/messages/__tests__/MessageComposer.test.tsx +350 -0
  1305. package/src/backend/messages/__tests__/SendObjectMessageDialog.test.tsx +77 -0
  1306. package/src/backend/messages/index.ts +22 -0
  1307. package/src/backend/messages/message-compose-form-groups.tsx +383 -0
  1308. package/src/backend/messages/message-composer.types.ts +82 -0
  1309. package/src/backend/messages/message-priority.ts +53 -0
  1310. package/src/backend/messages/useMessageCompose.ts +666 -0
  1311. package/src/backend/messages/useMessageComposeOperations.ts +259 -0
  1312. package/src/backend/messages/useMessagesPoll.ts +118 -0
  1313. package/src/backend/notifications/NotificationBell.tsx +91 -0
  1314. package/src/backend/notifications/NotificationCountBadge.tsx +14 -0
  1315. package/src/backend/notifications/NotificationDispatcher.ts +213 -0
  1316. package/src/backend/notifications/NotificationItem.tsx +262 -0
  1317. package/src/backend/notifications/NotificationPanel.tsx +225 -0
  1318. package/src/backend/notifications/__tests__/NotificationCountBadge.test.tsx +34 -0
  1319. package/src/backend/notifications/index.ts +20 -0
  1320. package/src/backend/notifications/useNotificationActions.ts +161 -0
  1321. package/src/backend/notifications/useNotificationEffect.ts +17 -0
  1322. package/src/backend/notifications/useNotifications.ts +12 -0
  1323. package/src/backend/notifications/useNotificationsPoll.ts +167 -0
  1324. package/src/backend/notifications/useNotificationsSse.ts +220 -0
  1325. package/src/backend/operations/LastOperationBanner.tsx +85 -0
  1326. package/src/backend/operations/__tests__/LastOperationBanner.test.tsx +101 -0
  1327. package/src/backend/operations/store.ts +230 -0
  1328. package/src/backend/progress/ProgressTopBar.tsx +189 -0
  1329. package/src/backend/progress/useProgress.ts +12 -0
  1330. package/src/backend/progress/useProgressPoll.ts +100 -0
  1331. package/src/backend/progress/useProgressSse.ts +118 -0
  1332. package/src/backend/schedule/ScheduleAgenda.tsx +136 -0
  1333. package/src/backend/schedule/ScheduleGrid.tsx +136 -0
  1334. package/src/backend/schedule/ScheduleToolbar.tsx +178 -0
  1335. package/src/backend/schedule/ScheduleView.tsx +201 -0
  1336. package/src/backend/schedule/index.ts +5 -0
  1337. package/src/backend/schedule/recurrence.ts +99 -0
  1338. package/src/backend/schedule/types.ts +26 -0
  1339. package/src/backend/section-page/SectionNav.tsx +140 -0
  1340. package/src/backend/section-page/SectionPage.tsx +34 -0
  1341. package/src/backend/section-page/index.ts +3 -0
  1342. package/src/backend/section-page/types.ts +30 -0
  1343. package/src/backend/settings/SettingsNavigation.tsx +74 -0
  1344. package/src/backend/settings/SettingsPageWrapper.tsx +78 -0
  1345. package/src/backend/settings/index.ts +3 -0
  1346. package/src/backend/upgrades/UpgradeActionBanner.tsx +128 -0
  1347. package/src/backend/utils/__tests__/api.scoped-headers.test.ts +58 -0
  1348. package/src/backend/utils/__tests__/api.test.ts +148 -0
  1349. package/src/backend/utils/__tests__/apiCall.scoped-request-headers.test.ts +58 -0
  1350. package/src/backend/utils/__tests__/apiCall.test.ts +109 -0
  1351. package/src/backend/utils/__tests__/crud.test.ts +87 -0
  1352. package/src/backend/utils/__tests__/customFieldDefs.test.ts +25 -0
  1353. package/src/backend/utils/__tests__/customFieldValues.test.ts +35 -0
  1354. package/src/backend/utils/__tests__/richTextSanitizer.test.ts +57 -0
  1355. package/src/backend/utils/api.ts +214 -0
  1356. package/src/backend/utils/apiCall.ts +123 -0
  1357. package/src/backend/utils/crud.ts +174 -0
  1358. package/src/backend/utils/customFieldColumns.ts +118 -0
  1359. package/src/backend/utils/customFieldDefs.ts +247 -0
  1360. package/src/backend/utils/customFieldFilters.ts +174 -0
  1361. package/src/backend/utils/customFieldForms.ts +203 -0
  1362. package/src/backend/utils/customFieldRelationDisplay.ts +302 -0
  1363. package/src/backend/utils/customFieldValues.ts +41 -0
  1364. package/src/backend/utils/flash.ts +17 -0
  1365. package/src/backend/utils/nav.ts +402 -0
  1366. package/src/backend/utils/richTextSanitizer.ts +7 -0
  1367. package/src/backend/utils/scopedHeaderStack.ts +49 -0
  1368. package/src/backend/utils/serverErrors.ts +313 -0
  1369. package/src/backend/utils/useAutoDiscoveredFields.ts +138 -0
  1370. package/src/backend/version-history/VersionHistoryAction.tsx +76 -0
  1371. package/src/backend/version-history/VersionHistoryDetail.tsx +87 -0
  1372. package/src/backend/version-history/VersionHistoryPanel.tsx +331 -0
  1373. package/src/backend/version-history/index.ts +9 -0
  1374. package/src/backend/version-history/labels.ts +56 -0
  1375. package/src/backend/version-history/types.ts +27 -0
  1376. package/src/backend/version-history/useAuditPermissions.ts +94 -0
  1377. package/src/backend/version-history/useVersionHistory.ts +173 -0
  1378. package/src/backend/views/NewViewForm.tsx +54 -0
  1379. package/src/backend/views/ShareForm.tsx +62 -0
  1380. package/src/backend/views/ViewChip.tsx +123 -0
  1381. package/src/backend/views/ViewChipMenu.tsx +94 -0
  1382. package/src/backend/views/types.ts +4 -0
  1383. package/src/frontend/AuthFooter.tsx +29 -0
  1384. package/src/frontend/LanguageSwitcher.tsx +66 -0
  1385. package/src/frontend/Layout.tsx +13 -0
  1386. package/src/hooks/useIsMobile.ts +27 -0
  1387. package/src/index.ts +40 -0
  1388. package/src/portal/PortalContext.tsx +180 -0
  1389. package/src/portal/PortalLayoutProvider.tsx +5 -0
  1390. package/src/portal/PortalLayoutShell.tsx +64 -0
  1391. package/src/portal/PortalShell.tsx +370 -0
  1392. package/src/portal/components/PortalCard.tsx +77 -0
  1393. package/src/portal/components/PortalEmptyState.tsx +32 -0
  1394. package/src/portal/components/PortalFeatureCard.tsx +41 -0
  1395. package/src/portal/components/PortalNotificationBell.tsx +81 -0
  1396. package/src/portal/components/PortalNotificationPanel.tsx +230 -0
  1397. package/src/portal/components/PortalPageHeader.tsx +33 -0
  1398. package/src/portal/components/index.ts +6 -0
  1399. package/src/portal/hooks/useCustomerAuth.ts +97 -0
  1400. package/src/portal/hooks/usePortalAppEvent.ts +56 -0
  1401. package/src/portal/hooks/usePortalDashboardWidgets.ts +50 -0
  1402. package/src/portal/hooks/usePortalEventBridge.ts +162 -0
  1403. package/src/portal/hooks/usePortalInjectedMenuItems.ts +130 -0
  1404. package/src/portal/hooks/usePortalNotifications.ts +116 -0
  1405. package/src/portal/hooks/useTenantContext.ts +83 -0
  1406. package/src/primitives/DataLoader.tsx +93 -0
  1407. package/src/primitives/ErrorNotice.tsx +17 -0
  1408. package/src/primitives/Notice.tsx +83 -0
  1409. package/src/primitives/alert.tsx +59 -0
  1410. package/src/primitives/badge.tsx +43 -0
  1411. package/src/primitives/button.tsx +60 -0
  1412. package/src/primitives/calendar.tsx +69 -0
  1413. package/src/primitives/card.tsx +92 -0
  1414. package/src/primitives/checkbox.tsx +34 -0
  1415. package/src/primitives/dialog.tsx +122 -0
  1416. package/src/primitives/form-field.tsx +106 -0
  1417. package/src/primitives/icon-button.tsx +48 -0
  1418. package/src/primitives/input.tsx +26 -0
  1419. package/src/primitives/label.tsx +18 -0
  1420. package/src/primitives/popover.tsx +37 -0
  1421. package/src/primitives/primitives.stories.tsx +223 -0
  1422. package/src/primitives/progress.tsx +28 -0
  1423. package/src/primitives/separator.tsx +7 -0
  1424. package/src/primitives/sheet.tsx +137 -0
  1425. package/src/primitives/spinner.tsx +27 -0
  1426. package/src/primitives/status-badge.tsx +62 -0
  1427. package/src/primitives/switch.tsx +92 -0
  1428. package/src/primitives/table.tsx +27 -0
  1429. package/src/primitives/tabs.tsx +131 -0
  1430. package/src/primitives/textarea.tsx +20 -0
  1431. package/src/primitives/tooltip.tsx +85 -0
  1432. package/src/primitives-v2/AGENTS.md +66 -0
  1433. package/src/primitives-v2/Alert.stories.tsx +56 -0
  1434. package/src/primitives-v2/Alert.tsx +108 -0
  1435. package/src/primitives-v2/Avatar.stories.tsx +60 -0
  1436. package/src/primitives-v2/Avatar.tsx +139 -0
  1437. package/src/primitives-v2/Badge.stories.tsx +80 -0
  1438. package/src/primitives-v2/Badge.tsx +99 -0
  1439. package/src/primitives-v2/Breadcrumb.stories.tsx +61 -0
  1440. package/src/primitives-v2/Breadcrumb.tsx +96 -0
  1441. package/src/primitives-v2/Button.stories.tsx +72 -0
  1442. package/src/primitives-v2/Button.tsx +178 -0
  1443. package/src/primitives-v2/Checkbox.stories.tsx +34 -0
  1444. package/src/primitives-v2/Checkbox.tsx +103 -0
  1445. package/src/primitives-v2/IconButton.stories.tsx +40 -0
  1446. package/src/primitives-v2/IconButton.tsx +40 -0
  1447. package/src/primitives-v2/Input.stories.tsx +60 -0
  1448. package/src/primitives-v2/Input.tsx +113 -0
  1449. package/src/primitives-v2/NavGroup.stories.tsx +50 -0
  1450. package/src/primitives-v2/NavGroup.tsx +76 -0
  1451. package/src/primitives-v2/NavItem.stories.tsx +68 -0
  1452. package/src/primitives-v2/NavItem.tsx +105 -0
  1453. package/src/primitives-v2/SearchInput.stories.tsx +44 -0
  1454. package/src/primitives-v2/SearchInput.tsx +123 -0
  1455. package/src/primitives-v2/Sidebar.stories.tsx +91 -0
  1456. package/src/primitives-v2/Sidebar.tsx +92 -0
  1457. package/src/primitives-v2/Tab.stories.tsx +62 -0
  1458. package/src/primitives-v2/Tab.tsx +148 -0
  1459. package/src/primitives-v2/Tag.stories.tsx +123 -0
  1460. package/src/primitives-v2/Tag.tsx +115 -0
  1461. package/src/primitives-v2/Toggle.stories.tsx +37 -0
  1462. package/src/primitives-v2/Toggle.tsx +93 -0
  1463. package/src/primitives-v2/Topbar.stories.tsx +65 -0
  1464. package/src/primitives-v2/Topbar.tsx +37 -0
  1465. package/src/primitives-v2/index.ts +31 -0
  1466. package/src/primitives-v2/utils.ts +9 -0
  1467. package/src/theme/BrandThemeProvider.tsx +122 -0
  1468. package/src/theme/QueryProvider.tsx +46 -0
  1469. package/src/theme/ThemeProvider.tsx +120 -0
  1470. package/src/theme/ThemeToggle.tsx +88 -0
  1471. package/src/theme/index.ts +4 -0
  1472. package/src/theme/m3.css +622 -0
  1473. package/src/theme/tokens.v2.css +559 -0
  1474. package/src/theme/typography.css +163 -0
  1475. package/src/theme/typography.stories.tsx +157 -0
  1476. package/src/theme/typography.tokens.ts +326 -0
  1477. package/src/types/react-big-calendar.d.ts +16 -0
@@ -0,0 +1,3796 @@
1
+ "use client"
2
+ import * as React from 'react'
3
+ import Link from 'next/link'
4
+ import { z } from 'zod'
5
+ import { useRouter } from 'next/navigation'
6
+ import { DataLoader } from '../primitives/DataLoader'
7
+ import { flash } from './FlashMessages'
8
+ import dynamic from 'next/dynamic'
9
+ import remarkGfm from 'remark-gfm'
10
+ import { FormHeader } from './forms/FormHeader'
11
+ import { FormFooter } from './forms/FormFooter'
12
+ import { Button } from '../primitives/button'
13
+ import { IconButton } from '../primitives/icon-button'
14
+ import {
15
+ Settings,
16
+ Layers,
17
+ Tag,
18
+ Sparkles,
19
+ Package,
20
+ Shirt,
21
+ Grid,
22
+ ShoppingBag,
23
+ ShoppingCart,
24
+ Store,
25
+ Users,
26
+ Briefcase,
27
+ Building,
28
+ BookOpen,
29
+ Bookmark,
30
+ Camera,
31
+ Car,
32
+ Clock,
33
+ Cloud,
34
+ Compass,
35
+ CreditCard,
36
+ Database,
37
+ Flame,
38
+ Gift,
39
+ Globe,
40
+ Heart,
41
+ Info,
42
+ Key,
43
+ Map as MapIcon,
44
+ Palette,
45
+ Shield,
46
+ Star,
47
+ Truck,
48
+ Zap,
49
+ Coins,
50
+ } from 'lucide-react'
51
+ import { loadGeneratedFieldRegistrations } from './fields/registry'
52
+ import type { CustomFieldDefDto, CustomFieldDefinitionsPayload, CustomFieldsetDto } from './utils/customFieldDefs'
53
+ import { buildFormFieldsFromCustomFields, buildFormFieldFromCustomFieldDef } from './utils/customFieldForms'
54
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
55
+ import { TagsInput } from './inputs/TagsInput'
56
+ import { ComboboxInput } from './inputs/ComboboxInput'
57
+ import { format, parseISO } from 'date-fns'
58
+ import type { Locale } from 'date-fns'
59
+ import { DateTimePicker } from './inputs/DateTimePicker'
60
+ import { TimePicker } from './inputs/TimePicker'
61
+ import { DatePicker } from './inputs/DatePicker'
62
+ import { mapCrudServerErrorToFormErrors, parseServerMessage } from './utils/serverErrors'
63
+ import { withScopedApiRequestHeaders } from './utils/apiCall'
64
+ import type { CustomFieldDefLike } from '@open-mercato/shared/modules/entities/validation'
65
+ import type { MDEditorProps as UiWMDEditorProps } from '@uiw/react-md-editor'
66
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../primitives/dialog'
67
+ import { FieldDefinitionsManager, type FieldDefinitionsManagerHandle } from './custom-fields/FieldDefinitionsManager'
68
+ import { useConfirmDialog } from './confirm-dialog'
69
+ import { useInjectionSpotEvents, InjectionSpot, useInjectionWidgets } from './injection/InjectionSpot'
70
+ import { dispatchBackendMutationError } from './injection/mutationEvents'
71
+ import { VersionHistoryAction } from './version-history/VersionHistoryAction'
72
+ import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
73
+ import { cn } from '@open-mercato/shared/lib/utils'
74
+ import { useInjectionDataWidgets } from './injection/useInjectionDataWidgets'
75
+ import { InjectedField } from './injection/InjectedField'
76
+ import type { InjectionFieldDefinition, FieldContext } from '@open-mercato/shared/modules/widgets/injection'
77
+ import { evaluateInjectedVisibility } from './injection/visibility-utils'
78
+ import { ComponentReplacementHandles } from '@open-mercato/shared/modules/widgets/component-registry'
79
+ import { sanitizeHtmlRichText, sanitizeRichTextHref, sanitizeRichTextPasteContent } from './utils/richTextSanitizer'
80
+
81
+ // Stable empty options array to avoid creating a new [] every render
82
+ const EMPTY_OPTIONS: CrudFieldOption[] = []
83
+ const FOCUSABLE_SELECTOR =
84
+ '[data-crud-focus-target], input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
85
+ const CRUDFORM_EXTENDED_EVENTS_ENABLED = parseBooleanWithDefault(
86
+ process.env.NEXT_PUBLIC_OM_CRUDFORM_EXTENDED_EVENTS_ENABLED,
87
+ true,
88
+ )
89
+
90
+ function resolveInternalNavigationTarget(target: string | URL | null | undefined): string | null {
91
+ if (typeof window === 'undefined' || target == null) return null
92
+ try {
93
+ const url = target instanceof URL ? target : new URL(target, window.location.origin)
94
+ if (url.origin !== window.location.origin) return null
95
+ const currentPath = `${window.location.pathname}${window.location.search}`
96
+ const nextPath = `${url.pathname}${url.search}`
97
+ if (nextPath === currentPath) return null
98
+ return `${url.pathname}${url.search}${url.hash}`
99
+ } catch {
100
+ return null
101
+ }
102
+ }
103
+
104
+ function buildResolvedEntityIdsKey(entityId?: string, entityIds?: string[]): string {
105
+ const dedup = new Set<string>()
106
+ const list: string[] = []
107
+
108
+ if (Array.isArray(entityIds) && entityIds.length) {
109
+ entityIds.forEach((id) => {
110
+ const trimmed = typeof id === 'string' ? id.trim() : ''
111
+ if (!trimmed || dedup.has(trimmed)) return
112
+ dedup.add(trimmed)
113
+ list.push(trimmed)
114
+ })
115
+ } else if (typeof entityId === 'string' && entityId.trim().length > 0) {
116
+ list.push(entityId.trim())
117
+ }
118
+
119
+ return list.join('\0')
120
+ }
121
+
122
+ export type CrudFieldBase = {
123
+ id: string
124
+ label: string
125
+ placeholder?: string
126
+ description?: React.ReactNode // inline field-level help
127
+ required?: boolean
128
+ layout?: 'full' | 'half' | 'third'
129
+ disabled?: boolean
130
+ readOnly?: boolean
131
+ defaultValue?: unknown
132
+ }
133
+
134
+ export type CrudFieldOption = { value: string; label: string }
135
+
136
+ export type CrudBuiltinField = CrudFieldBase & {
137
+ type:
138
+ | 'text'
139
+ | 'password'
140
+ | 'textarea'
141
+ | 'checkbox'
142
+ | 'select'
143
+ | 'number'
144
+ | 'date'
145
+ | 'datepicker'
146
+ | 'datetime-local'
147
+ | 'datetime'
148
+ | 'time'
149
+ | 'tags'
150
+ | 'richtext'
151
+ | 'relation'
152
+ | 'combobox'
153
+ placeholder?: string
154
+ options?: CrudFieldOption[]
155
+ multiple?: boolean
156
+ listbox?: boolean
157
+ // for relation/select style fields; if provided, options are loaded on mount
158
+ loadOptions?: (query?: string) => Promise<CrudFieldOption[]>
159
+ // when type === 'richtext', choose editor implementation
160
+ editor?: 'simple' | 'uiw' | 'html'
161
+ // for text fields; provides datalist suggestions while allowing free-text input
162
+ suggestions?: string[]
163
+ // for combobox fields; allow custom values or restrict to suggestions only
164
+ allowCustomValues?: boolean
165
+ // for datetime/time fields
166
+ minuteStep?: number
167
+ minDate?: Date
168
+ maxDate?: Date
169
+ displayFormat?: string
170
+ closeOnSelect?: boolean
171
+ locale?: Locale
172
+ }
173
+
174
+ export type CrudCustomFieldRenderProps = {
175
+ id: string
176
+ value: unknown
177
+ error?: string
178
+ autoFocus?: boolean
179
+ disabled?: boolean
180
+ values?: Record<string, unknown>
181
+ setValue: (value: unknown) => void
182
+ // Optional helper to update other form values from within a custom field
183
+ setFormValue?: (id: string, value: unknown) => void
184
+ // Optional context for advanced custom inputs
185
+ entityId?: string
186
+ recordId?: string
187
+ }
188
+
189
+ export type CrudCustomField = CrudFieldBase & {
190
+ type: 'custom'
191
+ component: (props: CrudCustomFieldRenderProps) => React.ReactNode
192
+ }
193
+
194
+ export type CrudField = CrudBuiltinField | CrudCustomField
195
+
196
+ type CrudFormValues<TValues extends Record<string, unknown>> = Partial<TValues> & Record<string, unknown>
197
+
198
+ export type CrudFormSubmitContext = {
199
+ submitter?: {
200
+ id?: string
201
+ name?: string
202
+ value?: string
203
+ dataset: Record<string, string>
204
+ }
205
+ }
206
+
207
+ export type CrudFormProps<TValues extends Record<string, unknown>> = {
208
+ schema?: z.ZodType<TValues>
209
+ fields: CrudField[]
210
+ initialValues?: Partial<TValues>
211
+ submitLabel?: string
212
+ submitIcon?: React.ComponentType<{ className?: string }>
213
+ formId?: string
214
+ customFieldsLoadingMessage?: string
215
+ cancelHref?: string
216
+ successRedirect?: string
217
+ deleteRedirect?: string
218
+ onSubmit?: (values: TValues, context?: CrudFormSubmitContext) => Promise<void> | void
219
+ onDelete?: () => Promise<void> | void
220
+ // When true, shows Delete button whenever onDelete is provided, even without an id
221
+ deleteVisible?: boolean
222
+ // Legacy field-only grid toggle. Use `groups` for advanced layout.
223
+ twoColumn?: boolean
224
+ title?: string
225
+ backHref?: string
226
+ // Optional extra action buttons rendered next to Delete/Cancel/Save
227
+ // Useful for custom links like "Show Records" etc.
228
+ extraActions?: React.ReactNode
229
+ /** When provided, shows a Version History clock icon in the header that opens a side panel. */
230
+ versionHistory?: {
231
+ resourceKind: string
232
+ resourceId: string
233
+ canUndoRedo?: boolean
234
+ autoCheckAcl?: boolean
235
+ }
236
+ // When provided, CrudForm will fetch custom field definitions and append
237
+ // form-editable custom fields automatically to the provided `fields`.
238
+ entityId?: string
239
+ entityIds?: string[]
240
+ // Optional grouped layout rendered in two responsive columns (1 on mobile).
241
+ groups?: CrudFormGroup[]
242
+ // Loading state for the entire form (e.g., when loading record data)
243
+ isLoading?: boolean
244
+ loadingMessage?: string
245
+ // User-defined entity mode: all fields are custom, use bare keys (no cf_)
246
+ customEntity?: boolean
247
+ // Embedded mode hides outer chrome; useful for inline sections
248
+ embedded?: boolean
249
+ // Hide the footer action bar (Save/Cancel/Delete) when embedding in a custom layout
250
+ hideFooterActions?: boolean
251
+ // Optional custom content injected between the header actions and the form body
252
+ contentHeader?: React.ReactNode
253
+ readOnly?: boolean
254
+ readOnlyOverlay?: React.ReactNode
255
+ // Optional mapping of entityId -> form value key storing the selected fieldset code
256
+ customFieldsetBindings?: Record<string, { valueKey: string }>
257
+ // Optional injection spot ID for widget injection
258
+ injectionSpotId?: string
259
+ replacementHandle?: string
260
+ }
261
+
262
+ // Group-level custom component context
263
+ export type CrudFormGroupComponentProps = {
264
+ values: Record<string, unknown>
265
+ setValue: (id: string, v: unknown) => void
266
+ errors: Record<string, string>
267
+ }
268
+
269
+ // Special group kind for automatic Custom Fields section
270
+ export type CrudFormGroup = {
271
+ id: string
272
+ title?: string
273
+ column?: 1 | 2
274
+ description?: string
275
+ // Either list field ids, inline field configs, or mix of both
276
+ fields?: (CrudField | string)[]
277
+ // Inject a custom component into the group card
278
+ component?: (ctx: CrudFormGroupComponentProps) => React.ReactNode
279
+ // When kind === 'customFields', the group renders form-editable custom fields
280
+ kind?: 'customFields'
281
+ // When true, render component output inline without wrapping group chrome
282
+ bare?: boolean
283
+ }
284
+
285
+ function readByDotPath(source: Record<string, unknown> | undefined, path: string): unknown {
286
+ if (!source || !path) return undefined
287
+ if (Object.prototype.hasOwnProperty.call(source, path)) return source[path]
288
+ const segments = path.split('.').filter(Boolean)
289
+ let current: unknown = source
290
+ for (const segment of segments) {
291
+ if (!current || typeof current !== 'object' || Array.isArray(current)) return undefined
292
+ current = (current as Record<string, unknown>)[segment]
293
+ }
294
+ return current
295
+ }
296
+
297
+ function serializeIssuePath(path: ReadonlyArray<string | number | symbol>): string | null {
298
+ if (!Array.isArray(path) || path.length === 0) return null
299
+ const segments = path
300
+ .map((segment) => {
301
+ if (typeof segment === 'string' || typeof segment === 'number') {
302
+ const normalized = String(segment).trim()
303
+ return normalized.length > 0 ? normalized : null
304
+ }
305
+ return null
306
+ })
307
+ .filter((segment): segment is string => segment !== null)
308
+ return segments.length > 0 ? segments.join('.') : null
309
+ }
310
+
311
+ const FIELDSET_ICON_COMPONENTS: Record<string, React.ComponentType<{ className?: string }>> = {
312
+ layers: Layers,
313
+ tag: Tag,
314
+ sparkles: Sparkles,
315
+ package: Package,
316
+ shirt: Shirt,
317
+ grid: Grid,
318
+ shoppingBag: ShoppingBag,
319
+ shoppingCart: ShoppingCart,
320
+ store: Store,
321
+ users: Users,
322
+ briefcase: Briefcase,
323
+ building: Building,
324
+ bookOpen: BookOpen,
325
+ bookmark: Bookmark,
326
+ camera: Camera,
327
+ car: Car,
328
+ clock: Clock,
329
+ cloud: Cloud,
330
+ compass: Compass,
331
+ creditCard: CreditCard,
332
+ database: Database,
333
+ flame: Flame,
334
+ gift: Gift,
335
+ globe: Globe,
336
+ heart: Heart,
337
+ key: Key,
338
+ map: MapIcon,
339
+ palette: Palette,
340
+ shield: Shield,
341
+ star: Star,
342
+ truck: Truck,
343
+ zap: Zap,
344
+ coins: Coins,
345
+ }
346
+
347
+ type CustomFieldGroupLayout = {
348
+ code: string | null
349
+ label?: string
350
+ hint?: string
351
+ fields: CrudField[]
352
+ }
353
+
354
+ type CustomFieldSectionLayout = {
355
+ entityId: string
356
+ fieldsetCode: string | null
357
+ fieldset?: CustomFieldsetDto
358
+ title: string
359
+ description?: string
360
+ groups: CustomFieldGroupLayout[]
361
+ }
362
+
363
+ type CustomFieldEntityLayout = {
364
+ entityId: string
365
+ sections: CustomFieldSectionLayout[]
366
+ availableFieldsets: CustomFieldsetDto[]
367
+ singleFieldsetPerRecord: boolean
368
+ hasFieldsets: boolean
369
+ activeFieldset: string | null
370
+ }
371
+
372
+ class FieldDefinitionsManagerErrorBoundary extends React.Component<
373
+ { children: React.ReactNode; onClose: () => void; unavailableMessage: string },
374
+ { hasError: boolean }
375
+ > {
376
+ constructor(props: { children: React.ReactNode; onClose: () => void; unavailableMessage: string }) {
377
+ super(props)
378
+ this.state = { hasError: false }
379
+ }
380
+ static getDerivedStateFromError() {
381
+ return { hasError: true }
382
+ }
383
+ render() {
384
+ if (this.state.hasError) {
385
+ return (
386
+ <div className="flex flex-col items-center justify-center gap-3 p-6 text-sm text-muted-foreground">
387
+ <p>{this.props.unavailableMessage}</p>
388
+ <Button type="button" variant="outline" size="sm" onClick={this.props.onClose}>
389
+ Close
390
+ </Button>
391
+ </div>
392
+ )
393
+ }
394
+ return this.props.children
395
+ }
396
+ }
397
+
398
+ export function CrudForm<TValues extends Record<string, unknown>>({
399
+ schema,
400
+ fields,
401
+ initialValues,
402
+ submitLabel,
403
+ submitIcon,
404
+ formId: providedFormId,
405
+ customFieldsLoadingMessage,
406
+ cancelHref,
407
+ successRedirect,
408
+ deleteRedirect,
409
+ onSubmit,
410
+ onDelete,
411
+ deleteVisible,
412
+ twoColumn = false,
413
+ title,
414
+ backHref,
415
+ entityId,
416
+ entityIds,
417
+ groups,
418
+ isLoading = false,
419
+ loadingMessage,
420
+ customEntity = false,
421
+ embedded = false,
422
+ hideFooterActions = false,
423
+ extraActions,
424
+ versionHistory,
425
+ contentHeader,
426
+ readOnly = false,
427
+ readOnlyOverlay,
428
+ customFieldsetBindings,
429
+ injectionSpotId,
430
+ replacementHandle,
431
+ }: CrudFormProps<TValues>) {
432
+ // Ensure module field components are registered (client-side)
433
+ React.useEffect(() => { loadGeneratedFieldRegistrations().catch(() => {}) }, [])
434
+ const { confirm, ConfirmDialogElement } = useConfirmDialog()
435
+ const router = useRouter()
436
+ const t = useT()
437
+ const resolvedSubmitLabel = submitLabel ?? t('ui.forms.actions.save')
438
+ const resolvedLoadingMessage = loadingMessage ?? t('ui.forms.loading')
439
+ const resolvedCustomFieldsLoadingMessage = customFieldsLoadingMessage ?? resolvedLoadingMessage
440
+ const cancelLabel = t('ui.forms.actions.cancel')
441
+ const deleteLabel = t('ui.forms.actions.delete')
442
+ const savingLabel = t('ui.forms.status.saving')
443
+ const backLabel = t('ui.navigation.back')
444
+ const customFieldsLabel = t('entities.customFields.title')
445
+ const fieldsetSelectorLabel = t('entities.customFields.fieldsetSelectorLabel', 'Fieldset')
446
+ const emptyFieldsetMessage = t('entities.customFields.emptyFieldset', 'No fields defined for this fieldset.')
447
+ const defaultFieldsetLabel = t('entities.customFields.defaultFieldset', 'Default')
448
+ const manageFieldsetLabel = t('entities.customFields.manageFieldset', 'Manage fields')
449
+ const fieldsetDialogTitle = t('entities.customFields.manageDialogTitle', 'Edit custom fields')
450
+ const fieldsetDialogUnavailable = t('entities.customFields.manageDialogUnavailable', 'Field definitions page is unavailable.')
451
+ const hasVersionHistory = Boolean(versionHistory?.resourceKind)
452
+ const deleteConfirmMessage = hasVersionHistory
453
+ ? t('ui.forms.confirmDeleteWithUndo', 'Delete this record? You can restore it using version history.')
454
+ : t('ui.forms.confirmDelete')
455
+ const deleteSuccessMessage = t('ui.forms.flash.deleteSuccess')
456
+ const deleteErrorMessage = t('ui.forms.flash.deleteError')
457
+ const saveErrorMessage = t('ui.forms.flash.saveError')
458
+ const internalFormId = React.useId()
459
+ const formId = providedFormId ?? internalFormId
460
+ const formReadOnly = Boolean(readOnly)
461
+ const [values, setValues] = React.useState<CrudFormValues<TValues>>(
462
+ () => ({ ...(initialValues ?? {}) } as CrudFormValues<TValues>)
463
+ )
464
+ const valuesRef = React.useRef(values)
465
+ const [errors, setErrors] = React.useState<Record<string, string>>({})
466
+ const [pending, setPending] = React.useState(false)
467
+ const [formError, setFormError] = React.useState<string | null>(null)
468
+ const [dynamicOptions, setDynamicOptions] = React.useState<Record<string, CrudFieldOption[]>>({})
469
+ const [cfDefinitions, setCfDefinitions] = React.useState<CustomFieldDefDto[]>([])
470
+ const [cfMetadata, setCfMetadata] = React.useState<CustomFieldDefinitionsPayload | null>(null)
471
+ const [cfFieldsetSelections, setCfFieldsetSelections] = React.useState<Record<string, string | null>>({})
472
+ const [isLoadingCustomFields, setIsLoadingCustomFields] = React.useState(false)
473
+ const [customFieldDefsVersion, setCustomFieldDefsVersion] = React.useState(0)
474
+ const [fieldsetEditorTarget, setFieldsetEditorTarget] = React.useState<{ entityId: string; fieldsetCode: string | null; view: 'entity' | 'fieldset' } | null>(null)
475
+ const [isInDialog, setIsInDialog] = React.useState(false)
476
+ const rootRef = React.useRef<HTMLDivElement | null>(null)
477
+ const fieldsetManagerRef = React.useRef<FieldDefinitionsManagerHandle | null>(null)
478
+ const resolvedEntityIdsKey = React.useMemo(() => buildResolvedEntityIdsKey(entityId, entityIds), [entityId, entityIds])
479
+ const resolvedEntityIds = React.useMemo(
480
+ () => (resolvedEntityIdsKey ? resolvedEntityIdsKey.split('\0') : []),
481
+ [resolvedEntityIdsKey],
482
+ )
483
+ const primaryEntityId = resolvedEntityIds.length ? resolvedEntityIds[0] : null
484
+
485
+ // Injection spot events for widget lifecycle management
486
+ const resolvedInjectionSpotId = React.useMemo(() => {
487
+ if (injectionSpotId) return injectionSpotId
488
+ if (resolvedEntityIds.length) {
489
+ const normalized = resolvedEntityIds[0].replace(/[:]+/g, '.')
490
+ return `crud-form:${normalized}`
491
+ }
492
+ return undefined
493
+ }, [injectionSpotId, resolvedEntityIds])
494
+ const resolvedReplacementHandle = React.useMemo(() => {
495
+ if (replacementHandle) return replacementHandle
496
+ if (resolvedEntityIds.length) return ComponentReplacementHandles.crudForm(resolvedEntityIds[0].replace(/[:]+/g, '.'))
497
+ return ComponentReplacementHandles.crudForm('unknown')
498
+ }, [replacementHandle, resolvedEntityIds])
499
+ const headerInjectionSpotId = resolvedInjectionSpotId ? `${resolvedInjectionSpotId}:header` : undefined
500
+
501
+ const recordId = React.useMemo(() => {
502
+ const raw = values.id
503
+ if (typeof raw === 'string') return raw
504
+ if (typeof raw === 'number') return String(raw)
505
+ return undefined
506
+ }, [values])
507
+ const fallbackRecordId = recordId || (
508
+ versionHistory?.resourceId === undefined || versionHistory.resourceId === null
509
+ ? undefined
510
+ : String(versionHistory.resourceId).trim() || undefined
511
+ )
512
+
513
+ const operation = recordId ? 'update' : 'create'
514
+ const injectionContext = React.useMemo(() => ({
515
+ formId,
516
+ entityId: primaryEntityId,
517
+ resourceKind: versionHistory?.resourceKind,
518
+ resourceId: recordId ?? versionHistory?.resourceId,
519
+ recordId: fallbackRecordId,
520
+ isLoading,
521
+ pending,
522
+ operation,
523
+ }), [formId, primaryEntityId, versionHistory?.resourceKind, versionHistory?.resourceId, recordId, fallbackRecordId, isLoading, pending, operation])
524
+ const injectionContextRef = React.useRef(injectionContext)
525
+ React.useEffect(() => {
526
+ injectionContextRef.current = injectionContext
527
+ }, [injectionContext])
528
+ React.useEffect(() => {
529
+ valuesRef.current = values
530
+ }, [values])
531
+
532
+ const isDirtyRef = React.useRef(false)
533
+ const navigationPromptBypassRef = React.useRef(false)
534
+ const navigationConfirmPendingRef = React.useRef(false)
535
+ const submitNavigationBypassRef = React.useRef(false)
536
+ const submitNavigationBypassTimeoutRef = React.useRef<number | null>(null)
537
+ const [hasUnsavedChanges, setHasUnsavedChanges] = React.useState(false)
538
+ const popStateRollbackRef = React.useRef(false)
539
+
540
+ React.useEffect(() => {
541
+ if (embedded) {
542
+ isDirtyRef.current = false
543
+ setHasUnsavedChanges(false)
544
+ return
545
+ }
546
+ const snapshot = dirtyBaselineSnapshotRef.current
547
+ if (!snapshot) {
548
+ isDirtyRef.current = false
549
+ setHasUnsavedChanges(false)
550
+ return
551
+ }
552
+ const currentSnapshot = JSON.stringify(values)
553
+ const dirty = currentSnapshot !== snapshot
554
+ isDirtyRef.current = dirty
555
+ setHasUnsavedChanges(dirty)
556
+ }, [embedded, values])
557
+
558
+ const allowNextNavigation = React.useCallback(() => {
559
+ navigationPromptBypassRef.current = true
560
+ if (typeof window !== 'undefined') {
561
+ window.setTimeout(() => {
562
+ navigationPromptBypassRef.current = false
563
+ }, 0)
564
+ }
565
+ }, [])
566
+
567
+ const clearSubmitNavigationBypass = React.useCallback(() => {
568
+ submitNavigationBypassRef.current = false
569
+ if (typeof window !== 'undefined' && submitNavigationBypassTimeoutRef.current !== null) {
570
+ window.clearTimeout(submitNavigationBypassTimeoutRef.current)
571
+ submitNavigationBypassTimeoutRef.current = null
572
+ }
573
+ }, [])
574
+
575
+ const keepSubmitNavigationBypassAlive = React.useCallback(() => {
576
+ if (typeof window === 'undefined') return
577
+ submitNavigationBypassRef.current = true
578
+ if (submitNavigationBypassTimeoutRef.current !== null) {
579
+ window.clearTimeout(submitNavigationBypassTimeoutRef.current)
580
+ }
581
+ submitNavigationBypassTimeoutRef.current = window.setTimeout(() => {
582
+ submitNavigationBypassRef.current = false
583
+ submitNavigationBypassTimeoutRef.current = null
584
+ }, 1500)
585
+ }, [])
586
+
587
+ React.useEffect(() => {
588
+ return () => {
589
+ clearSubmitNavigationBypass()
590
+ }
591
+ }, [clearSubmitNavigationBypass])
592
+
593
+ const clearDirtyState = React.useCallback((snapshotSource?: Record<string, unknown>) => {
594
+ const source = snapshotSource ?? (valuesRef.current as Record<string, unknown>)
595
+ dirtyBaselineSnapshotRef.current = JSON.stringify(source)
596
+ isDirtyRef.current = false
597
+ setHasUnsavedChanges(false)
598
+ }, [])
599
+
600
+ const confirmUnsavedChanges = React.useCallback(async (): Promise<boolean> => {
601
+ if (!isDirtyRef.current || typeof window === 'undefined') return true
602
+ if (navigationPromptBypassRef.current || submitNavigationBypassRef.current) return true
603
+ if (navigationConfirmPendingRef.current) return false
604
+ navigationConfirmPendingRef.current = true
605
+ try {
606
+ const confirmed = await confirm({
607
+ title: t('ui.forms.confirmUnsavedChanges', 'You have unsaved changes. Are you sure you want to leave?'),
608
+ })
609
+ if (confirmed) allowNextNavigation()
610
+ return confirmed
611
+ } finally {
612
+ navigationConfirmPendingRef.current = false
613
+ }
614
+ }, [allowNextNavigation, confirm, t])
615
+
616
+ React.useEffect(() => {
617
+ if (embedded || !hasUnsavedChanges) return
618
+ const beforeUnloadHandler = (event: BeforeUnloadEvent) => {
619
+ if (!isDirtyRef.current) return
620
+ event.preventDefault()
621
+ event.returnValue = ''
622
+ }
623
+ window.addEventListener('beforeunload', beforeUnloadHandler)
624
+
625
+ const clickHandler = (event: MouseEvent) => {
626
+ if (event.defaultPrevented) return
627
+ if (!isDirtyRef.current) return
628
+ const anchor = (event.target as HTMLElement)?.closest?.('a[href]') as HTMLAnchorElement | null
629
+ if (!anchor) return
630
+ const href = anchor.getAttribute('href')
631
+ if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:')) return
632
+ if (anchor.target === '_blank' || event.metaKey || event.ctrlKey || event.shiftKey) return
633
+ const target = resolveInternalNavigationTarget(href)
634
+ if (!target) return
635
+ event.preventDefault()
636
+ event.stopPropagation()
637
+ if (navigationConfirmPendingRef.current) return
638
+ void confirmUnsavedChanges().then((confirmed) => {
639
+ if (!confirmed) return
640
+ clearDirtyState()
641
+ router.push(target)
642
+ })
643
+ }
644
+ document.addEventListener('click', clickHandler, true)
645
+
646
+ const originalPushState = window.history.pushState.bind(window.history)
647
+ const originalReplaceState = window.history.replaceState.bind(window.history)
648
+
649
+ const createHistoryInterceptor = (original: History['pushState']): History['pushState'] =>
650
+ ((data: unknown, unused: string, url?: string | URL | null) => {
651
+ const target = resolveInternalNavigationTarget(url ?? null)
652
+ if (!target || navigationPromptBypassRef.current || submitNavigationBypassRef.current) {
653
+ return original(data, unused, url)
654
+ }
655
+ if (navigationConfirmPendingRef.current) {
656
+ return undefined
657
+ }
658
+ void confirmUnsavedChanges().then((confirmed) => {
659
+ if (!confirmed) return
660
+ clearDirtyState()
661
+ original(data, unused, url)
662
+ })
663
+ return undefined
664
+ }) as History['pushState']
665
+
666
+ const popStateHandler = () => {
667
+ if (popStateRollbackRef.current) {
668
+ popStateRollbackRef.current = false
669
+ return
670
+ }
671
+ if (!isDirtyRef.current || navigationPromptBypassRef.current || submitNavigationBypassRef.current) return
672
+ popStateRollbackRef.current = true
673
+ window.history.go(1)
674
+ if (navigationConfirmPendingRef.current) return
675
+ void confirmUnsavedChanges().then((confirmed) => {
676
+ if (!confirmed) return
677
+ clearDirtyState()
678
+ allowNextNavigation()
679
+ window.history.back()
680
+ })
681
+ }
682
+
683
+ window.history.pushState = createHistoryInterceptor(originalPushState)
684
+ window.history.replaceState = createHistoryInterceptor(originalReplaceState)
685
+ window.addEventListener('popstate', popStateHandler)
686
+
687
+ return () => {
688
+ window.removeEventListener('beforeunload', beforeUnloadHandler)
689
+ document.removeEventListener('click', clickHandler, true)
690
+ window.removeEventListener('popstate', popStateHandler)
691
+ window.history.pushState = originalPushState
692
+ window.history.replaceState = originalReplaceState
693
+ }
694
+ }, [allowNextNavigation, clearDirtyState, confirmUnsavedChanges, embedded, hasUnsavedChanges, router])
695
+
696
+ const { widgets: injectionWidgets } = useInjectionWidgets(resolvedInjectionSpotId, {
697
+ context: injectionContext,
698
+ triggerOnLoad: true,
699
+ })
700
+ const { widgets: injectedFieldWidgets } = useInjectionDataWidgets(
701
+ resolvedInjectionSpotId ? `${resolvedInjectionSpotId}:fields` : '__disabled__:fields'
702
+ )
703
+
704
+ const { triggerEvent: triggerInjectionEvent } = useInjectionSpotEvents(resolvedInjectionSpotId ?? '', injectionWidgets)
705
+ const extendedInjectionEventsEnabled = CRUDFORM_EXTENDED_EVENTS_ENABLED && Boolean(resolvedInjectionSpotId)
706
+
707
+ const transformValidationErrors = React.useCallback(
708
+ async (fieldErrors: Record<string, string>): Promise<Record<string, string>> => {
709
+ if (!extendedInjectionEventsEnabled || !Object.keys(fieldErrors).length) return fieldErrors
710
+ try {
711
+ const result = await triggerInjectionEvent(
712
+ 'transformValidation',
713
+ fieldErrors as unknown as TValues,
714
+ injectionContextRef.current,
715
+ { originalData: valuesRef.current as TValues },
716
+ )
717
+ const transformed = result.data
718
+ if (!transformed || typeof transformed !== 'object' || Array.isArray(transformed)) return fieldErrors
719
+ return Object.fromEntries(
720
+ Object.entries(transformed as Record<string, unknown>).map(([key, value]) => [key, String(value)]),
721
+ )
722
+ } catch (err) {
723
+ console.error('[CrudForm] Error in transformValidation:', err)
724
+ return fieldErrors
725
+ }
726
+ },
727
+ [extendedInjectionEventsEnabled, triggerInjectionEvent],
728
+ )
729
+
730
+ const translateValidationMessage = React.useCallback(
731
+ (message: string | null | undefined): string => {
732
+ if (typeof message !== 'string') return ''
733
+ const trimmed = message.trim()
734
+ if (!trimmed) return ''
735
+ return t(trimmed, trimmed)
736
+ },
737
+ [t],
738
+ )
739
+
740
+ const translateValidationErrors = React.useCallback(
741
+ (fieldErrors: Record<string, string>): Record<string, string> =>
742
+ Object.fromEntries(
743
+ Object.entries(fieldErrors).map(([fieldId, message]) => [fieldId, translateValidationMessage(message)]),
744
+ ),
745
+ [translateValidationMessage],
746
+ )
747
+
748
+ const mapDefsForValidation = React.useCallback(
749
+ (definitions: CustomFieldDefDto[]): CustomFieldDefLike[] =>
750
+ definitions.map((definition) => ({
751
+ key: definition.key,
752
+ kind: definition.kind,
753
+ configJson: {
754
+ validation: Array.isArray(definition.validation) ? definition.validation : [],
755
+ },
756
+ })),
757
+ [],
758
+ )
759
+
760
+ const canNavigateTo = React.useCallback(
761
+ async (target: string): Promise<boolean> => {
762
+ if (!extendedInjectionEventsEnabled) return true
763
+ try {
764
+ const result = await triggerInjectionEvent(
765
+ 'onBeforeNavigate',
766
+ valuesRef.current as TValues,
767
+ injectionContextRef.current,
768
+ { target },
769
+ )
770
+ if (!result.ok) {
771
+ flash(result.message || t('ui.forms.flash.saveBlocked', 'Save blocked by validation'), 'error')
772
+ return false
773
+ }
774
+ return true
775
+ } catch (err) {
776
+ const message = err instanceof Error && err.message ? err.message : t('ui.forms.flash.saveBlocked', 'Save blocked by validation')
777
+ flash(message, 'error')
778
+ return false
779
+ }
780
+ },
781
+ [extendedInjectionEventsEnabled, t, triggerInjectionEvent],
782
+ )
783
+
784
+ const navigateWithGuard = React.useCallback(
785
+ async (target: string) => {
786
+ if (!target) return
787
+ const allowed = await canNavigateTo(target)
788
+ if (allowed) {
789
+ allowNextNavigation()
790
+ router.push(target)
791
+ }
792
+ },
793
+ [allowNextNavigation, canNavigateTo, router],
794
+ )
795
+
796
+ React.useEffect(() => {
797
+ if (!extendedInjectionEventsEnabled || typeof window === 'undefined') return
798
+ const handleEvent = (event: Event) => {
799
+ const customEvent = event as CustomEvent<unknown>
800
+ void triggerInjectionEvent('onAppEvent', valuesRef.current as TValues, injectionContextRef.current, {
801
+ appEvent: customEvent.detail,
802
+ }).catch((err) => {
803
+ console.error('[CrudForm] Error in onAppEvent:', err)
804
+ })
805
+ }
806
+ window.addEventListener('om:event', handleEvent as EventListener)
807
+ return () => {
808
+ window.removeEventListener('om:event', handleEvent as EventListener)
809
+ }
810
+ }, [extendedInjectionEventsEnabled, triggerInjectionEvent])
811
+
812
+ React.useEffect(() => {
813
+ if (!extendedInjectionEventsEnabled || typeof document === 'undefined') return
814
+ const emitVisibility = () => {
815
+ void triggerInjectionEvent('onVisibilityChange', valuesRef.current as TValues, injectionContextRef.current, {
816
+ visible: document.visibilityState === 'visible',
817
+ }).catch((err) => {
818
+ console.error('[CrudForm] Error in onVisibilityChange:', err)
819
+ })
820
+ }
821
+ document.addEventListener('visibilitychange', emitVisibility)
822
+ emitVisibility()
823
+ return () => {
824
+ document.removeEventListener('visibilitychange', emitVisibility)
825
+ }
826
+ }, [extendedInjectionEventsEnabled, triggerInjectionEvent])
827
+
828
+ React.useEffect(() => {
829
+ if (!extendedInjectionEventsEnabled) return
830
+ const root = rootRef.current
831
+ if (!root || typeof window === 'undefined') return
832
+ const handleClickCapture = (event: MouseEvent) => {
833
+ if (event.defaultPrevented) return
834
+ if (event.button !== 0) return
835
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
836
+ const targetElement = event.target instanceof Element ? event.target : null
837
+ const linkElement = targetElement?.closest('a[href]')
838
+ if (!(linkElement instanceof HTMLAnchorElement)) return
839
+ if (!root.contains(linkElement)) return
840
+ if (linkElement.target && linkElement.target !== '_self') return
841
+ const rawHref = linkElement.getAttribute('href')
842
+ if (!rawHref || rawHref.startsWith('#')) return
843
+ let target = rawHref
844
+ if (rawHref.startsWith('http://') || rawHref.startsWith('https://')) {
845
+ try {
846
+ const parsed = new URL(rawHref)
847
+ if (parsed.origin !== window.location.origin) return
848
+ target = `${parsed.pathname}${parsed.search}${parsed.hash}`
849
+ } catch {
850
+ return
851
+ }
852
+ } else if (!rawHref.startsWith('/')) {
853
+ return
854
+ }
855
+ event.preventDefault()
856
+ void navigateWithGuard(target)
857
+ }
858
+ root.addEventListener('click', handleClickCapture, true)
859
+ return () => {
860
+ root.removeEventListener('click', handleClickCapture, true)
861
+ }
862
+ }, [extendedInjectionEventsEnabled, navigateWithGuard])
863
+
864
+ React.useEffect(() => {
865
+ const root = rootRef.current
866
+ if (!root) return
867
+ setIsInDialog(Boolean(root.closest('[data-dialog-content]')))
868
+ }, [])
869
+ const dialogFooterClass = isInDialog
870
+ ? 'sticky bottom-0 left-0 right-0 z-20 -mx-6 px-6 bg-card border-t border-border/70 py-2 sm:-mx-6 sm:px-6'
871
+ : ''
872
+ const dialogFormPadding = isInDialog ? 'pb-4' : ''
873
+
874
+ const buildCustomFieldsManageHref = React.useCallback(
875
+ (targetEntityId: string | null) => {
876
+ if (!targetEntityId) return null
877
+ try {
878
+ const encoded = encodeURIComponent(targetEntityId)
879
+ return customEntity ? `/backend/entities/user/${encoded}` : `/backend/entities/system/${encoded}`
880
+ } catch {
881
+ return null
882
+ }
883
+ },
884
+ [customEntity],
885
+ )
886
+
887
+ const refreshCustomFieldDefinitions = React.useCallback(() => {
888
+ setCustomFieldDefsVersion((prev) => prev + 1)
889
+ }, [])
890
+
891
+ // Unified delete handler with confirmation
892
+ const handleDelete = React.useCallback(async () => {
893
+ if (!onDelete) return
894
+ const deletePayload = values as TValues
895
+ try {
896
+ const confirmed = await confirm({
897
+ title: deleteConfirmMessage,
898
+ variant: 'destructive',
899
+ })
900
+ if (!confirmed) return
901
+
902
+ let injectionRequestHeaders: Record<string, string> | undefined
903
+ if (resolvedInjectionSpotId) {
904
+ try {
905
+ const result = await triggerInjectionEvent('onBeforeDelete', deletePayload, injectionContext)
906
+ if (!result.ok) {
907
+ try {
908
+ if (typeof window !== 'undefined') {
909
+ dispatchBackendMutationError({
910
+ contextId: formId,
911
+ formId,
912
+ error: result.details ?? result,
913
+ })
914
+ window.dispatchEvent(new CustomEvent('om:crud-save-error', {
915
+ detail: {
916
+ formId,
917
+ error: result.details ?? result,
918
+ },
919
+ }))
920
+ }
921
+ } catch {
922
+ // ignore event dispatch failures
923
+ }
924
+ if (result.fieldErrors && Object.keys(result.fieldErrors).length) {
925
+ const transformedErrors = await transformValidationErrors(result.fieldErrors)
926
+ setErrors(translateValidationErrors(transformedErrors))
927
+ }
928
+ const message = result.message || t('ui.forms.flash.saveBlocked', 'Save blocked by validation')
929
+ flash(message, 'error')
930
+ return
931
+ }
932
+ injectionRequestHeaders = result.requestHeaders
933
+ } catch (err) {
934
+ console.error('[CrudForm] Error in onBeforeDelete:', err)
935
+ flash(t('ui.forms.flash.saveBlocked', 'Save blocked by validation'), 'error')
936
+ return
937
+ }
938
+ }
939
+
940
+ setPending(true)
941
+ if (resolvedInjectionSpotId) {
942
+ try {
943
+ await triggerInjectionEvent('onDelete', deletePayload, injectionContext)
944
+ } catch (err) {
945
+ console.error('[CrudForm] Error in onDelete:', err)
946
+ flash(t('ui.forms.flash.saveBlocked', 'Save blocked by validation'), 'error')
947
+ return
948
+ }
949
+ }
950
+
951
+ if (injectionRequestHeaders && Object.keys(injectionRequestHeaders).length > 0) {
952
+ await withScopedApiRequestHeaders(injectionRequestHeaders, async () => {
953
+ await onDelete()
954
+ })
955
+ } else {
956
+ await onDelete()
957
+ }
958
+
959
+ if (resolvedInjectionSpotId) {
960
+ try {
961
+ await triggerInjectionEvent('onAfterDelete', deletePayload, injectionContext)
962
+ } catch (err) {
963
+ console.error('[CrudForm] Error in onAfterDelete:', err)
964
+ }
965
+ }
966
+ try { flash(deleteSuccessMessage, 'success') } catch {}
967
+ // Redirect if requested by caller
968
+ if (typeof deleteRedirect === 'string' && deleteRedirect) {
969
+ await navigateWithGuard(deleteRedirect)
970
+ }
971
+ } catch (err) {
972
+ if (resolvedInjectionSpotId) {
973
+ try {
974
+ await triggerInjectionEvent('onDeleteError', deletePayload, injectionContext, { error: err })
975
+ } catch (hookError) {
976
+ console.error('[CrudForm] Error in onDeleteError:', hookError)
977
+ }
978
+ }
979
+ try {
980
+ if (typeof window !== 'undefined') {
981
+ dispatchBackendMutationError({
982
+ contextId: formId,
983
+ formId,
984
+ error: err,
985
+ })
986
+ window.dispatchEvent(new CustomEvent('om:crud-save-error', {
987
+ detail: {
988
+ formId,
989
+ error: err,
990
+ },
991
+ }))
992
+ }
993
+ } catch {
994
+ // ignore event dispatch failures
995
+ }
996
+ const message = err instanceof Error && err.message ? err.message : deleteErrorMessage
997
+ try { flash(message, 'error') } catch {}
998
+ } finally {
999
+ setPending(false)
1000
+ }
1001
+ }, [
1002
+ confirm,
1003
+ deleteConfirmMessage,
1004
+ deleteErrorMessage,
1005
+ deleteRedirect,
1006
+ deleteSuccessMessage,
1007
+ formId,
1008
+ injectionContext,
1009
+ onDelete,
1010
+ resolvedInjectionSpotId,
1011
+ navigateWithGuard,
1012
+ t,
1013
+ transformValidationErrors,
1014
+ triggerInjectionEvent,
1015
+ values,
1016
+ ])
1017
+
1018
+ // Determine whether this form is creating a new record (no `id` yet)
1019
+ const isNewRecord = React.useMemo(() => {
1020
+ const rawId = values.id
1021
+ if (rawId === undefined || rawId === null) return true
1022
+ return typeof rawId === 'string' ? rawId.trim().length === 0 : false
1023
+ }, [values])
1024
+ const showDelete = Boolean(onDelete) && (typeof deleteVisible === 'boolean' ? deleteVisible : !isNewRecord)
1025
+ const versionHistoryEnabled = Boolean(versionHistory?.resourceId && String(versionHistory.resourceId).trim().length > 0)
1026
+ const versionHistoryAction = (
1027
+ <VersionHistoryAction
1028
+ config={versionHistoryEnabled ? versionHistory! : null}
1029
+ t={t}
1030
+ canUndoRedo={versionHistory?.canUndoRedo}
1031
+ autoCheckAcl={versionHistory?.autoCheckAcl}
1032
+ />
1033
+ )
1034
+ const headerInjectionAction = headerInjectionSpotId ? (
1035
+ <InjectionSpot
1036
+ spotId={headerInjectionSpotId}
1037
+ context={injectionContext}
1038
+ data={values}
1039
+ onDataChange={(newData) => setValues(newData as CrudFormValues<TValues>)}
1040
+ disabled={pending}
1041
+ />
1042
+ ) : null
1043
+ const headerExtraActions = versionHistoryEnabled || headerInjectionAction || extraActions ? (
1044
+ <>
1045
+ {versionHistoryEnabled ? versionHistoryAction : null}
1046
+ {headerInjectionAction}
1047
+ {extraActions}
1048
+ </>
1049
+ ) : undefined
1050
+
1051
+ // Auto-append custom fields for this entityId
1052
+ React.useEffect(() => {
1053
+ let cancelled = false
1054
+ async function load() {
1055
+ if (!resolvedEntityIds.length) {
1056
+ setCfDefinitions([])
1057
+ setCfMetadata(null)
1058
+ setIsLoadingCustomFields(false)
1059
+ return
1060
+ }
1061
+
1062
+ setIsLoadingCustomFields(true)
1063
+ try {
1064
+ const mod = await import('./utils/customFieldForms')
1065
+ const { definitions, metadata } = await mod.fetchCustomFieldFormStructure(resolvedEntityIds, undefined, { bareIds: customEntity })
1066
+ if (!cancelled) {
1067
+ setCfDefinitions(definitions)
1068
+ setCfMetadata(metadata)
1069
+ setCfFieldsetSelections((prev) => {
1070
+ const next: Record<string, string | null> = {}
1071
+ let changed = false
1072
+ resolvedEntityIds.forEach((entityId) => {
1073
+ const existing = prev[entityId]
1074
+ const fieldsets = metadata.fieldsetsByEntity?.[entityId] ?? []
1075
+ const defaultSelection = fieldsets[0]?.code ?? null
1076
+ const value = existing !== undefined ? existing : defaultSelection
1077
+ next[entityId] = value
1078
+ if (existing !== value) changed = true
1079
+ })
1080
+ if (Object.keys(prev).length !== Object.keys(next).length) changed = true
1081
+ return changed ? next : prev
1082
+ })
1083
+ setIsLoadingCustomFields(false)
1084
+ }
1085
+ } catch {
1086
+ if (!cancelled) {
1087
+ setCfDefinitions([])
1088
+ setCfMetadata(null)
1089
+ setIsLoadingCustomFields(false)
1090
+ }
1091
+ }
1092
+ }
1093
+ load()
1094
+ return () => { cancelled = true }
1095
+ }, [resolvedEntityIds, customEntity, customFieldDefsVersion])
1096
+
1097
+ React.useEffect(() => {
1098
+ if (!customFieldsetBindings) return
1099
+ setCfFieldsetSelections((prev) => {
1100
+ let changed = false
1101
+ const next = { ...prev }
1102
+ resolvedEntityIds.forEach((entityId) => {
1103
+ const binding = customFieldsetBindings[entityId]
1104
+ if (!binding) return
1105
+ const raw = values[binding.valueKey]
1106
+ if (typeof raw === 'string' && raw.trim().length > 0) {
1107
+ const normalized = raw.trim()
1108
+ if (next[entityId] !== normalized) {
1109
+ next[entityId] = normalized
1110
+ changed = true
1111
+ }
1112
+ }
1113
+ })
1114
+ return changed ? next : prev
1115
+ })
1116
+ }, [customFieldsetBindings, resolvedEntityIds, values])
1117
+
1118
+ const fieldsetsByEntity = cfMetadata?.fieldsetsByEntity ?? {}
1119
+ const entitySettings = cfMetadata?.entitySettings ?? {}
1120
+
1121
+ const { cfFields, customFieldLayout } = React.useMemo(() => {
1122
+ if (!cfDefinitions.length) return { cfFields: [], customFieldLayout: [] as CustomFieldEntityLayout[] }
1123
+ const aggregated: CrudField[] = []
1124
+ const layout: CustomFieldEntityLayout[] = []
1125
+ const defsByEntity = new globalThis.Map<string, CustomFieldDefDto[]>()
1126
+ cfDefinitions.forEach((def) => {
1127
+ const entityId = typeof def.entityId === 'string' && def.entityId.trim().length
1128
+ ? def.entityId.trim()
1129
+ : resolvedEntityIds[0]
1130
+ if (!entityId) return
1131
+ const bucket = defsByEntity.get(entityId) ?? []
1132
+ bucket.push(def)
1133
+ defsByEntity.set(entityId, bucket)
1134
+ })
1135
+
1136
+ const buildSection = (
1137
+ entityId: string,
1138
+ fieldsetCode: string | null,
1139
+ defList: CustomFieldDefDto[],
1140
+ fieldset?: CustomFieldsetDto,
1141
+ ): CustomFieldSectionLayout | null => {
1142
+ if (!defList.length) return null
1143
+ const groupsMap = new globalThis.Map<string, CustomFieldGroupLayout>()
1144
+ const order: string[] = []
1145
+ const fieldsetGroupMap = new globalThis.Map<string, { title?: string; hint?: string; code: string }>()
1146
+ if (Array.isArray(fieldset?.groups)) {
1147
+ fieldset.groups.forEach((group) => {
1148
+ if (!group?.code) return
1149
+ fieldsetGroupMap.set(group.code, { code: group.code, title: group.title, hint: group.hint })
1150
+ })
1151
+ }
1152
+ const sortedDefs = [...defList].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0))
1153
+ const ensureBucket = (code: string | null, def: CustomFieldDefDto): CustomFieldGroupLayout => {
1154
+ const key = code ?? '__default__'
1155
+ let bucket = groupsMap.get(key)
1156
+ if (!bucket) {
1157
+ const fallbackMeta = code ? fieldsetGroupMap.get(code) : undefined
1158
+ const directMeta = code ? def.group : undefined
1159
+ const label =
1160
+ code === null
1161
+ ? undefined
1162
+ : directMeta?.title || fallbackMeta?.title || directMeta?.code || fallbackMeta?.code || code
1163
+ const hint = directMeta?.hint || fallbackMeta?.hint
1164
+ bucket = { code, label, hint, fields: [] }
1165
+ groupsMap.set(key, bucket)
1166
+ order.push(key)
1167
+ } else if (code && !bucket.label) {
1168
+ const fallbackMeta = fieldsetGroupMap.get(code)
1169
+ const directMeta = def.group ?? undefined
1170
+ bucket.label = directMeta?.title || fallbackMeta?.title || directMeta?.code || fallbackMeta?.code || bucket.label
1171
+ bucket.hint = directMeta?.hint || fallbackMeta?.hint || bucket.hint
1172
+ }
1173
+ return bucket
1174
+ }
1175
+ sortedDefs.forEach((definition) => {
1176
+ const field = buildFormFieldFromCustomFieldDef(definition, { bareIds: customEntity })
1177
+ if (!field) return
1178
+ aggregated.push(field)
1179
+ const bucket = ensureBucket(definition.group?.code ?? null, definition)
1180
+ bucket.fields.push(field)
1181
+ })
1182
+ const groups = order
1183
+ .map((key) => groupsMap.get(key)!)
1184
+ .filter((group) => group.fields.length > 0)
1185
+ if (!groups.length) return null
1186
+ return {
1187
+ entityId,
1188
+ fieldsetCode,
1189
+ fieldset,
1190
+ title: fieldset?.label ?? customFieldsLabel,
1191
+ description: fieldset?.description,
1192
+ groups,
1193
+ }
1194
+ }
1195
+
1196
+ const entityIds = resolvedEntityIds.length ? resolvedEntityIds : Array.from(defsByEntity.keys())
1197
+ entityIds.forEach((entityId) => {
1198
+ const defsForEntity = defsByEntity.get(entityId) ?? []
1199
+ if (!defsForEntity.length) return
1200
+ const availableFieldsets = fieldsetsByEntity[entityId] ?? []
1201
+ const hasFieldsets = availableFieldsets.length > 0
1202
+ const singleFieldsetPerRecord =
1203
+ entitySettings[entityId]?.singleFieldsetPerRecord !== false
1204
+ const defsByFieldset = new globalThis.Map<string | null, CustomFieldDefDto[]>()
1205
+ defsForEntity.forEach((def) => {
1206
+ const memberships = Array.isArray(def.fieldsets)
1207
+ ? def.fieldsets
1208
+ .filter((entry): entry is string => typeof entry === 'string')
1209
+ .map((entry) => entry.trim())
1210
+ .filter((entry) => entry.length > 0)
1211
+ : []
1212
+ if (memberships.length > 0) {
1213
+ memberships.forEach((code) => {
1214
+ const bucket = defsByFieldset.get(code) ?? []
1215
+ bucket.push(def)
1216
+ defsByFieldset.set(code, bucket)
1217
+ })
1218
+ return
1219
+ }
1220
+ const code = typeof def.fieldset === 'string' && def.fieldset.trim().length > 0 ? def.fieldset.trim() : null
1221
+ const bucket = defsByFieldset.get(code) ?? []
1222
+ bucket.push(def)
1223
+ defsByFieldset.set(code, bucket)
1224
+ })
1225
+ const sections: CustomFieldSectionLayout[] = []
1226
+
1227
+ const createEmptySection = (code: string | null): CustomFieldSectionLayout => {
1228
+ const fieldset = code ? availableFieldsets.find((fs) => fs.code === code) : undefined
1229
+ return {
1230
+ entityId,
1231
+ fieldsetCode: code,
1232
+ fieldset,
1233
+ title: fieldset?.label ?? customFieldsLabel,
1234
+ description: fieldset?.description,
1235
+ groups: [],
1236
+ }
1237
+ }
1238
+
1239
+ if (!hasFieldsets) {
1240
+ const fallbackDefs =
1241
+ defsByFieldset.get(null) ?? Array.from(defsByFieldset.values()).flat()
1242
+ const section = buildSection(entityId, null, fallbackDefs, undefined)
1243
+ if (section) sections.push(section)
1244
+ } else if (singleFieldsetPerRecord) {
1245
+ const availableCodes = availableFieldsets.map((fs) => fs.code)
1246
+ const activeFieldset =
1247
+ cfFieldsetSelections[entityId] && availableCodes.includes(cfFieldsetSelections[entityId]!)
1248
+ ? cfFieldsetSelections[entityId]
1249
+ : availableFieldsets[0]?.code ?? null
1250
+ const targetDefs = activeFieldset ? defsByFieldset.get(activeFieldset) ?? [] : defsByFieldset.get(null) ?? []
1251
+ const targetSection = activeFieldset
1252
+ ? buildSection(
1253
+ entityId,
1254
+ activeFieldset,
1255
+ targetDefs,
1256
+ availableFieldsets.find((fs) => fs.code === activeFieldset),
1257
+ )
1258
+ : buildSection(entityId, null, targetDefs, undefined)
1259
+ if (targetSection) {
1260
+ sections.push(targetSection)
1261
+ } else if (activeFieldset) {
1262
+ sections.push(createEmptySection(activeFieldset))
1263
+ }
1264
+ const unassigned = defsByFieldset.get(null)
1265
+ if (unassigned?.length && activeFieldset) {
1266
+ const generalSection = buildSection(entityId, null, unassigned, undefined)
1267
+ if (generalSection) sections.push(generalSection)
1268
+ }
1269
+ } else {
1270
+ availableFieldsets.forEach((fieldset) => {
1271
+ const list = defsByFieldset.get(fieldset.code) ?? []
1272
+ const section = buildSection(entityId, fieldset.code, list, fieldset)
1273
+ if (section) sections.push(section)
1274
+ })
1275
+ const unassigned = defsByFieldset.get(null)
1276
+ if (unassigned?.length) {
1277
+ const section = buildSection(entityId, null, unassigned, undefined)
1278
+ if (section) sections.push(section)
1279
+ }
1280
+ }
1281
+
1282
+ if (!sections.length && hasFieldsets) {
1283
+ const fallbackCode = availableFieldsets[0]?.code ?? null
1284
+ sections.push(createEmptySection(fallbackCode))
1285
+ }
1286
+
1287
+ layout.push({
1288
+ entityId,
1289
+ sections,
1290
+ availableFieldsets,
1291
+ singleFieldsetPerRecord,
1292
+ hasFieldsets,
1293
+ activeFieldset: cfFieldsetSelections[entityId] ?? availableFieldsets[0]?.code ?? null,
1294
+ })
1295
+ })
1296
+
1297
+ return { cfFields: aggregated, customFieldLayout: layout }
1298
+ }, [
1299
+ cfDefinitions,
1300
+ cfFieldsetSelections,
1301
+ customEntity,
1302
+ customFieldsLabel,
1303
+ entitySettings,
1304
+ fieldsetsByEntity,
1305
+ resolvedEntityIds,
1306
+ ])
1307
+
1308
+ const injectedFieldDefinitions = React.useMemo<InjectionFieldDefinition[]>(() => {
1309
+ const definitions: InjectionFieldDefinition[] = []
1310
+ for (const widget of injectedFieldWidgets) {
1311
+ if (!('fields' in widget)) continue
1312
+ for (const field of widget.fields ?? []) {
1313
+ definitions.push(field as InjectionFieldDefinition)
1314
+ }
1315
+ }
1316
+ return definitions
1317
+ }, [injectedFieldWidgets])
1318
+
1319
+ const injectedFieldContext = React.useMemo<FieldContext>(() => {
1320
+ const recordValues = values as Record<string, unknown>
1321
+ const organizationId = typeof recordValues.organizationId === 'string' ? recordValues.organizationId : null
1322
+ const tenantId = typeof recordValues.tenantId === 'string' ? recordValues.tenantId : null
1323
+ const userId = typeof recordValues.userId === 'string' ? recordValues.userId : null
1324
+ return {
1325
+ organizationId,
1326
+ tenantId,
1327
+ userId,
1328
+ record: recordValues,
1329
+ }
1330
+ }, [values])
1331
+
1332
+ const hiddenInjectedFieldIds = React.useMemo(() => {
1333
+ const hidden = new Set<string>()
1334
+ for (const definition of injectedFieldDefinitions) {
1335
+ if (!evaluateInjectedVisibility(definition.visibleWhen, values as Record<string, unknown>, injectedFieldContext)) {
1336
+ hidden.add(definition.id)
1337
+ }
1338
+ }
1339
+ return hidden
1340
+ }, [injectedFieldContext, injectedFieldDefinitions, values])
1341
+ const injectedFieldIdSet = React.useMemo(
1342
+ () => new Set(injectedFieldDefinitions.map((definition) => definition.id)),
1343
+ [injectedFieldDefinitions],
1344
+ )
1345
+
1346
+ const injectedCrudFields = React.useMemo<CrudField[]>(() => {
1347
+ return injectedFieldDefinitions.map((definition) => ({
1348
+ id: definition.id,
1349
+ label: definition.label,
1350
+ type: 'custom',
1351
+ readOnly: definition.readOnly,
1352
+ component: ({ value, setValue, values: formValues }) => (
1353
+ <InjectedField
1354
+ field={definition}
1355
+ value={value}
1356
+ onChange={(_, nextValue) => setValue(nextValue)}
1357
+ context={injectedFieldContext}
1358
+ formData={(formValues ?? values) as Record<string, unknown>}
1359
+ />
1360
+ ),
1361
+ }))
1362
+ }, [injectedFieldContext, injectedFieldDefinitions, values])
1363
+
1364
+ const allFields = React.useMemo(() => {
1365
+ const base = [...fields, ...injectedCrudFields]
1366
+
1367
+ if (groups) {
1368
+ const provided = new Set(base.map(f => f.id))
1369
+ for (const group of groups) {
1370
+ if (!group.fields) continue
1371
+ for (const entry of group.fields) {
1372
+ if (typeof entry === 'string') continue
1373
+ if (!provided.has(entry.id)) {
1374
+ provided.add(entry.id)
1375
+ base.push(entry)
1376
+ }
1377
+ }
1378
+ }
1379
+ }
1380
+ if (!cfFields.length) return base
1381
+ const provided = new Set(base.map(f => f.id))
1382
+ const extras = cfFields.filter(f => !provided.has(f.id))
1383
+ return [...base, ...extras]
1384
+ }, [fields, injectedCrudFields, cfFields, groups])
1385
+
1386
+ const fieldById = React.useMemo(() => {
1387
+ return new globalThis.Map(allFields.map((f) => [f.id, f]))
1388
+ }, [allFields])
1389
+
1390
+ const validateFieldOnBlur = React.useCallback(async (
1391
+ fieldId: string,
1392
+ sourceValues?: CrudFormValues<TValues>,
1393
+ ) => {
1394
+ if (formReadOnly) return
1395
+ const field = fieldById.get(fieldId)
1396
+ if (!field || field.disabled) return
1397
+ if (hiddenInjectedFieldIds.has(fieldId)) return
1398
+
1399
+ const nextValues = sourceValues ?? valuesRef.current
1400
+ const nextFieldErrors: Record<string, string> = {}
1401
+ const requiredMessage = t('ui.forms.errors.required')
1402
+ const value = nextValues[fieldId]
1403
+ const isArray = Array.isArray(value)
1404
+ const isString = typeof value === 'string'
1405
+ const empty =
1406
+ value === undefined ||
1407
+ value === null ||
1408
+ (isString && value.trim() === '') ||
1409
+ (isArray && value.length === 0) ||
1410
+ (field.type === 'checkbox' && value !== true)
1411
+
1412
+ if (field.required && empty) {
1413
+ nextFieldErrors[fieldId] = requiredMessage
1414
+ }
1415
+
1416
+ const customFieldKey = customEntity
1417
+ ? cfDefinitions.some((definition) => definition.key === fieldId)
1418
+ ? fieldId
1419
+ : null
1420
+ : fieldId.startsWith('cf_')
1421
+ ? fieldId.replace(/^cf_/, '')
1422
+ : null
1423
+
1424
+ if (!Object.keys(nextFieldErrors).length && customFieldKey) {
1425
+ const defsForField = cfDefinitions.filter((definition) => definition.key === customFieldKey)
1426
+ if (defsForField.length) {
1427
+ try {
1428
+ const { validateValuesAgainstDefs } = await import('@open-mercato/shared/modules/entities/validation')
1429
+ const validationResult = validateValuesAgainstDefs(
1430
+ { [customFieldKey]: value },
1431
+ mapDefsForValidation(defsForField),
1432
+ )
1433
+ if (!validationResult.ok) {
1434
+ const normalizedFieldErrors = customEntity
1435
+ ? Object.fromEntries(
1436
+ Object.entries(validationResult.fieldErrors).map(([key, message]) => [
1437
+ key.replace(/^cf_/, ''),
1438
+ String(message),
1439
+ ]),
1440
+ )
1441
+ : Object.fromEntries(
1442
+ Object.entries(validationResult.fieldErrors).map(([key, message]) => [key, String(message)]),
1443
+ )
1444
+ const transformedErrors = await transformValidationErrors(normalizedFieldErrors)
1445
+ Object.assign(nextFieldErrors, translateValidationErrors(transformedErrors))
1446
+ }
1447
+ } catch {
1448
+ // ignore client-side custom field validation if helper is unavailable
1449
+ }
1450
+ }
1451
+ }
1452
+
1453
+ if (!Object.keys(nextFieldErrors).length && schema) {
1454
+ const widgetValues = { ...(nextValues as Record<string, unknown>) }
1455
+ for (const hiddenId of hiddenInjectedFieldIds) {
1456
+ delete widgetValues[hiddenId]
1457
+ }
1458
+ const coreValues = { ...widgetValues }
1459
+ for (const injectedId of injectedFieldIdSet) {
1460
+ delete coreValues[injectedId]
1461
+ }
1462
+ const result = schema.safeParse(coreValues)
1463
+ if (!result.success) {
1464
+ const schemaFieldErrors: Record<string, string> = {}
1465
+ result.error.issues.forEach((issue) => {
1466
+ const path = serializeIssuePath(issue.path)
1467
+ if (!path) return
1468
+ if (
1469
+ path === fieldId ||
1470
+ path.startsWith(`${fieldId}.`) ||
1471
+ fieldId.startsWith(`${path}.`)
1472
+ ) {
1473
+ schemaFieldErrors[path] = issue.message
1474
+ }
1475
+ })
1476
+ if (Object.keys(schemaFieldErrors).length) {
1477
+ const transformedErrors = await transformValidationErrors(schemaFieldErrors)
1478
+ Object.assign(nextFieldErrors, translateValidationErrors(transformedErrors))
1479
+ }
1480
+ }
1481
+ }
1482
+
1483
+ setErrors((prev) => {
1484
+ const next = Object.fromEntries(
1485
+ Object.entries(prev).filter(([existingFieldId]) => !(
1486
+ existingFieldId === fieldId ||
1487
+ existingFieldId.startsWith(`${fieldId}.`) ||
1488
+ fieldId.startsWith(`${existingFieldId}.`)
1489
+ )),
1490
+ )
1491
+ for (const [key, message] of Object.entries(nextFieldErrors)) {
1492
+ next[key] = message
1493
+ }
1494
+ const same =
1495
+ Object.keys(next).length === Object.keys(prev).length &&
1496
+ Object.entries(next).every(([key, message]) => prev[key] === message)
1497
+ return same ? prev : next
1498
+ })
1499
+ }, [
1500
+ cfDefinitions,
1501
+ customEntity,
1502
+ fieldById,
1503
+ formReadOnly,
1504
+ hiddenInjectedFieldIds,
1505
+ injectedFieldIdSet,
1506
+ mapDefsForValidation,
1507
+ schema,
1508
+ t,
1509
+ transformValidationErrors,
1510
+ translateValidationErrors,
1511
+ ])
1512
+
1513
+ const allFieldsRef = React.useRef(allFields)
1514
+ allFieldsRef.current = allFields
1515
+
1516
+ const dynamicOptionLoaderKey = React.useMemo(() => {
1517
+ return allFields
1518
+ .filter(f => f.type !== 'custom' && typeof (f as CrudBuiltinField).loadOptions === 'function')
1519
+ .map(f => f.id)
1520
+ .join('\0')
1521
+ }, [allFields])
1522
+
1523
+ const injectionGroupCards = React.useMemo<CrudFormGroup[]>(() => {
1524
+ if (!injectionWidgets || injectionWidgets.length === 0) return []
1525
+ const pairs = injectionWidgets
1526
+ .filter((widget) => (widget.placement?.kind ?? 'stack') === 'group')
1527
+ .map((widget) => {
1528
+ const priority = typeof widget.placement?.priority === 'number' ? widget.placement.priority : 0
1529
+ const group: CrudFormGroup = {
1530
+ id: `widget:${widget.widgetId}`,
1531
+ title: widget.placement?.groupLabel ?? widget.module.metadata.title,
1532
+ description: widget.placement?.groupDescription ?? widget.module.metadata.description,
1533
+ column: widget.placement?.column === 2 ? 2 : 1,
1534
+ component: () => (
1535
+ <widget.module.Widget
1536
+ context={injectionContext}
1537
+ data={values as unknown as CrudFormValues<TValues>}
1538
+ onDataChange={(next) => setValues(next as CrudFormValues<TValues>)}
1539
+ disabled={pending}
1540
+ />
1541
+ ),
1542
+ }
1543
+ return { group, priority }
1544
+ })
1545
+ pairs.sort((a, b) => b.priority - a.priority)
1546
+ return pairs.map((p) => p.group)
1547
+ }, [injectionWidgets, injectionContext, pending, setValues, values])
1548
+
1549
+ const groupsWithInjectedFields = React.useMemo(() => {
1550
+ if (!groups || groups.length === 0 || injectedFieldDefinitions.length === 0) return groups
1551
+ const cloned = groups.map((group) => ({ ...group, fields: [...(group.fields ?? [])] }))
1552
+ const fallbackIndex = cloned.length - 1
1553
+ for (const definition of injectedFieldDefinitions) {
1554
+ const targetIndex = cloned.findIndex((group) => group.id === definition.group)
1555
+ const index = targetIndex >= 0 ? targetIndex : fallbackIndex
1556
+ if (targetIndex < 0 && process.env.NODE_ENV !== 'production') {
1557
+ console.warn(`[CrudForm] Injected field "${definition.id}" targets group "${definition.group}" which does not exist. Appended to last group.`)
1558
+ }
1559
+ if (index < 0) continue
1560
+ const fieldEntries = cloned[index].fields ?? []
1561
+ if (!fieldEntries.some((entry) => typeof entry === 'string' && entry === definition.id)) {
1562
+ fieldEntries.push(definition.id)
1563
+ }
1564
+ cloned[index].fields = fieldEntries
1565
+ }
1566
+ return cloned
1567
+ }, [groups, injectedFieldDefinitions])
1568
+
1569
+ const shouldAutoGroup = (!groupsWithInjectedFields || groupsWithInjectedFields.length === 0) && injectionGroupCards.length > 0
1570
+ const resolvedGroupsForLayout = React.useMemo(() => {
1571
+ const baseGroups = groupsWithInjectedFields && groupsWithInjectedFields.length ? groupsWithInjectedFields : []
1572
+ const autoGroup = shouldAutoGroup ? [{ id: '__auto-fields__', fields: allFields }] as CrudFormGroup[] : []
1573
+ return [...(baseGroups.length ? baseGroups : autoGroup), ...injectionGroupCards]
1574
+ }, [allFields, groupsWithInjectedFields, injectionGroupCards, shouldAutoGroup])
1575
+ const useGroupedLayout = resolvedGroupsForLayout.length > 0
1576
+ const stackedInjectionWidgets = React.useMemo(
1577
+ () => (injectionWidgets ?? []).filter((widget) => (widget.placement?.kind ?? 'stack') === 'stack'),
1578
+ [injectionWidgets],
1579
+ )
1580
+
1581
+ const placedCustomFieldIds = React.useMemo(() => {
1582
+ const placed = new Set<string>()
1583
+ for (const other of (groups ?? [])) {
1584
+ if (other.kind === 'customFields' || !other.fields) continue
1585
+ for (const entry of other.fields) {
1586
+ if (typeof entry === 'string') placed.add(entry)
1587
+ else if (entry && typeof (entry as CrudField).id === 'string') placed.add((entry as CrudField).id)
1588
+ }
1589
+ }
1590
+ return placed
1591
+ }, [groups])
1592
+
1593
+ const resolveGroupFields = React.useCallback((g: CrudFormGroup): CrudField[] => {
1594
+ if (g.kind === 'customFields') {
1595
+ return cfFields.filter((f) => !placedCustomFieldIds.has(f.id))
1596
+ }
1597
+
1598
+ const src = g.fields || []
1599
+ const result: CrudField[] = []
1600
+
1601
+ for (const item of src) {
1602
+ if (typeof item === 'string') {
1603
+ const found = fieldById.get(item)
1604
+ if (found) result.push(found)
1605
+ } else if (item) {
1606
+ result.push(item as CrudField)
1607
+ }
1608
+ }
1609
+
1610
+ return result
1611
+ }, [cfFields, fieldById, placedCustomFieldIds])
1612
+
1613
+ const customFieldsManageHref = React.useMemo(() => buildCustomFieldsManageHref(primaryEntityId), [buildCustomFieldsManageHref, primaryEntityId])
1614
+
1615
+ const customFieldsEmptyState = React.useMemo(() => {
1616
+ const text = t('entities.customFields.empty')
1617
+ const action = t('entities.customFields.addFirst')
1618
+ return (
1619
+ <div className="rounded-md border border-dashed border-muted-foreground/50 bg-muted/10 px-3 py-4 text-sm text-muted-foreground">
1620
+ <span>{text} </span>
1621
+ {customFieldsManageHref ? (
1622
+ <Link href={customFieldsManageHref} className="font-medium text-primary hover:underline">
1623
+ {action}
1624
+ </Link>
1625
+ ) : (
1626
+ <span className="font-medium text-foreground">{action}</span>
1627
+ )}
1628
+ </div>
1629
+ )
1630
+ }, [customFieldsManageHref, t])
1631
+
1632
+ const firstFieldId = React.useMemo(() => {
1633
+ if (useGroupedLayout) {
1634
+ const col1: CrudFormGroup[] = []
1635
+ const col2: CrudFormGroup[] = []
1636
+
1637
+ for (const g of resolvedGroupsForLayout) {
1638
+ if ((g.column ?? 1) === 2) col2.push(g)
1639
+ else col1.push(g)
1640
+ }
1641
+
1642
+ const scan = (list: CrudFormGroup[]) => {
1643
+ for (const group of list) {
1644
+ const resolved = resolveGroupFields(group)
1645
+ for (const field of resolved) {
1646
+ if (field?.id && !field.disabled) return field.id
1647
+ }
1648
+ }
1649
+ return null as string | null
1650
+ }
1651
+
1652
+ const fromCol1 = scan(col1)
1653
+ if (fromCol1) return fromCol1
1654
+ const fromCol2 = scan(col2)
1655
+ if (fromCol2) return fromCol2
1656
+ }
1657
+
1658
+ for (const field of allFields) {
1659
+ if (field?.id && !field.disabled) return field.id
1660
+ }
1661
+ return null
1662
+ }, [allFields, resolveGroupFields, resolvedGroupsForLayout, useGroupedLayout])
1663
+
1664
+ const requestSubmit = React.useCallback(() => {
1665
+ if (formReadOnly) return
1666
+ if (typeof document === 'undefined') return
1667
+ const form = document.getElementById(formId) as HTMLFormElement | null
1668
+ form?.requestSubmit()
1669
+ }, [formId, formReadOnly])
1670
+
1671
+ const lastFocusedFieldRef = React.useRef<string | null>(null)
1672
+ const lastErrorFieldRef = React.useRef<string | null>(null)
1673
+
1674
+ React.useEffect(() => {
1675
+ if (typeof window === 'undefined' || typeof document === 'undefined') return
1676
+
1677
+ if (isLoading || isLoadingCustomFields || formReadOnly) {
1678
+ lastFocusedFieldRef.current = null
1679
+ return
1680
+ }
1681
+
1682
+ if (!firstFieldId) return
1683
+ if (lastFocusedFieldRef.current === firstFieldId) return
1684
+
1685
+ const run = () => {
1686
+ const form = document.getElementById(formId)
1687
+ if (!form) return
1688
+
1689
+ // Do not steal focus if the user is already interacting with any element inside the form
1690
+ const active = typeof document !== 'undefined' ? (document.activeElement as HTMLElement | null) : null
1691
+ if (active && form.contains(active)) {
1692
+ return
1693
+ }
1694
+
1695
+ const container = form.querySelector<HTMLElement>(`[data-crud-field-id="${firstFieldId}"]`)
1696
+ const target =
1697
+ container?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR) ??
1698
+ form.querySelector<HTMLElement>(FOCUSABLE_SELECTOR)
1699
+
1700
+ if (target && typeof target.focus === 'function') {
1701
+ target.focus({ preventScroll: true })
1702
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
1703
+ try {
1704
+ target.select()
1705
+ } catch {}
1706
+ }
1707
+ lastFocusedFieldRef.current = firstFieldId
1708
+ }
1709
+ }
1710
+
1711
+ const frame =
1712
+ typeof window.requestAnimationFrame === 'function'
1713
+ ? window.requestAnimationFrame(run)
1714
+ : window.setTimeout(run, 0)
1715
+
1716
+ return () => {
1717
+ if (typeof window === 'undefined') return
1718
+ if (typeof window.cancelAnimationFrame === 'function') {
1719
+ window.cancelAnimationFrame(frame as number)
1720
+ } else {
1721
+ window.clearTimeout(frame as number)
1722
+ }
1723
+ }
1724
+ }, [firstFieldId, formId, formReadOnly, isLoading, isLoadingCustomFields])
1725
+
1726
+ React.useEffect(() => {
1727
+ if (typeof window === 'undefined' || typeof document === 'undefined') return
1728
+ const entries = Object.entries(errors)
1729
+ if (!entries.length) {
1730
+ lastErrorFieldRef.current = null
1731
+ return
1732
+ }
1733
+ const [fieldId] = entries[0]
1734
+ if (!fieldId || lastErrorFieldRef.current === fieldId) return
1735
+
1736
+ const form = document.getElementById(formId)
1737
+ if (!form) return
1738
+ const container = form.querySelector<HTMLElement>(`[data-crud-field-id="${fieldId}"]`)
1739
+ const target =
1740
+ container?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR) ??
1741
+ form.querySelector<HTMLElement>(`[name="${fieldId}"]`) ??
1742
+ container ??
1743
+ null
1744
+
1745
+ if (target && typeof target.focus === 'function') {
1746
+ target.focus()
1747
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
1748
+ try {
1749
+ target.select()
1750
+ } catch {}
1751
+ }
1752
+ lastErrorFieldRef.current = fieldId
1753
+ }
1754
+ }, [errors, formId])
1755
+
1756
+ const setValue = React.useCallback((id: string, nextValue: unknown) => {
1757
+ let nextData: CrudFormValues<TValues> | null = null
1758
+ setValues((prev) => {
1759
+ if (Object.is(prev[id], nextValue)) return prev
1760
+ nextData = { ...prev, [id]: nextValue } as CrudFormValues<TValues>
1761
+ valuesRef.current = nextData
1762
+ return nextData
1763
+ })
1764
+ const clearedMessages: string[] = []
1765
+ setErrors((prev) => {
1766
+ if (!Object.keys(prev).length) return prev
1767
+ let changed = false
1768
+ const nextEntries = Object.entries(prev).filter(([fieldId, message]) => {
1769
+ const shouldClear =
1770
+ fieldId === id ||
1771
+ fieldId.startsWith(`${id}.`) ||
1772
+ id.startsWith(`${fieldId}.`)
1773
+ if (shouldClear) {
1774
+ changed = true
1775
+ clearedMessages.push(message)
1776
+ }
1777
+ return !shouldClear
1778
+ })
1779
+ return changed ? Object.fromEntries(nextEntries) : prev
1780
+ })
1781
+ if (clearedMessages.length) {
1782
+ const highlightedMessage = t('ui.forms.errors.highlighted')
1783
+ const translatedMessages = new Set(
1784
+ clearedMessages
1785
+ .map((message) => translateValidationMessage(message))
1786
+ .filter((message) => message.trim().length > 0),
1787
+ )
1788
+ setFormError((prev) => {
1789
+ if (!prev) return prev
1790
+ const normalized = prev.trim()
1791
+ if (!normalized) return null
1792
+ if (normalized === highlightedMessage || translatedMessages.has(normalized)) {
1793
+ return null
1794
+ }
1795
+ return prev
1796
+ })
1797
+ }
1798
+ if (!nextData || !extendedInjectionEventsEnabled) return
1799
+ void triggerInjectionEvent('onFieldChange', nextData as TValues, injectionContextRef.current, {
1800
+ fieldId: id,
1801
+ fieldValue: nextValue,
1802
+ }).then((result) => {
1803
+ if (!result.ok) return
1804
+ const change = result.fieldChange
1805
+ if (!change) return
1806
+ const updates: Record<string, unknown> = { ...(change.sideEffects ?? {}) }
1807
+ if (change.value !== undefined) {
1808
+ updates[id] = change.value
1809
+ }
1810
+ if (Object.keys(updates).length > 0) {
1811
+ setValues((prev) => {
1812
+ let changed = false
1813
+ const next = { ...prev } as Record<string, unknown>
1814
+ for (const [key, value] of Object.entries(updates)) {
1815
+ if (Object.is(next[key], value)) continue
1816
+ next[key] = value
1817
+ changed = true
1818
+ }
1819
+ return changed ? (next as CrudFormValues<TValues>) : prev
1820
+ })
1821
+ }
1822
+ for (const message of change.messages ?? []) {
1823
+ flash(message.text, message.severity)
1824
+ }
1825
+ }).catch((err) => {
1826
+ console.error('[CrudForm] Error in onFieldChange:', err)
1827
+ })
1828
+ }, [extendedInjectionEventsEnabled, t, translateValidationMessage, triggerInjectionEvent])
1829
+
1830
+ const handleFieldsetSelectionChange = React.useCallback(
1831
+ (entityId: string, nextCode: string | null) => {
1832
+ setCfFieldsetSelections((prev) => ({ ...prev, [entityId]: nextCode }))
1833
+ const bindingKey = customFieldsetBindings?.[entityId]?.valueKey
1834
+ if (bindingKey) {
1835
+ setValue(bindingKey, nextCode ?? undefined)
1836
+ }
1837
+ },
1838
+ [customFieldsetBindings, setValue],
1839
+ )
1840
+
1841
+ const handleOpenFieldsetEditor = React.useCallback(
1842
+ (entityId: string, fieldsetCode: string | null, view: 'entity' | 'fieldset' = 'entity') => {
1843
+ const href = buildCustomFieldsManageHref(entityId)
1844
+ if (!href) return
1845
+ setFieldsetEditorTarget({ entityId, fieldsetCode, view })
1846
+ },
1847
+ [buildCustomFieldsManageHref],
1848
+ )
1849
+
1850
+ const appliedInitialValuesSnapshotRef = React.useRef<string | undefined>(undefined)
1851
+ const dirtyBaselineSnapshotRef = React.useRef<string | undefined>(undefined)
1852
+ React.useLayoutEffect(() => {
1853
+ if (!initialValues) return
1854
+ const snapshot = JSON.stringify(initialValues)
1855
+ if (appliedInitialValuesSnapshotRef.current === snapshot) return
1856
+ appliedInitialValuesSnapshotRef.current = snapshot
1857
+ let mergedValues: CrudFormValues<TValues> | null = null
1858
+ setValues((prev) => {
1859
+ const merged = { ...prev, ...initialValues } as CrudFormValues<TValues>
1860
+ for (const definition of injectedFieldDefinitions) {
1861
+ if (merged[definition.id] !== undefined) continue
1862
+ const extracted = readByDotPath(initialValues as Record<string, unknown>, definition.id)
1863
+ if (extracted !== undefined) {
1864
+ ;(merged as Record<string, unknown>)[definition.id] = extracted
1865
+ }
1866
+ }
1867
+ mergedValues = merged
1868
+ return mergedValues
1869
+ })
1870
+ if (mergedValues) {
1871
+ dirtyBaselineSnapshotRef.current = JSON.stringify(mergedValues)
1872
+ }
1873
+ if (!extendedInjectionEventsEnabled || !mergedValues) return
1874
+ let cancelled = false
1875
+ const run = async () => {
1876
+ try {
1877
+ const result = await triggerInjectionEvent(
1878
+ 'transformDisplayData',
1879
+ mergedValues as TValues,
1880
+ injectionContextRef.current,
1881
+ )
1882
+ const transformed = result.data
1883
+ if (cancelled || !transformed) return
1884
+ dirtyBaselineSnapshotRef.current = JSON.stringify(transformed as Record<string, unknown>)
1885
+ setValues(transformed as CrudFormValues<TValues>)
1886
+ } catch (err) {
1887
+ console.error('[CrudForm] Error in transformDisplayData:', err)
1888
+ }
1889
+ }
1890
+ void run()
1891
+ return () => {
1892
+ cancelled = true
1893
+ }
1894
+ }, [extendedInjectionEventsEnabled, initialValues, injectedFieldDefinitions, triggerInjectionEvent])
1895
+
1896
+ // Apply custom field defaults on create flows (one-time, ref-guarded).
1897
+ // Mode is determined from the host-provided initialValues prop, not from
1898
+ // the reactive values.id (which lags on async-loading edit pages).
1899
+ // We also wait for isLoading to settle so async edit pages that start with
1900
+ // initialValues={} don't get mis-detected as create flows.
1901
+ const cfDefaultsAppliedRef = React.useRef(false)
1902
+ const initialValuesHasId = React.useMemo(() => {
1903
+ if (!initialValues) return false
1904
+ const raw = (initialValues as Record<string, unknown>).id
1905
+ return raw !== undefined && raw !== null && raw !== ''
1906
+ }, [initialValues])
1907
+
1908
+ React.useEffect(() => {
1909
+ // Do not fire while the host is still loading the record
1910
+ if (isLoading) return
1911
+ // Edit flow: initialValues contains an id — never apply defaults
1912
+ if (initialValuesHasId) return
1913
+ // Wait until custom field definitions have loaded
1914
+ if (!cfDefinitions.length) return
1915
+ // One-shot guard: do not reapply if already done
1916
+ if (cfDefaultsAppliedRef.current) return
1917
+ cfDefaultsAppliedRef.current = true
1918
+
1919
+ const defaults: Record<string, unknown> = {}
1920
+ const prefix = customEntity ? '' : 'cf_'
1921
+ for (const def of cfDefinitions) {
1922
+ if (def.defaultValue === undefined || def.defaultValue === null) continue
1923
+ const fieldId = `${prefix}${def.key}`
1924
+ defaults[fieldId] = def.defaultValue
1925
+ }
1926
+
1927
+ if (Object.keys(defaults).length === 0) return
1928
+
1929
+ let mergedValues: CrudFormValues<TValues> | null = null
1930
+ setValues((prev) => {
1931
+ const merged = { ...prev } as CrudFormValues<TValues>
1932
+ let applied = false
1933
+ for (const [fieldId, defaultVal] of Object.entries(defaults)) {
1934
+ // Skip if a value already exists (from initialValues or user input)
1935
+ if (merged[fieldId] !== undefined) continue
1936
+ ;(merged as Record<string, unknown>)[fieldId] = defaultVal
1937
+ applied = true
1938
+ }
1939
+ if (!applied) return prev
1940
+ mergedValues = merged
1941
+ return mergedValues
1942
+ })
1943
+
1944
+ // Update the dirty baseline so the form doesn't appear dirty from defaults
1945
+ if (mergedValues) {
1946
+ dirtyBaselineSnapshotRef.current = JSON.stringify(mergedValues)
1947
+ }
1948
+ }, [isLoading, initialValuesHasId, cfDefinitions, customEntity])
1949
+
1950
+ const markFormAsClean = React.useCallback((snapshotSource?: Record<string, unknown>) => {
1951
+ clearDirtyState(snapshotSource)
1952
+ }, [clearDirtyState])
1953
+
1954
+ const buildFieldsetEditorHref = React.useCallback(
1955
+ (includeViewParam: boolean) => {
1956
+ if (!fieldsetEditorTarget) return null
1957
+ const base = buildCustomFieldsManageHref(fieldsetEditorTarget.entityId)
1958
+ if (!base) return null
1959
+ const params: string[] = []
1960
+ if (fieldsetEditorTarget.fieldsetCode) {
1961
+ params.push(`fieldset=${encodeURIComponent(fieldsetEditorTarget.fieldsetCode)}`)
1962
+ }
1963
+ if (includeViewParam && fieldsetEditorTarget.view === 'fieldset') {
1964
+ params.push('view=fieldset')
1965
+ }
1966
+ if (!params.length) return base
1967
+ const connector = base.includes('?') ? '&' : '?'
1968
+ return `${base}${connector}${params.join('&')}`
1969
+ },
1970
+ [buildCustomFieldsManageHref, fieldsetEditorTarget],
1971
+ )
1972
+
1973
+ const fieldsetEditorFullHref = React.useMemo(() => buildFieldsetEditorHref(false), [buildFieldsetEditorHref])
1974
+
1975
+ const handleFieldsetDialogSave = React.useCallback(() => {
1976
+ if (!fieldsetManagerRef.current) return
1977
+ void fieldsetManagerRef.current.submit()
1978
+ }, [])
1979
+
1980
+ const handleSubmit = async (e: React.FormEvent) => {
1981
+ e.preventDefault()
1982
+ if (formReadOnly) return
1983
+ setFormError(null)
1984
+ setErrors({})
1985
+
1986
+ const requiredMessage = t('ui.forms.errors.required')
1987
+ const highlightedMessage = t('ui.forms.errors.highlighted')
1988
+
1989
+ // Make sure inputs that commit on blur flush their local state before submit.
1990
+ try {
1991
+ if (typeof document !== 'undefined') {
1992
+ const activeElement = document.activeElement
1993
+ if (activeElement instanceof HTMLElement) {
1994
+ activeElement.blur()
1995
+ await new Promise<void>((resolve) => setTimeout(resolve, 0))
1996
+ }
1997
+ }
1998
+ } catch {
1999
+ // ignore focus cleanup errors
2000
+ }
2001
+
2002
+ // Basic required-field validation when no zod schema is provided
2003
+ const requiredErrors: Record<string, string> = {}
2004
+ for (const field of allFields) {
2005
+ if (!field.required) continue
2006
+ if (field.disabled) continue
2007
+ if (hiddenInjectedFieldIds.has(field.id)) continue
2008
+ const v = values[field.id]
2009
+ const isArray = Array.isArray(v)
2010
+ const isString = typeof v === 'string'
2011
+ const empty =
2012
+ v === undefined ||
2013
+ v === null ||
2014
+ (isString && v.trim() === '') ||
2015
+ (isArray && v.length === 0) ||
2016
+ (field.type === 'checkbox' && v !== true)
2017
+ if (empty) requiredErrors[field.id] = requiredMessage
2018
+ }
2019
+ if (Object.keys(requiredErrors).length) {
2020
+ if (process.env.NODE_ENV !== 'production') {
2021
+ console.debug('[crud-form] Required field errors prevented submit', requiredErrors)
2022
+ }
2023
+ const transformedErrors = await transformValidationErrors(requiredErrors)
2024
+ setErrors(transformedErrors)
2025
+ flash(highlightedMessage, 'error')
2026
+ return
2027
+ }
2028
+
2029
+ // Custom fields validation via definitions (rules)
2030
+ if (resolvedEntityIds.length) {
2031
+ try {
2032
+ const defs = cfDefinitions.length
2033
+ ? cfDefinitions
2034
+ : await import('./utils/customFieldDefs').then((mod) => mod.fetchCustomFieldDefs(resolvedEntityIds))
2035
+ const { validateValuesAgainstDefs } = await import('@open-mercato/shared/modules/entities/validation')
2036
+ // Build values keyed by def.key for validation
2037
+ const cfValues: Record<string, unknown> = {}
2038
+ if (customEntity) {
2039
+ for (const def of defs) {
2040
+ if (Object.prototype.hasOwnProperty.call(values, def.key)) {
2041
+ cfValues[def.key] = values[def.key]
2042
+ }
2043
+ }
2044
+ } else {
2045
+ for (const [k, v] of Object.entries(values)) {
2046
+ if (k.startsWith('cf_')) cfValues[k.replace(/^cf_/, '')] = v
2047
+ }
2048
+ }
2049
+ const defsForValidation = mapDefsForValidation(defs)
2050
+ const result = validateValuesAgainstDefs(cfValues, defsForValidation)
2051
+ if (!result.ok) {
2052
+ if (customEntity) {
2053
+ const mapped: Record<string, string> = {}
2054
+ for (const [ek, ev] of Object.entries(result.fieldErrors)) mapped[ek.replace(/^cf_/, '')] = String(ev)
2055
+ const transformedErrors = await transformValidationErrors(mapped)
2056
+ setErrors((prev) => ({ ...prev, ...translateValidationErrors(transformedErrors) }))
2057
+ } else {
2058
+ const transformedErrors = await transformValidationErrors(
2059
+ Object.fromEntries(
2060
+ Object.entries(result.fieldErrors).map(([key, value]) => [key, String(value)]),
2061
+ ),
2062
+ )
2063
+ setErrors((prev) => ({ ...prev, ...translateValidationErrors(transformedErrors) }))
2064
+ }
2065
+ flash(highlightedMessage, 'error')
2066
+ return
2067
+ }
2068
+ } catch {
2069
+ // ignore validation errors if helper not available
2070
+ }
2071
+ }
2072
+
2073
+ const widgetValues = { ...(valuesRef.current as Record<string, unknown>) }
2074
+ for (const hiddenId of hiddenInjectedFieldIds) {
2075
+ delete widgetValues[hiddenId]
2076
+ }
2077
+ const coreValues = { ...widgetValues }
2078
+ for (const injectedId of injectedFieldIdSet) {
2079
+ delete coreValues[injectedId]
2080
+ }
2081
+
2082
+ let parsedValues: TValues
2083
+ if (schema) {
2084
+ const res = schema.safeParse(coreValues)
2085
+ if (!res.success) {
2086
+ const fieldErrors: Record<string, string> = {}
2087
+ res.error.issues.forEach((issue) => {
2088
+ const path = serializeIssuePath(issue.path)
2089
+ if (path) fieldErrors[path] = issue.message
2090
+ })
2091
+ if (process.env.NODE_ENV !== 'production') {
2092
+ console.debug('[crud-form] Schema validation failed', res.error.issues)
2093
+ }
2094
+ const transformedErrors = await transformValidationErrors(fieldErrors)
2095
+ setErrors(translateValidationErrors(transformedErrors))
2096
+ flash(highlightedMessage, 'error')
2097
+ return
2098
+ }
2099
+ parsedValues = res.data
2100
+ } else {
2101
+ parsedValues = coreValues as TValues
2102
+ }
2103
+ let submitValues = widgetValues as TValues
2104
+ let coreSubmitValues = parsedValues
2105
+ if (extendedInjectionEventsEnabled) {
2106
+ try {
2107
+ const result = await triggerInjectionEvent('transformFormData', submitValues, injectionContext)
2108
+ if (result.data) {
2109
+ submitValues = result.data as TValues
2110
+ const projectedCoreValues = { ...(result.data as Record<string, unknown>) }
2111
+ for (const injectedId of injectedFieldIdSet) {
2112
+ delete projectedCoreValues[injectedId]
2113
+ }
2114
+ coreSubmitValues = schema ? schema.parse(projectedCoreValues) : (projectedCoreValues as TValues)
2115
+ if (result.applyToForm) {
2116
+ setValues(result.data as CrudFormValues<TValues>)
2117
+ }
2118
+ }
2119
+ } catch (err) {
2120
+ console.error('[CrudForm] Error in transformFormData:', err)
2121
+ }
2122
+ }
2123
+
2124
+ // Trigger onBeforeSave event for injection widgets
2125
+ let injectionRequestHeaders: Record<string, string> | undefined
2126
+ if (resolvedInjectionSpotId) {
2127
+ try {
2128
+ const result = await triggerInjectionEvent('onBeforeSave', submitValues, injectionContext)
2129
+ if (!result.ok) {
2130
+ try {
2131
+ if (typeof window !== 'undefined') {
2132
+ dispatchBackendMutationError({
2133
+ contextId: formId,
2134
+ formId,
2135
+ error: result.details ?? result,
2136
+ })
2137
+ window.dispatchEvent(new CustomEvent('om:crud-save-error', {
2138
+ detail: {
2139
+ formId,
2140
+ error: result.details ?? result,
2141
+ },
2142
+ }))
2143
+ }
2144
+ } catch {
2145
+ // ignore event dispatch failures
2146
+ }
2147
+ if (result.fieldErrors && Object.keys(result.fieldErrors).length) {
2148
+ const transformedErrors = await transformValidationErrors(result.fieldErrors)
2149
+ setErrors(translateValidationErrors(transformedErrors))
2150
+ }
2151
+ const message = result.message || t('ui.forms.flash.saveBlocked', 'Save blocked by validation')
2152
+ flash(message, 'error')
2153
+ setPending(false)
2154
+ return
2155
+ }
2156
+ injectionRequestHeaders = result.requestHeaders
2157
+ } catch (err) {
2158
+ console.error('[CrudForm] Error in onBeforeSave:', err)
2159
+ flash(t('ui.forms.flash.saveBlocked', 'Save blocked by validation'), 'error')
2160
+ setPending(false)
2161
+ return
2162
+ }
2163
+ }
2164
+
2165
+ setPending(true)
2166
+
2167
+ const nativeSubmitter = 'nativeEvent' in e ? (e.nativeEvent as SubmitEvent).submitter : null
2168
+ const submitContext: CrudFormSubmitContext | undefined =
2169
+ nativeSubmitter instanceof HTMLElement
2170
+ ? {
2171
+ submitter: {
2172
+ id: nativeSubmitter.id || undefined,
2173
+ name:
2174
+ 'name' in nativeSubmitter && typeof nativeSubmitter.name === 'string' && nativeSubmitter.name.trim().length > 0
2175
+ ? nativeSubmitter.name
2176
+ : undefined,
2177
+ value:
2178
+ 'value' in nativeSubmitter && typeof nativeSubmitter.value === 'string' && nativeSubmitter.value.trim().length > 0
2179
+ ? nativeSubmitter.value
2180
+ : undefined,
2181
+ dataset: Object.fromEntries(
2182
+ Object.entries(nativeSubmitter.dataset).filter((entry): entry is [string, string] => typeof entry[1] === 'string'),
2183
+ ),
2184
+ },
2185
+ }
2186
+ : undefined
2187
+
2188
+ // Trigger onSave event for injection widgets
2189
+ if (resolvedInjectionSpotId) {
2190
+ try {
2191
+ await triggerInjectionEvent('onSave', submitValues, injectionContext)
2192
+ } catch (err) {
2193
+ console.error('[CrudForm] Error in onSave:', err)
2194
+ flash(t('ui.forms.flash.saveBlocked', 'Save blocked by validation'), 'error')
2195
+ setPending(false)
2196
+ return
2197
+ }
2198
+ }
2199
+
2200
+ try {
2201
+ submitNavigationBypassRef.current = true
2202
+ if (injectionRequestHeaders && Object.keys(injectionRequestHeaders).length > 0) {
2203
+ await withScopedApiRequestHeaders(injectionRequestHeaders, async () => {
2204
+ await onSubmit?.(coreSubmitValues, submitContext)
2205
+ })
2206
+ } else {
2207
+ await onSubmit?.(coreSubmitValues, submitContext)
2208
+ }
2209
+
2210
+ // Trigger onAfterSave event for injection widgets
2211
+ if (resolvedInjectionSpotId) {
2212
+ try {
2213
+ await triggerInjectionEvent('onAfterSave', submitValues, injectionContext)
2214
+ } catch (err) {
2215
+ console.error('[CrudForm] Error in onAfterSave:', err)
2216
+ }
2217
+ }
2218
+
2219
+ markFormAsClean(valuesRef.current as Record<string, unknown>)
2220
+ keepSubmitNavigationBypassAlive()
2221
+ if (successRedirect) await navigateWithGuard(successRedirect)
2222
+ } catch (err: unknown) {
2223
+ clearSubmitNavigationBypass()
2224
+ try {
2225
+ if (typeof window !== 'undefined') {
2226
+ dispatchBackendMutationError({
2227
+ contextId: formId,
2228
+ formId,
2229
+ error: err,
2230
+ })
2231
+ window.dispatchEvent(new CustomEvent('om:crud-save-error', {
2232
+ detail: {
2233
+ formId,
2234
+ error: err,
2235
+ },
2236
+ }))
2237
+ }
2238
+ } catch {
2239
+ // ignore event dispatch failures
2240
+ }
2241
+ const { message: helperMessage, fieldErrors: serverFieldErrors } = mapCrudServerErrorToFormErrors(err, { customEntity })
2242
+ const combinedFieldErrors = serverFieldErrors ?? {}
2243
+ const hasFieldErrors = Object.keys(combinedFieldErrors).length > 0
2244
+ const firstFieldMessage = hasFieldErrors
2245
+ ? (() => {
2246
+ const firstKey = Object.keys(combinedFieldErrors)[0]
2247
+ if (!firstKey) return null
2248
+ const value = combinedFieldErrors[firstKey]
2249
+ if (typeof value !== 'string' || !value.trim().length) return null
2250
+ return translateValidationMessage(value)
2251
+ })()
2252
+ : null
2253
+ if (hasFieldErrors) {
2254
+ const transformedErrors = await transformValidationErrors(combinedFieldErrors)
2255
+ setErrors(translateValidationErrors(transformedErrors))
2256
+ if (process.env.NODE_ENV !== 'production') {
2257
+ console.debug('[crud-form] Submission failed with field errors', transformedErrors)
2258
+ }
2259
+ }
2260
+
2261
+ let displayMessage = typeof helperMessage === 'string' && helperMessage.trim() ? helperMessage.trim() : ''
2262
+ if (hasFieldErrors) {
2263
+ const lowered = displayMessage.toLowerCase()
2264
+ const highlightedLower = highlightedMessage.toLowerCase()
2265
+ if (!displayMessage || lowered === 'invalid input' || lowered === highlightedLower) {
2266
+ displayMessage = firstFieldMessage ?? highlightedMessage
2267
+ }
2268
+ }
2269
+ if (!displayMessage && err instanceof Error && typeof err.message === 'string' && err.message.trim()) {
2270
+ displayMessage = err.message.trim()
2271
+ }
2272
+ if (!displayMessage) {
2273
+ displayMessage = hasFieldErrors ? highlightedMessage : saveErrorMessage
2274
+ }
2275
+ displayMessage = parseServerMessage(displayMessage)
2276
+ if (!hasFieldErrors) {
2277
+ flash(displayMessage, 'error')
2278
+ }
2279
+ setFormError(displayMessage)
2280
+ } finally {
2281
+ setPending(false)
2282
+ }
2283
+ }
2284
+
2285
+ // Stable key prevents infinite re-render loop (see #814) — do not depend on allFields directly.
2286
+ React.useEffect(() => {
2287
+ let cancelled = false
2288
+ const loadAll = async () => {
2289
+ const loaders = allFieldsRef.current
2290
+ .filter(
2291
+ (f): f is CrudBuiltinField & { loadOptions: NonNullable<CrudBuiltinField['loadOptions']> } =>
2292
+ f.type !== 'custom' && typeof f.loadOptions === 'function'
2293
+ )
2294
+ .map(async (f) => {
2295
+ try {
2296
+ const opts = await f.loadOptions()
2297
+ if (!cancelled) setDynamicOptions((prev) => ({ ...prev, [f.id]: opts }))
2298
+ } catch {
2299
+ // ignore
2300
+ }
2301
+ })
2302
+ await Promise.all(loaders)
2303
+ }
2304
+ loadAll()
2305
+ return () => {
2306
+ cancelled = true
2307
+ }
2308
+ }, [dynamicOptionLoaderKey])
2309
+
2310
+ const loadFieldOptions = React.useCallback(async (field: CrudField, query?: string): Promise<CrudFieldOption[]> => {
2311
+ if (!('type' in field) || field.type === 'custom') return EMPTY_OPTIONS
2312
+ const builtin = field as CrudBuiltinField
2313
+ const loader = builtin.loadOptions
2314
+ if (typeof loader === 'function') {
2315
+ if (query === undefined && Array.isArray(dynamicOptions[field.id])) return dynamicOptions[field.id]
2316
+ try {
2317
+ const fetched = await loader(query)
2318
+ if (query === undefined) {
2319
+ setDynamicOptions((prev) => ({
2320
+ ...prev,
2321
+ [field.id]: fetched,
2322
+ }))
2323
+ }
2324
+ return fetched
2325
+ } catch {
2326
+ return builtin.options ?? EMPTY_OPTIONS
2327
+ }
2328
+ }
2329
+ return dynamicOptions[field.id] || builtin.options || EMPTY_OPTIONS
2330
+ }, [dynamicOptions])
2331
+
2332
+ const fieldOptionsById = React.useMemo(() => {
2333
+ const map = new globalThis.Map<string, CrudFieldOption[]>()
2334
+ for (const f of allFields) {
2335
+ if (!('type' in f) || f.type === 'custom') continue
2336
+ const builtin = f as CrudBuiltinField
2337
+ const staticOptions = builtin.options ?? EMPTY_OPTIONS
2338
+ const dynamic = dynamicOptions[f.id]
2339
+ if (dynamic && dynamic.length) {
2340
+ const merged: CrudFieldOption[] = []
2341
+ const seen = new Set<string>()
2342
+ for (const opt of staticOptions) {
2343
+ if (seen.has(opt.value)) continue
2344
+ seen.add(opt.value)
2345
+ merged.push(opt)
2346
+ }
2347
+ for (const opt of dynamic) {
2348
+ if (seen.has(opt.value)) continue
2349
+ seen.add(opt.value)
2350
+ merged.push(opt)
2351
+ }
2352
+ map.set(f.id, merged)
2353
+ } else if (staticOptions.length) {
2354
+ map.set(f.id, staticOptions)
2355
+ } else if (dynamic) {
2356
+ map.set(f.id, dynamic)
2357
+ }
2358
+ }
2359
+ return map
2360
+ }, [allFields, dynamicOptions])
2361
+
2362
+ // no auto-focus; let the browser/user manage focus
2363
+
2364
+ const usesResponsiveLayout = allFields.some(
2365
+ (field) => field.layout === 'half' || field.layout === 'third'
2366
+ )
2367
+ const grid = twoColumn
2368
+ ? 'grid grid-cols-1 lg:grid-cols-[7fr_3fr] gap-4'
2369
+ : usesResponsiveLayout
2370
+ ? 'grid grid-cols-1 gap-4 md:grid-cols-6'
2371
+ : 'grid grid-cols-1 gap-4'
2372
+
2373
+ // Helper to render a list of field configs
2374
+ const resolveLayoutClass = (layout?: CrudFieldBase['layout']) => {
2375
+ switch (layout) {
2376
+ case 'half':
2377
+ return 'md:col-span-3'
2378
+ case 'third':
2379
+ return 'md:col-span-2'
2380
+ default:
2381
+ return 'md:col-span-6'
2382
+ }
2383
+ }
2384
+
2385
+ const renderFields = (fieldList: CrudField[]) => {
2386
+ const usesResponsive = fieldList.some(
2387
+ (field) => field.layout === 'half' || field.layout === 'third'
2388
+ )
2389
+ const gridClass = usesResponsive ? 'grid grid-cols-1 gap-4 md:grid-cols-6' : 'grid grid-cols-1 gap-4'
2390
+ return (
2391
+ <div className={gridClass}>
2392
+ {fieldList.map((f) => {
2393
+ const layout = f.layout ?? 'full'
2394
+ const wrapperClassName = usesResponsive ? resolveLayoutClass(layout) : undefined
2395
+ return (
2396
+ <FieldControl
2397
+ key={f.id}
2398
+ field={f}
2399
+ value={values[f.id]}
2400
+ error={errors[f.id]}
2401
+ options={fieldOptionsById.get(f.id) || EMPTY_OPTIONS}
2402
+ setValue={setValue}
2403
+ onBlurRequest={(fieldId) => { void validateFieldOnBlur(fieldId) }}
2404
+ values={values}
2405
+ loadFieldOptions={loadFieldOptions}
2406
+ autoFocus={!formReadOnly && Boolean(firstFieldId && f.id === firstFieldId)}
2407
+ onSubmitRequest={requestSubmit}
2408
+ wrapperClassName={wrapperClassName}
2409
+ entityIdForField={primaryEntityId ?? undefined}
2410
+ recordId={recordId}
2411
+ />
2412
+ )
2413
+ })}
2414
+ </div>
2415
+ )
2416
+ }
2417
+
2418
+ const renderCustomFieldsContent = React.useCallback((): React.ReactNode[] => {
2419
+ if (!customFieldLayout.length) {
2420
+ return [
2421
+ <div key="custom-fields-empty" className="rounded-lg border bg-card p-4">
2422
+ {customFieldsEmptyState}
2423
+ </div>,
2424
+ ]
2425
+ }
2426
+
2427
+ const nodes: React.ReactNode[] = []
2428
+ const multipleEntities = customFieldLayout.length > 1
2429
+
2430
+ customFieldLayout.forEach((entityLayout) => {
2431
+ const manageHref = buildCustomFieldsManageHref(entityLayout.entityId)
2432
+ const showSelector =
2433
+ entityLayout.hasFieldsets &&
2434
+ entityLayout.singleFieldsetPerRecord &&
2435
+ entityLayout.availableFieldsets.length > 0
2436
+
2437
+ if (multipleEntities) {
2438
+ nodes.push(
2439
+ <div
2440
+ key={`custom-fields-entity-${entityLayout.entityId}`}
2441
+ className="px-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground"
2442
+ >
2443
+ {entityLayout.entityId}
2444
+ </div>,
2445
+ )
2446
+ }
2447
+
2448
+ if (showSelector) {
2449
+ nodes.push(
2450
+ <div key={`custom-fields-selector-${entityLayout.entityId}`} className="rounded-lg border bg-card p-4">
2451
+ <div className="flex flex-wrap items-center gap-2 text-sm">
2452
+ <label className="text-xs uppercase tracking-wide text-muted-foreground">
2453
+ {fieldsetSelectorLabel}
2454
+ </label>
2455
+ <select
2456
+ className="h-9 rounded border pl-3 pr-8 text-sm"
2457
+ value={entityLayout.activeFieldset ?? ''}
2458
+ onChange={(event) =>
2459
+ handleFieldsetSelectionChange(
2460
+ entityLayout.entityId,
2461
+ event.target.value || null,
2462
+ )}
2463
+ >
2464
+ <option value="">{defaultFieldsetLabel}</option>
2465
+ {entityLayout.availableFieldsets.map((fs) => (
2466
+ <option key={fs.code} value={fs.code}>
2467
+ {fs.label}
2468
+ </option>
2469
+ ))}
2470
+ </select>
2471
+ <IconButton
2472
+ variant="outline"
2473
+ className="text-muted-foreground hover:text-foreground"
2474
+ onClick={() =>
2475
+ handleOpenFieldsetEditor(entityLayout.entityId, entityLayout.activeFieldset ?? null, 'fieldset')}
2476
+ disabled={!manageHref}
2477
+ title={manageFieldsetLabel}
2478
+ >
2479
+ <Settings className="size-4" />
2480
+ </IconButton>
2481
+ </div>
2482
+ </div>,
2483
+ )
2484
+ }
2485
+
2486
+ if (entityLayout.sections.length) {
2487
+ entityLayout.sections.forEach((section) => {
2488
+ const FieldsetIcon = section.fieldset?.icon
2489
+ ? FIELDSET_ICON_COMPONENTS[section.fieldset.icon]
2490
+ : null
2491
+ const sectionKey = `${entityLayout.entityId}:${section.fieldsetCode ?? 'default'}`
2492
+ const manageDisabled = !manageHref
2493
+ nodes.push(
2494
+ <div key={sectionKey} className="rounded-lg border bg-card p-4 space-y-4">
2495
+ <div className="flex items-start justify-between gap-3">
2496
+ <div className="flex items-start gap-2">
2497
+ {FieldsetIcon ? (
2498
+ <FieldsetIcon className="size-5 text-muted-foreground" />
2499
+ ) : null}
2500
+ <div>
2501
+ <div className="text-sm font-medium">{section.title}</div>
2502
+ {section.description ? (
2503
+ <div className="text-xs text-muted-foreground">
2504
+ {section.description}
2505
+ </div>
2506
+ ) : null}
2507
+ </div>
2508
+ </div>
2509
+ <Button
2510
+ type="button"
2511
+ variant="muted"
2512
+ size="sm"
2513
+ className="text-xs"
2514
+ onClick={() => handleOpenFieldsetEditor(entityLayout.entityId, section.fieldsetCode, 'fieldset')}
2515
+ disabled={manageDisabled}
2516
+ >
2517
+ <Settings className="size-4" />
2518
+ {manageFieldsetLabel}
2519
+ </Button>
2520
+ </div>
2521
+ {section.groups.map((group) => {
2522
+ const groupKey = `${section.fieldsetCode ?? 'default'}:${group.code ?? 'default'}`
2523
+ const visibleFields = placedCustomFieldIds.size > 0
2524
+ ? group.fields.filter((f) => !placedCustomFieldIds.has(f.id))
2525
+ : group.fields
2526
+ if (!visibleFields.length) return null
2527
+ return (
2528
+ <div key={groupKey} className="space-y-2">
2529
+ {group.label ? (
2530
+ <div>
2531
+ <div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
2532
+ {group.label}
2533
+ </div>
2534
+ {group.hint ? (
2535
+ <div className="text-xs text-muted-foreground">{group.hint}</div>
2536
+ ) : null}
2537
+ </div>
2538
+ ) : null}
2539
+ {renderFields(visibleFields)}
2540
+ </div>
2541
+ )
2542
+ })}
2543
+ {!section.groups.length ? (
2544
+ <div className="text-xs text-muted-foreground">{emptyFieldsetMessage}</div>
2545
+ ) : null}
2546
+ </div>,
2547
+ )
2548
+ })
2549
+ } else {
2550
+ nodes.push(
2551
+ <div key={`custom-fields-empty-${entityLayout.entityId}`} className="rounded-lg border bg-card p-4">
2552
+ {customFieldsEmptyState}
2553
+ </div>,
2554
+ )
2555
+ }
2556
+ })
2557
+
2558
+ return nodes
2559
+ }, [
2560
+ buildCustomFieldsManageHref,
2561
+ customFieldLayout,
2562
+ customFieldsEmptyState,
2563
+ defaultFieldsetLabel,
2564
+ emptyFieldsetMessage,
2565
+ fieldsetSelectorLabel,
2566
+ handleFieldsetSelectionChange,
2567
+ handleOpenFieldsetEditor,
2568
+ manageFieldsetLabel,
2569
+ placedCustomFieldIds,
2570
+ renderFields,
2571
+ ])
2572
+
2573
+ const fieldsetManagerDialog = (
2574
+ <Dialog open={fieldsetEditorTarget !== null} onOpenChange={(open) => { if (!open) setFieldsetEditorTarget(null) }}>
2575
+ <DialogContent
2576
+ className="max-w-5xl w-full"
2577
+ aria-describedby={undefined}
2578
+ onKeyDown={(event) => {
2579
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
2580
+ event.preventDefault()
2581
+ handleFieldsetDialogSave()
2582
+ }
2583
+ }}
2584
+ >
2585
+ <DialogHeader>
2586
+ <DialogTitle>{fieldsetDialogTitle}</DialogTitle>
2587
+ </DialogHeader>
2588
+ {fieldsetEditorTarget ? (
2589
+ <FieldDefinitionsManagerErrorBoundary
2590
+ onClose={() => setFieldsetEditorTarget(null)}
2591
+ unavailableMessage={fieldsetDialogUnavailable}
2592
+ >
2593
+ <FieldDefinitionsManager
2594
+ ref={fieldsetManagerRef}
2595
+ entityId={fieldsetEditorTarget.entityId}
2596
+ initialFieldset={fieldsetEditorTarget.fieldsetCode}
2597
+ fullEditorHref={fieldsetEditorFullHref ?? undefined}
2598
+ onSaved={refreshCustomFieldDefinitions}
2599
+ onClose={() => setFieldsetEditorTarget(null)}
2600
+ />
2601
+ </FieldDefinitionsManagerErrorBoundary>
2602
+ ) : (
2603
+ <div className="flex h-full items-center justify-center text-sm text-muted-foreground px-4 text-center">
2604
+ {fieldsetDialogUnavailable}
2605
+ </div>
2606
+ )}
2607
+ </DialogContent>
2608
+ </Dialog>
2609
+ )
2610
+
2611
+ const handleReadOnlyFocusCapture = React.useCallback((event: React.FocusEvent<HTMLDivElement>) => {
2612
+ if (!formReadOnly) return
2613
+ const target = event.target
2614
+ if (target instanceof HTMLElement) target.blur()
2615
+ }, [formReadOnly])
2616
+
2617
+ const handleReadOnlyKeyDownCapture = React.useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
2618
+ if (!formReadOnly) return
2619
+ event.preventDefault()
2620
+ }, [formReadOnly])
2621
+
2622
+ const wrapFormBody = React.useCallback((children: React.ReactNode, className?: string) => {
2623
+ if (!formReadOnly) return children
2624
+ return (
2625
+ <div
2626
+ className={cn('relative', className)}
2627
+ onFocusCapture={handleReadOnlyFocusCapture}
2628
+ onKeyDownCapture={handleReadOnlyKeyDownCapture}
2629
+ >
2630
+ <div className="pointer-events-none select-none opacity-70">
2631
+ {children}
2632
+ </div>
2633
+ <div className="absolute inset-0 z-10 flex items-start justify-center rounded-lg bg-background/60 p-4 backdrop-blur-[1px] sm:p-6">
2634
+ {readOnlyOverlay ?? <div className="rounded-xl border border-border/70 bg-background/95 px-4 py-3 shadow-sm" />}
2635
+ </div>
2636
+ </div>
2637
+ )
2638
+ }, [formReadOnly, handleReadOnlyFocusCapture, handleReadOnlyKeyDownCapture, readOnlyOverlay])
2639
+
2640
+ // If groups are provided, render the two-column grouped layout
2641
+ if (useGroupedLayout) {
2642
+
2643
+ const col1: CrudFormGroup[] = []
2644
+ const col2: CrudFormGroup[] = []
2645
+ for (const g of resolvedGroupsForLayout) {
2646
+ if ((g.column ?? 1) === 2) col2.push(g)
2647
+ else col1.push(g)
2648
+ }
2649
+
2650
+ const renderGroupedCards = (items: CrudFormGroup[]) => {
2651
+ const nodes: React.ReactNode[] = []
2652
+ for (const g of items) {
2653
+ const isCustomFieldsGroup = g.kind === 'customFields'
2654
+ if (isCustomFieldsGroup) {
2655
+ if (isLoadingCustomFields) {
2656
+ nodes.push(
2657
+ <div key={`${g.id}-loading`} className="rounded-lg border bg-card p-4">
2658
+ <DataLoader
2659
+ isLoading
2660
+ loadingMessage={resolvedCustomFieldsLoadingMessage}
2661
+ spinnerSize="md"
2662
+ className="min-h-[1px]"
2663
+ >
2664
+ <div />
2665
+ </DataLoader>
2666
+ </div>,
2667
+ )
2668
+ continue
2669
+ }
2670
+ if (g.component) {
2671
+ nodes.push(
2672
+ <div key={`${g.id}-component`} className="rounded-lg border bg-card px-4 py-3">
2673
+ {g.component({ values, setValue, errors })}
2674
+ </div>,
2675
+ )
2676
+ }
2677
+ const renderedSections = renderCustomFieldsContent()
2678
+ if (renderedSections.length) nodes.push(...renderedSections)
2679
+ continue
2680
+ }
2681
+
2682
+ const componentNode = g.component ? g.component({ values, setValue, errors }) : null
2683
+ if (g.bare) {
2684
+ if (componentNode) {
2685
+ nodes.push(<React.Fragment key={g.id}>{componentNode}</React.Fragment>)
2686
+ }
2687
+ continue
2688
+ }
2689
+ const groupFields = resolveGroupFields(g)
2690
+ nodes.push(
2691
+ <div key={g.id} className="rounded-lg border bg-card px-4 py-3 space-y-3">
2692
+ {g.title ? (
2693
+ <div className="text-sm font-medium">{t(g.title, g.title)}</div>
2694
+ ) : null}
2695
+ {g.description ? <div className="text-xs text-muted-foreground">{t(g.description, g.description)}</div> : null}
2696
+ {componentNode ? (
2697
+ <div>{componentNode}</div>
2698
+ ) : null}
2699
+ <DataLoader
2700
+ isLoading={false}
2701
+ loadingMessage={resolvedLoadingMessage}
2702
+ spinnerSize="md"
2703
+ className="min-h-[1px]"
2704
+ >
2705
+ {groupFields.length > 0 ? renderFields(groupFields) : <div className="min-h-[1px]" />}
2706
+ </DataLoader>
2707
+ </div>,
2708
+ )
2709
+ }
2710
+ return nodes
2711
+ }
2712
+
2713
+ const col1Content = renderGroupedCards(col1)
2714
+ const col2Content = renderGroupedCards(col2)
2715
+ const hasSecondaryColumn = col2Content.length > 0
2716
+
2717
+ return (
2718
+ <div className="space-y-4" ref={rootRef} data-component-handle={resolvedReplacementHandle}>
2719
+ {!embedded ? (
2720
+ <FormHeader
2721
+ mode="edit"
2722
+ backHref={backHref}
2723
+ backLabel={backLabel}
2724
+ title={title}
2725
+ actions={{
2726
+ extraActions: headerExtraActions,
2727
+ showDelete: !formReadOnly && showDelete,
2728
+ onDelete: handleDelete, // NOSONAR — async→void assignment is valid TypeScript
2729
+ deleteLabel,
2730
+ cancelHref,
2731
+ cancelLabel,
2732
+ submit: formReadOnly ? undefined : { formId, pending: pending, label: resolvedSubmitLabel, pendingLabel: savingLabel, icon: submitIcon },
2733
+ }}
2734
+ />
2735
+ ) : headerExtraActions ? (
2736
+ <div className="flex justify-end gap-2 mb-2">{headerExtraActions}</div>
2737
+ ) : null}
2738
+ {contentHeader}
2739
+ <DataLoader
2740
+ isLoading={isLoading}
2741
+ loadingMessage={resolvedLoadingMessage}
2742
+ spinnerSize="md"
2743
+ className={embedded ? 'min-h-[1px]' : 'min-h-[400px]'}
2744
+ >
2745
+ {wrapFormBody(
2746
+ <form id={formId} onSubmit={handleSubmit} className={`space-y-4 ${dialogFormPadding}`}>
2747
+ {resolvedInjectionSpotId ? (
2748
+ <InjectionSpot
2749
+ spotId={resolvedInjectionSpotId}
2750
+ context={injectionContext}
2751
+ data={values}
2752
+ onDataChange={(newData) => setValues(newData as CrudFormValues<TValues>)}
2753
+ disabled={pending || formReadOnly}
2754
+ widgetsOverride={stackedInjectionWidgets}
2755
+ />
2756
+ ) : null}
2757
+ <div
2758
+ className={hasSecondaryColumn
2759
+ ? 'grid grid-cols-1 lg:grid-cols-[7fr_3fr] gap-4'
2760
+ : 'grid grid-cols-1 gap-4'}
2761
+ >
2762
+ <div className="space-y-3">{col1Content}</div>
2763
+ {hasSecondaryColumn ? <div className="space-y-3">{col2Content}</div> : null}
2764
+ </div>
2765
+ {formError && !Object.keys(errors).length ? <div className="text-sm text-red-600">{formError}</div> : null}
2766
+ {hideFooterActions || formReadOnly ? null : (
2767
+ <FormFooter
2768
+ embedded={embedded}
2769
+ className={dialogFooterClass}
2770
+ actions={{
2771
+ extraActions,
2772
+ showDelete: !embedded && showDelete,
2773
+ onDelete: handleDelete, // NOSONAR — async→void assignment is valid TypeScript
2774
+ deleteLabel,
2775
+ cancelHref: !embedded ? cancelHref : undefined,
2776
+ cancelLabel,
2777
+ submit: { pending: pending, label: resolvedSubmitLabel, pendingLabel: savingLabel, icon: submitIcon }
2778
+ }}
2779
+ />
2780
+ )}
2781
+ </form>,
2782
+ )}
2783
+ </DataLoader>
2784
+ {fieldsetManagerDialog}
2785
+ {ConfirmDialogElement}
2786
+ </div>
2787
+ )
2788
+ }
2789
+
2790
+ // Default single-card layout (compatible with previous API)
2791
+ return (
2792
+ <div className="space-y-4" ref={rootRef} data-component-handle={resolvedReplacementHandle}>
2793
+ {!embedded ? (
2794
+ <FormHeader
2795
+ mode="edit"
2796
+ backHref={backHref}
2797
+ backLabel={backLabel}
2798
+ title={title}
2799
+ actions={{
2800
+ extraActions: headerExtraActions,
2801
+ showDelete: !formReadOnly && showDelete,
2802
+ onDelete: handleDelete, // NOSONAR — async→void assignment is valid TypeScript
2803
+ deleteLabel,
2804
+ cancelHref,
2805
+ cancelLabel,
2806
+ submit: formReadOnly ? undefined : { formId, pending: pending, label: resolvedSubmitLabel, pendingLabel: savingLabel, icon: submitIcon },
2807
+ }}
2808
+ />
2809
+ ) : headerExtraActions ? (
2810
+ <div className="flex justify-end gap-2 mb-2">{headerExtraActions}</div>
2811
+ ) : null}
2812
+ {contentHeader}
2813
+ <DataLoader
2814
+ isLoading={isLoading}
2815
+ loadingMessage={resolvedLoadingMessage}
2816
+ spinnerSize="md"
2817
+ className={embedded ? 'min-h-[1px]' : 'min-h-[400px]'}
2818
+ >
2819
+ {wrapFormBody(
2820
+ <div>
2821
+ <form
2822
+ id={formId}
2823
+ onSubmit={handleSubmit}
2824
+ className={`${embedded ? 'space-y-4' : 'rounded-lg border bg-card p-4 space-y-4'} ${dialogFormPadding}`}
2825
+ >
2826
+ {resolvedInjectionSpotId ? (
2827
+ <InjectionSpot
2828
+ spotId={resolvedInjectionSpotId}
2829
+ context={injectionContext}
2830
+ data={values}
2831
+ onDataChange={(newData) => setValues(newData as CrudFormValues<TValues>)}
2832
+ disabled={pending || formReadOnly}
2833
+ widgetsOverride={stackedInjectionWidgets}
2834
+ />
2835
+ ) : null}
2836
+ <div className={grid}>
2837
+ {allFields.map((f) => {
2838
+ const layout = f.layout ?? 'full'
2839
+ const wrapperClassName = usesResponsiveLayout ? resolveLayoutClass(layout) : undefined
2840
+ return (
2841
+ <FieldControl
2842
+ key={f.id}
2843
+ field={f}
2844
+ value={values[f.id]}
2845
+ error={errors[f.id]}
2846
+ options={fieldOptionsById.get(f.id) || EMPTY_OPTIONS}
2847
+ setValue={setValue}
2848
+ onBlurRequest={(fieldId) => { void validateFieldOnBlur(fieldId) }}
2849
+ values={values}
2850
+ loadFieldOptions={loadFieldOptions}
2851
+ autoFocus={!formReadOnly && Boolean(firstFieldId && f.id === firstFieldId)}
2852
+ onSubmitRequest={requestSubmit}
2853
+ wrapperClassName={wrapperClassName}
2854
+ entityIdForField={primaryEntityId ?? undefined}
2855
+ recordId={recordId}
2856
+ />
2857
+ )
2858
+ })}
2859
+ </div>
2860
+ {formError && !Object.keys(errors).length ? <div className="text-sm text-red-600">{formError}</div> : null}
2861
+ {hideFooterActions || formReadOnly ? null : (
2862
+ <FormFooter
2863
+ embedded={embedded}
2864
+ className={dialogFooterClass}
2865
+ actions={{
2866
+ extraActions,
2867
+ showDelete: !embedded && showDelete,
2868
+ onDelete: handleDelete, // NOSONAR — async→void assignment is valid TypeScript
2869
+ deleteLabel,
2870
+ cancelHref: !embedded ? cancelHref : undefined,
2871
+ cancelLabel,
2872
+ submit: { pending: pending, label: resolvedSubmitLabel, pendingLabel: savingLabel, icon: submitIcon },
2873
+ }}
2874
+ />
2875
+ )}
2876
+ </form>
2877
+ </div>,
2878
+ )}
2879
+ </DataLoader>
2880
+ {fieldsetManagerDialog}
2881
+ {ConfirmDialogElement}
2882
+ </div>
2883
+ )
2884
+ }
2885
+
2886
+ function RelationSelect({
2887
+ value,
2888
+ onChange,
2889
+ options,
2890
+ placeholder,
2891
+ autoFocus,
2892
+ }: {
2893
+ value: string
2894
+ onChange: (v: string) => void
2895
+ options: CrudFieldOption[]
2896
+ placeholder?: string
2897
+ autoFocus?: boolean
2898
+ }) {
2899
+ const t = useT()
2900
+ const [query, setQuery] = React.useState('')
2901
+ const inputRef = React.useRef<HTMLInputElement | null>(null)
2902
+
2903
+ const filtered = React.useMemo(() => {
2904
+ const q = query.toLowerCase().trim()
2905
+ if (!q) return options
2906
+ return options.filter((o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q))
2907
+ }, [query, options])
2908
+
2909
+ return (
2910
+ <div className="space-y-1">
2911
+ <input
2912
+ ref={inputRef}
2913
+ className="w-full h-9 rounded border px-2 text-sm"
2914
+ placeholder={placeholder || t('ui.forms.listbox.searchPlaceholder', 'Search...')}
2915
+ value={query}
2916
+ onChange={(e) => setQuery(e.target.value)}
2917
+ autoFocus={autoFocus}
2918
+ data-crud-focus-target=""
2919
+ />
2920
+ <div className="max-h-40 overflow-auto rounded border">
2921
+ <Button
2922
+ variant="ghost"
2923
+ size="sm"
2924
+ className="w-full justify-start rounded-none font-normal"
2925
+ onClick={() => onChange('')}
2926
+ >
2927
+
2928
+ </Button>
2929
+ {filtered.map((opt) => (
2930
+ <Button
2931
+ key={opt.value}
2932
+ variant="ghost"
2933
+ size="sm"
2934
+ className={`w-full justify-start rounded-none font-normal ${
2935
+ value === opt.value ? 'bg-muted' : ''
2936
+ }`}
2937
+ onClick={() => onChange(opt.value)}
2938
+ >
2939
+ {opt.label}
2940
+ </Button>
2941
+ ))}
2942
+ </div>
2943
+ </div>
2944
+ )
2945
+ }
2946
+ // Local-buffer text input to avoid focus loss when parent re-renders
2947
+ function TextInput({
2948
+ value,
2949
+ onChange,
2950
+ placeholder,
2951
+ autoFocus,
2952
+ onSubmit,
2953
+ disabled,
2954
+ suggestions,
2955
+ inputType = 'text',
2956
+ }: {
2957
+ value: string
2958
+ onChange: (v: string) => void
2959
+ placeholder?: string
2960
+ autoFocus?: boolean
2961
+ onSubmit?: () => void
2962
+ disabled?: boolean
2963
+ suggestions?: string[]
2964
+ inputType?: 'text' | 'password'
2965
+ }) {
2966
+ const [local, setLocal] = React.useState<string>(value)
2967
+ const isFocusedRef = React.useRef(false)
2968
+ const userTypingRef = React.useRef(false)
2969
+ const datalistId = React.useId()
2970
+ const commitIfChanged = React.useCallback(() => {
2971
+ if (local === value) return
2972
+ onChange(local)
2973
+ }, [local, onChange, value])
2974
+
2975
+ React.useEffect(() => {
2976
+ // Sync from props whenever the input is unfocused or the user hasn't typed yet.
2977
+ if (!isFocusedRef.current || !userTypingRef.current) {
2978
+ setLocal(value)
2979
+ }
2980
+ }, [value])
2981
+
2982
+ const handleChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
2983
+ if (disabled) return
2984
+ const next = e.target.value
2985
+ userTypingRef.current = true
2986
+ setLocal(next)
2987
+ onChange(next)
2988
+ }, [disabled, onChange])
2989
+
2990
+ const handleKeyDown = React.useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
2991
+ if (disabled) return
2992
+ if (e.key === 'Enter' && !e.shiftKey) {
2993
+ e.preventDefault()
2994
+ commitIfChanged()
2995
+ onSubmit?.()
2996
+ }
2997
+ }, [commitIfChanged, disabled, onSubmit])
2998
+
2999
+ const handleFocus = React.useCallback(() => {
3000
+ isFocusedRef.current = true
3001
+ }, [])
3002
+
3003
+ const handleBlur = React.useCallback(() => {
3004
+ isFocusedRef.current = false
3005
+ userTypingRef.current = false
3006
+ commitIfChanged()
3007
+ }, [commitIfChanged])
3008
+
3009
+ return (
3010
+ <>
3011
+ <input
3012
+ type={inputType}
3013
+ className="w-full h-9 rounded border px-2 text-sm"
3014
+ placeholder={placeholder}
3015
+ value={local}
3016
+ onChange={handleChange}
3017
+ onKeyDown={handleKeyDown}
3018
+ onFocus={handleFocus}
3019
+ onBlur={handleBlur}
3020
+ spellCheck={false}
3021
+ autoFocus={autoFocus}
3022
+ data-crud-focus-target=""
3023
+ disabled={disabled}
3024
+ list={suggestions && suggestions.length > 0 ? datalistId : undefined}
3025
+ />
3026
+ {suggestions && suggestions.length > 0 && (
3027
+ <datalist id={datalistId}>
3028
+ {suggestions.map((suggestion) => (
3029
+ <option key={suggestion} value={suggestion} />
3030
+ ))}
3031
+ </datalist>
3032
+ )}
3033
+ </>
3034
+ )
3035
+ }
3036
+
3037
+ // Local-buffer number input to avoid focus loss when parent re-renders
3038
+ function NumberInput({
3039
+ value,
3040
+ onChange,
3041
+ placeholder,
3042
+ autoFocus,
3043
+ onSubmit,
3044
+ }: {
3045
+ value: number | string | null | undefined
3046
+ onChange: (v: number | undefined) => void
3047
+ placeholder?: string
3048
+ autoFocus?: boolean
3049
+ onSubmit?: () => void
3050
+ }) {
3051
+ const serializedValue = value !== undefined && value !== null ? String(value) : ''
3052
+ const [local, setLocal] = React.useState<string>(serializedValue)
3053
+ const isFocusedRef = React.useRef(false)
3054
+ const commitIfChanged = React.useCallback(() => {
3055
+ if (local === serializedValue) return
3056
+ const numValue = local === '' ? undefined : Number(local)
3057
+ onChange(numValue)
3058
+ }, [local, onChange, serializedValue])
3059
+
3060
+ React.useEffect(() => {
3061
+ // Only sync from props when not focused to avoid caret jumps
3062
+ if (!isFocusedRef.current) {
3063
+ setLocal(serializedValue)
3064
+ }
3065
+ }, [serializedValue])
3066
+
3067
+ const handleChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
3068
+ const next = e.target.value
3069
+ setLocal(next)
3070
+ const numValue = next === '' ? undefined : Number(next)
3071
+ onChange(numValue)
3072
+ }, [onChange])
3073
+
3074
+ const handleKeyDown = React.useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
3075
+ if (e.key === 'Enter' && !e.shiftKey) {
3076
+ e.preventDefault()
3077
+ commitIfChanged()
3078
+ onSubmit?.()
3079
+ }
3080
+ }, [commitIfChanged, onSubmit])
3081
+
3082
+ const handleFocus = React.useCallback(() => {
3083
+ isFocusedRef.current = true
3084
+ }, [])
3085
+
3086
+ const handleBlur = React.useCallback(() => {
3087
+ isFocusedRef.current = false
3088
+ commitIfChanged()
3089
+ }, [commitIfChanged])
3090
+
3091
+ return (
3092
+ <input
3093
+ type="number"
3094
+ className="w-full h-9 rounded border px-2 text-sm"
3095
+ placeholder={placeholder}
3096
+ value={local}
3097
+ onChange={handleChange}
3098
+ onKeyDown={handleKeyDown}
3099
+ onFocus={handleFocus}
3100
+ onBlur={handleBlur}
3101
+ autoFocus={autoFocus}
3102
+ data-crud-focus-target=""
3103
+ />
3104
+ )
3105
+ }
3106
+
3107
+ // Local-buffer textarea to avoid form-wide re-renders while typing
3108
+ function TextAreaInput({
3109
+ value,
3110
+ onChange,
3111
+ placeholder,
3112
+ autoFocus,
3113
+ }: {
3114
+ value: string
3115
+ onChange: (v: string) => void
3116
+ placeholder?: string
3117
+ autoFocus?: boolean
3118
+ }) {
3119
+ const [local, setLocal] = React.useState<string>(value)
3120
+ const isFocusedRef = React.useRef(false)
3121
+ const commitIfChanged = React.useCallback(() => {
3122
+ if (local === value) return
3123
+ onChange(local)
3124
+ }, [local, onChange, value])
3125
+
3126
+ React.useEffect(() => {
3127
+ if (!isFocusedRef.current) setLocal(value)
3128
+ }, [value])
3129
+
3130
+ const handleChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
3131
+ const next = e.target.value
3132
+ setLocal(next)
3133
+ onChange(next)
3134
+ }, [onChange])
3135
+
3136
+ const handleFocus = React.useCallback(() => { isFocusedRef.current = true }, [])
3137
+ const handleBlur = React.useCallback(() => {
3138
+ isFocusedRef.current = false
3139
+ commitIfChanged()
3140
+ }, [commitIfChanged])
3141
+
3142
+ return (
3143
+ <textarea
3144
+ className="w-full rounded border px-2 py-2 min-h-[80px] sm:min-h-[120px] text-sm"
3145
+ placeholder={placeholder}
3146
+ value={local}
3147
+ onChange={handleChange}
3148
+ onFocus={handleFocus}
3149
+ onBlur={handleBlur}
3150
+ autoFocus={autoFocus}
3151
+ data-crud-focus-target=""
3152
+ />
3153
+ )
3154
+ }
3155
+
3156
+ // Markdown editor using @uiw/react-md-editor (client-only)
3157
+ type MDProps = { value?: string; onChange: (md: string) => void }
3158
+ const MDEditor = dynamic(async () => {
3159
+ const mod = await import('@uiw/react-md-editor')
3160
+ return mod.default
3161
+ }, { ssr: false }) as React.ComponentType<UiWMDEditorProps>
3162
+ const MarkdownEditor = React.memo(function MarkdownEditor({ value = '', onChange }: MDProps) {
3163
+ const containerRef = React.useRef<HTMLDivElement | null>(null)
3164
+ const [local, setLocal] = React.useState<string>(value)
3165
+ const typingRef = React.useRef(false)
3166
+
3167
+ React.useEffect(() => {
3168
+ if (!typingRef.current) setLocal(value)
3169
+ }, [value])
3170
+
3171
+ const handleChange = React.useCallback((v?: string) => {
3172
+ typingRef.current = true
3173
+ setLocal(v ?? '')
3174
+ }, [])
3175
+
3176
+ const commit = React.useCallback(() => {
3177
+ if (!typingRef.current) return
3178
+ typingRef.current = false
3179
+ onChange(local)
3180
+ requestAnimationFrame(() => {
3181
+ const ta = containerRef.current?.querySelector('textarea') as HTMLTextAreaElement | null
3182
+ ta?.focus()
3183
+ })
3184
+ }, [local, onChange])
3185
+
3186
+ return (
3187
+ <div ref={containerRef} data-color-mode="light" className="w-full" onBlur={() => commit()}>
3188
+ <MDEditor
3189
+ value={local}
3190
+ height={220}
3191
+ onChange={handleChange}
3192
+ previewOptions={{ remarkPlugins: [remarkGfm] }}
3193
+ />
3194
+ </div>
3195
+ )
3196
+ }, (prev, next) => prev.value === next.value)
3197
+
3198
+ // HTML Rich Text editor (contentEditable) with shortcuts; returns HTML string
3199
+ type HtmlRTProps = { value?: string; onChange: (html: string) => void }
3200
+ const HtmlRichTextEditor = React.memo(function HtmlRichTextEditor({ value = '', onChange }: HtmlRTProps) {
3201
+ const t = useT()
3202
+ const boldLabel = t('ui.forms.richtext.bold')
3203
+ const italicLabel = t('ui.forms.richtext.italic')
3204
+ const underlineLabel = t('ui.forms.richtext.underline')
3205
+ const listLabel = t('ui.forms.richtext.list')
3206
+ const heading3Label = t('ui.forms.richtext.heading3')
3207
+ const linkLabel = t('ui.forms.richtext.link')
3208
+ const linkUrlPrompt = t('ui.forms.richtext.linkUrlPrompt')
3209
+ const ref = React.useRef<HTMLDivElement | null>(null)
3210
+ const applyingExternal = React.useRef(false)
3211
+ const typingRef = React.useRef(false)
3212
+
3213
+ React.useEffect(() => {
3214
+ const el = ref.current
3215
+ if (!el) return
3216
+ const current = el.innerHTML
3217
+ const sanitizedValue = sanitizeHtmlRichText(value)
3218
+ if (!typingRef.current && current !== sanitizedValue) {
3219
+ applyingExternal.current = true
3220
+ el.innerHTML = sanitizedValue
3221
+ requestAnimationFrame(() => { applyingExternal.current = false })
3222
+ }
3223
+ }, [value])
3224
+
3225
+ const exec = (cmd: string, arg?: string) => {
3226
+ const el = ref.current
3227
+ if (!el) return
3228
+ el.focus()
3229
+ try {
3230
+ document.execCommand(cmd, false, arg)
3231
+ } catch {
3232
+ // ignore execCommand failures
3233
+ }
3234
+ }
3235
+
3236
+ const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
3237
+ const isMod = e.metaKey || e.ctrlKey
3238
+ if (!isMod) return
3239
+ const k = e.key.toLowerCase()
3240
+ if (k === 'b') { e.preventDefault(); exec('bold') }
3241
+ if (k === 'i') { e.preventDefault(); exec('italic') }
3242
+ if (k === 'u') { e.preventDefault(); exec('underline') }
3243
+ }
3244
+
3245
+ const onPaste = (e: React.ClipboardEvent<HTMLDivElement>) => {
3246
+ const html = e.clipboardData.getData('text/html')
3247
+ const text = e.clipboardData.getData('text/plain')
3248
+ const sanitizedPaste = sanitizeRichTextPasteContent(html, text)
3249
+ if (!sanitizedPaste) return
3250
+
3251
+ e.preventDefault()
3252
+ exec(sanitizedPaste.command, sanitizedPaste.value)
3253
+ }
3254
+
3255
+ return (
3256
+ <div className="w-full rounded border">
3257
+ <div className="flex items-center gap-1 px-2 py-1 border-b">
3258
+ <Button variant="ghost" size="sm" className="h-auto px-2 py-0.5 text-xs" onMouseDown={(e) => e.preventDefault()} onClick={() => exec('bold')}>{boldLabel}</Button>
3259
+ <Button variant="ghost" size="sm" className="h-auto px-2 py-0.5 text-xs" onMouseDown={(e) => e.preventDefault()} onClick={() => exec('italic')}>{italicLabel}</Button>
3260
+ <Button variant="ghost" size="sm" className="h-auto px-2 py-0.5 text-xs" onMouseDown={(e) => e.preventDefault()} onClick={() => exec('underline')}>{underlineLabel}</Button>
3261
+ <span className="mx-2 text-muted-foreground">|</span>
3262
+ <Button variant="ghost" size="sm" className="h-auto px-2 py-0.5 text-xs" onMouseDown={(e) => e.preventDefault()} onClick={() => exec('insertUnorderedList')}>• {listLabel}</Button>
3263
+ <Button variant="ghost" size="sm" className="h-auto px-2 py-0.5 text-xs" onMouseDown={(e) => e.preventDefault()} onClick={() => exec('formatBlock', '<h3>')}>{heading3Label}</Button>
3264
+ <Button
3265
+ variant="ghost"
3266
+ size="sm"
3267
+ className="h-auto px-2 py-0.5 text-xs"
3268
+ onMouseDown={(e) => e.preventDefault()}
3269
+ onClick={() => {
3270
+ const url = sanitizeRichTextHref(window.prompt(linkUrlPrompt))
3271
+ if (url) exec('createLink', url)
3272
+ }}
3273
+ >{linkLabel}</Button>
3274
+ </div>
3275
+ <div
3276
+ ref={ref}
3277
+ className="w-full px-2 py-2 min-h-[100px] sm:min-h-[160px] focus:outline-none prose prose-sm max-w-none"
3278
+ contentEditable
3279
+ suppressContentEditableWarning
3280
+ onKeyDown={onKeyDown}
3281
+ onPaste={onPaste}
3282
+ onInput={() => { if (!applyingExternal.current) typingRef.current = true }}
3283
+ onBlur={() => {
3284
+ const el = ref.current
3285
+ if (!el) return
3286
+ typingRef.current = false
3287
+ const sanitizedValue = sanitizeHtmlRichText(el.innerHTML)
3288
+ if (el.innerHTML !== sanitizedValue) {
3289
+ applyingExternal.current = true
3290
+ el.innerHTML = sanitizedValue
3291
+ requestAnimationFrame(() => { applyingExternal.current = false })
3292
+ }
3293
+ onChange(sanitizedValue)
3294
+ }}
3295
+ />
3296
+ </div>
3297
+ )
3298
+ }, (prev, next) => prev.value === next.value)
3299
+
3300
+ // Very simple markdown editor with Bold/Italic/Underline + shortcuts.
3301
+ type SimpleMDProps = { value?: string; onChange: (md: string) => void }
3302
+ const SimpleMarkdownEditor = React.memo(function SimpleMarkdownEditor({ value = '', onChange }: SimpleMDProps) {
3303
+ const t = useT()
3304
+ const boldLabel = t('ui.forms.richtext.bold')
3305
+ const italicLabel = t('ui.forms.richtext.italic')
3306
+ const underlineLabel = t('ui.forms.richtext.underline')
3307
+ const markdownPlaceholder = t('ui.forms.richtext.placeholder')
3308
+ const sampleText = t('ui.forms.richtext.sampleText')
3309
+ const taRef = React.useRef<HTMLTextAreaElement | null>(null)
3310
+ const [local, setLocal] = React.useState<string>(value)
3311
+ const typingRef = React.useRef(false)
3312
+
3313
+ React.useEffect(() => {
3314
+ if (!typingRef.current) setLocal(value)
3315
+ }, [value])
3316
+
3317
+ const wrap = (before: string, after: string = before) => {
3318
+ const el = taRef.current
3319
+ if (!el) return
3320
+ const start = el.selectionStart ?? 0
3321
+ const end = el.selectionEnd ?? 0
3322
+ const sel = value.slice(start, end) || sampleText
3323
+ const next = value.slice(0, start) + before + sel + after + value.slice(end)
3324
+ onChange(next)
3325
+ queueMicrotask(() => {
3326
+ const caret = start + before.length + sel.length + after.length
3327
+ el.focus()
3328
+ el.setSelectionRange(caret, caret)
3329
+ })
3330
+ }
3331
+
3332
+ const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
3333
+ const isMod = e.metaKey || e.ctrlKey
3334
+ if (!isMod) return
3335
+ const key = e.key.toLowerCase()
3336
+ if (key === 'b') { e.preventDefault(); wrap('**') }
3337
+ if (key === 'i') { e.preventDefault(); wrap('_') }
3338
+ if (key === 'u') { e.preventDefault(); wrap('__') }
3339
+ }
3340
+
3341
+ return (
3342
+ <div className="w-full rounded border">
3343
+ <div className="flex items-center gap-1 px-2 py-1 border-b">
3344
+ <Button variant="ghost" size="sm" className="h-auto px-2 py-0.5 text-xs" onMouseDown={(e) => e.preventDefault()} onClick={() => wrap('**')}>{boldLabel}</Button>
3345
+ <Button variant="ghost" size="sm" className="h-auto px-2 py-0.5 text-xs" onMouseDown={(e) => e.preventDefault()} onClick={() => wrap('_')}>{italicLabel}</Button>
3346
+ <Button variant="ghost" size="sm" className="h-auto px-2 py-0.5 text-xs" onMouseDown={(e) => e.preventDefault()} onClick={() => wrap('__')}>{underlineLabel}</Button>
3347
+ </div>
3348
+ <textarea
3349
+ ref={taRef}
3350
+ className="w-full min-h-[100px] sm:min-h-[160px] resize-y px-2 py-2 font-mono text-sm outline-none"
3351
+ spellCheck={false}
3352
+ value={local}
3353
+ onChange={(e) => { typingRef.current = true; setLocal(e.target.value) }}
3354
+ onBlur={() => { if (typingRef.current) { typingRef.current = false; onChange(local) } }}
3355
+ onKeyDown={onKeyDown}
3356
+ placeholder={markdownPlaceholder}
3357
+ />
3358
+ </div>
3359
+ )
3360
+ }, (prev, next) => prev.value === next.value)
3361
+
3362
+ type FieldControlProps = {
3363
+ field: CrudField
3364
+ value: unknown
3365
+ error?: string
3366
+ options: CrudFieldOption[]
3367
+ setValue: (id: string, v: unknown) => void
3368
+ onBlurRequest: (fieldId: string) => void
3369
+ values: Record<string, unknown>
3370
+ loadFieldOptions: (field: CrudField, query?: string) => Promise<CrudFieldOption[]>
3371
+ autoFocus: boolean
3372
+ onSubmitRequest: () => void
3373
+ wrapperClassName?: string
3374
+ entityIdForField?: string
3375
+ recordId?: string
3376
+ }
3377
+
3378
+ function supportsWrapperBlurValidation(field: CrudField): boolean {
3379
+ return (
3380
+ field.type === 'text' ||
3381
+ field.type === 'password' ||
3382
+ field.type === 'textarea' ||
3383
+ field.type === 'checkbox' ||
3384
+ field.type === 'select' ||
3385
+ field.type === 'number'
3386
+ )
3387
+ }
3388
+
3389
+ type ListboxMultiSelectProps = {
3390
+ options: CrudFieldOption[]
3391
+ placeholder?: string
3392
+ value: string[]
3393
+ onChange: (vals: string[]) => void
3394
+ autoFocus?: boolean
3395
+ }
3396
+
3397
+ const ListboxMultiSelect = React.memo(function ListboxMultiSelect({
3398
+ options,
3399
+ placeholder,
3400
+ value,
3401
+ onChange,
3402
+ autoFocus,
3403
+ }: ListboxMultiSelectProps) {
3404
+ const t = useT()
3405
+ const searchPlaceholder = placeholder || t('ui.forms.listbox.searchPlaceholder')
3406
+ const noMatchesLabel = t('ui.forms.listbox.noMatches')
3407
+ const [query, setQuery] = React.useState('')
3408
+ const filtered = React.useMemo(() => {
3409
+ const q = query.toLowerCase().trim()
3410
+ if (!q) return options
3411
+ return options.filter((o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q))
3412
+ }, [options, query])
3413
+ const toggle = React.useCallback(
3414
+ (val: string) => {
3415
+ const set = new Set(value)
3416
+ if (set.has(val)) set.delete(val)
3417
+ else set.add(val)
3418
+ onChange(Array.from(set))
3419
+ },
3420
+ [value, onChange]
3421
+ )
3422
+ return (
3423
+ <div className="w-full">
3424
+ <input
3425
+ className="mb-2 w-full h-8 rounded border px-2 text-sm"
3426
+ placeholder={searchPlaceholder}
3427
+ value={query}
3428
+ onChange={(e) => setQuery(e.target.value)}
3429
+ autoFocus={autoFocus}
3430
+ data-crud-focus-target=""
3431
+ />
3432
+ <div className="rounded border max-h-48 overflow-auto divide-y">
3433
+ {filtered.map((opt) => {
3434
+ const isSel = value.includes(opt.value)
3435
+ return (
3436
+ <Button
3437
+ key={opt.value}
3438
+ variant="ghost"
3439
+ size="sm"
3440
+ onClick={() => toggle(opt.value)}
3441
+ className={`w-full justify-start rounded-none font-normal px-3 py-2 ${isSel ? 'bg-muted' : ''}`}
3442
+ >
3443
+ <span className="inline-flex items-center gap-2">
3444
+ <input type="checkbox" className="size-4" readOnly checked={isSel} />
3445
+ <span>{opt.label}</span>
3446
+ </span>
3447
+ </Button>
3448
+ )
3449
+ })}
3450
+ {!filtered.length ? (
3451
+ <div className="px-3 py-2 text-sm text-muted-foreground">{noMatchesLabel}</div>
3452
+ ) : null}
3453
+ </div>
3454
+ </div>
3455
+ )
3456
+ })
3457
+
3458
+ const FieldControl = React.memo(function FieldControlImpl({
3459
+ field,
3460
+ value,
3461
+ error,
3462
+ options,
3463
+ setValue,
3464
+ onBlurRequest,
3465
+ values,
3466
+ loadFieldOptions,
3467
+ autoFocus,
3468
+ onSubmitRequest,
3469
+ wrapperClassName,
3470
+ entityIdForField,
3471
+ recordId,
3472
+ }: FieldControlProps) {
3473
+ const t = useT()
3474
+ const fieldSetValue = React.useCallback(
3475
+ (nextValue: unknown) => setValue(field.id, nextValue),
3476
+ [setValue, field.id]
3477
+ )
3478
+ const setFormValue = React.useCallback(
3479
+ (targetId: string, nextValue: unknown) => setValue(targetId, nextValue),
3480
+ [setValue],
3481
+ )
3482
+ const builtin = field.type === 'custom' ? null : field
3483
+ const hasLoader = typeof builtin?.loadOptions === 'function'
3484
+ const disabled = Boolean(field.disabled)
3485
+ const readOnly = Boolean(field.readOnly)
3486
+ const autoFocusField = autoFocus && !disabled
3487
+
3488
+ React.useEffect(() => {
3489
+ if (!hasLoader || field.type === 'custom') return
3490
+ loadFieldOptions(field).catch(() => {})
3491
+ }, [field, hasLoader, loadFieldOptions])
3492
+
3493
+ const placeholder = builtin?.placeholder
3494
+ const rootClassName = wrapperClassName ? `space-y-1 ${wrapperClassName}` : 'space-y-1'
3495
+ const validateOnWrapperBlur = supportsWrapperBlurValidation(field)
3496
+
3497
+ return (
3498
+ <div
3499
+ className={rootClassName}
3500
+ data-crud-field-id={field.id}
3501
+ onBlur={validateOnWrapperBlur
3502
+ ? (event) => {
3503
+ const nextFocused = event.relatedTarget
3504
+ if (nextFocused instanceof Node && event.currentTarget.contains(nextFocused)) return
3505
+ onBlurRequest(field.id)
3506
+ }
3507
+ : undefined}
3508
+ >
3509
+ {field.type !== 'checkbox' && field.label.trim().length > 0 ? (
3510
+ <label className="block text-sm font-medium">
3511
+ {field.label}
3512
+ {field.required ? <span className="text-red-600"> *</span> : null}
3513
+ </label>
3514
+ ) : null}
3515
+ {field.type === 'text' && (
3516
+ <TextInput
3517
+ value={value == null ? '' : String(value)}
3518
+ placeholder={placeholder}
3519
+ onChange={(next) => fieldSetValue(next)}
3520
+ autoFocus={autoFocusField}
3521
+ onSubmit={onSubmitRequest}
3522
+ disabled={disabled}
3523
+ suggestions={field.type === 'text' ? field.suggestions : undefined}
3524
+ />
3525
+ )}
3526
+ {field.type === 'password' && (
3527
+ <TextInput
3528
+ value={value == null ? '' : String(value)}
3529
+ placeholder={placeholder}
3530
+ onChange={(next) => fieldSetValue(next)}
3531
+ autoFocus={autoFocusField}
3532
+ onSubmit={onSubmitRequest}
3533
+ disabled={disabled}
3534
+ inputType="password"
3535
+ />
3536
+ )}
3537
+ {field.type === 'number' && (
3538
+ <NumberInput
3539
+ value={typeof value === 'number' || typeof value === 'string' ? value : null}
3540
+ placeholder={placeholder}
3541
+ onChange={fieldSetValue}
3542
+ autoFocus={autoFocusField}
3543
+ onSubmit={onSubmitRequest}
3544
+ />
3545
+ )}
3546
+ {field.type === 'date' && (
3547
+ <input
3548
+ type="date"
3549
+ className="w-full h-9 rounded border px-2 text-sm"
3550
+ value={typeof value === 'string' ? value : ''}
3551
+ onChange={(e) => setValue(field.id, e.target.value || undefined)}
3552
+ autoFocus={autoFocusField}
3553
+ data-crud-focus-target=""
3554
+ disabled={disabled}
3555
+ />
3556
+ )}
3557
+ {field.type === 'datetime-local' && (
3558
+ <input
3559
+ type="datetime-local"
3560
+ className="w-full h-9 rounded border px-2 text-sm"
3561
+ value={typeof value === 'string' ? value : ''}
3562
+ onChange={(e) => setValue(field.id, e.target.value || undefined)}
3563
+ autoFocus={autoFocusField}
3564
+ data-crud-focus-target=""
3565
+ disabled={disabled}
3566
+ />
3567
+ )}
3568
+ {field.type === 'datepicker' && (
3569
+ <DatePicker
3570
+ value={typeof value === 'string' && value ? parseISO(value) : value instanceof Date ? value : null}
3571
+ onChange={(date) => setValue(field.id, date ? format(date, 'yyyy-MM-dd') : undefined)}
3572
+ disabled={disabled}
3573
+ readOnly={readOnly}
3574
+ placeholder={placeholder}
3575
+ minDate={builtin?.minDate}
3576
+ maxDate={builtin?.maxDate}
3577
+ displayFormat={builtin?.displayFormat}
3578
+ closeOnSelect={builtin?.closeOnSelect}
3579
+ locale={builtin?.locale}
3580
+ />
3581
+ )}
3582
+ {field.type === 'datetime' && (
3583
+ <DateTimePicker
3584
+ value={typeof value === 'string' && value ? new Date(value) : value instanceof Date ? value : null}
3585
+ onChange={(date) => setValue(field.id, date ? date.toISOString() : undefined)}
3586
+ disabled={disabled}
3587
+ readOnly={readOnly}
3588
+ placeholder={placeholder}
3589
+ minuteStep={builtin?.minuteStep}
3590
+ minDate={builtin?.minDate}
3591
+ maxDate={builtin?.maxDate}
3592
+ displayFormat={builtin?.displayFormat}
3593
+ locale={builtin?.locale}
3594
+ />
3595
+ )}
3596
+ {field.type === 'time' && (
3597
+ <TimePicker
3598
+ value={typeof value === 'string' ? value : null}
3599
+ onChange={(time) => setValue(field.id, time ?? undefined)}
3600
+ disabled={disabled}
3601
+ readOnly={readOnly}
3602
+ placeholder={placeholder}
3603
+ minuteStep={builtin?.minuteStep}
3604
+ />
3605
+ )}
3606
+ {field.type === 'textarea' && (
3607
+ <TextAreaInput
3608
+ value={value == null ? '' : String(value)}
3609
+ placeholder={placeholder}
3610
+ onChange={(next) => fieldSetValue(next)}
3611
+ autoFocus={autoFocusField}
3612
+ />
3613
+ )}
3614
+ {field.type === 'richtext' && builtin?.editor === 'simple' && (
3615
+ <SimpleMarkdownEditor value={String(value ?? '')} onChange={fieldSetValue} />
3616
+ )}
3617
+ {field.type === 'richtext' && builtin?.editor === 'html' && (
3618
+ <HtmlRichTextEditor value={String(value ?? '')} onChange={fieldSetValue} />
3619
+ )}
3620
+ {field.type === 'richtext' && (!builtin?.editor || (builtin.editor !== 'simple' && builtin.editor !== 'html')) && (
3621
+ <MarkdownEditor value={String(value ?? '')} onChange={fieldSetValue} />
3622
+ )}
3623
+ {field.type === 'tags' && (
3624
+ <TagsInput
3625
+ value={Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []}
3626
+ onChange={(next) => fieldSetValue(next)}
3627
+ placeholder={placeholder}
3628
+ autoFocus={autoFocusField}
3629
+ suggestions={options.map((opt) => ({ value: opt.value, label: opt.label }))}
3630
+ loadSuggestions={
3631
+ typeof builtin?.loadOptions === 'function'
3632
+ ? async (query?: string) => {
3633
+ const opts = await loadFieldOptions(field, query)
3634
+ return opts.map((opt) => ({ value: opt.value, label: opt.label }))
3635
+ }
3636
+ : undefined
3637
+ }
3638
+ />
3639
+ )}
3640
+ {field.type === 'combobox' && (
3641
+ <ComboboxInput
3642
+ value={typeof value === 'string' ? value : String(value ?? '')}
3643
+ onChange={(next) => fieldSetValue(next)}
3644
+ placeholder={placeholder}
3645
+ autoFocus={autoFocusField}
3646
+ suggestions={
3647
+ builtin?.suggestions
3648
+ ? builtin.suggestions
3649
+ : options.map((opt) => ({ value: opt.value, label: opt.label }))
3650
+ }
3651
+ loadSuggestions={
3652
+ typeof builtin?.loadOptions === 'function'
3653
+ ? async (query?: string) => {
3654
+ const opts = await loadFieldOptions(field, query)
3655
+ return opts.map((opt) => ({ value: opt.value, label: opt.label }))
3656
+ }
3657
+ : undefined
3658
+ }
3659
+ allowCustomValues={builtin?.allowCustomValues ?? true}
3660
+ disabled={disabled}
3661
+ />
3662
+ )}
3663
+ {field.type === 'checkbox' && (
3664
+ <label className="inline-flex items-center gap-2">
3665
+ <input
3666
+ type="checkbox"
3667
+ className="size-4"
3668
+ checked={value === true}
3669
+ onChange={(e) => setValue(field.id, e.target.checked)}
3670
+ data-crud-focus-target=""
3671
+ disabled={disabled}
3672
+ />
3673
+ <span className="text-sm">{field.label}</span>
3674
+ </label>
3675
+ )}
3676
+ {field.type === 'select' && !builtin?.multiple && (
3677
+ <select
3678
+ className="w-full h-9 rounded border pl-3 pr-8 text-sm"
3679
+ value={
3680
+ Array.isArray(value)
3681
+ ? String(value[0] ?? '')
3682
+ : value == null
3683
+ ? ''
3684
+ : String(value)
3685
+ }
3686
+ onChange={(e) => setValue(field.id, e.target.value || undefined)}
3687
+ data-crud-focus-target=""
3688
+ disabled={disabled}
3689
+ >
3690
+ <option value="">{t('ui.forms.select.emptyOption', '—')}</option>
3691
+ {options.map((opt) => (
3692
+ <option key={opt.value} value={opt.value}>
3693
+ {opt.label}
3694
+ </option>
3695
+ ))}
3696
+ </select>
3697
+ )}
3698
+ {field.type === 'select' && builtin?.multiple && builtin.listbox === true && (
3699
+ <ListboxMultiSelect
3700
+ options={options}
3701
+ placeholder={placeholder}
3702
+ value={Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []}
3703
+ onChange={(vals) => setValue(field.id, vals)}
3704
+ autoFocus={autoFocusField}
3705
+ />
3706
+ )}
3707
+ {field.type === 'select' && builtin?.multiple && builtin.listbox !== true && (
3708
+ <div className="flex flex-wrap gap-3">
3709
+ {options.map((opt) => {
3710
+ const arr = Array.isArray(value)
3711
+ ? value.filter((item): item is string => typeof item === 'string')
3712
+ : []
3713
+ const checked = arr.includes(opt.value)
3714
+ return (
3715
+ <label key={opt.value} className="inline-flex items-center gap-2">
3716
+ <input
3717
+ type="checkbox"
3718
+ className="size-4"
3719
+ checked={checked}
3720
+ onChange={(e) => {
3721
+ const next = new Set(arr)
3722
+ if (e.target.checked) {
3723
+ next.add(opt.value)
3724
+ } else {
3725
+ next.delete(opt.value)
3726
+ }
3727
+ setValue(field.id, Array.from(next))
3728
+ }}
3729
+ disabled={disabled}
3730
+ />
3731
+ <span className="text-sm">{opt.label}</span>
3732
+ </label>
3733
+ )
3734
+ })}
3735
+ </div>
3736
+ )}
3737
+ {field.type === 'relation' && (
3738
+ <RelationSelect
3739
+ options={options}
3740
+ placeholder={placeholder}
3741
+ value={
3742
+ Array.isArray(value)
3743
+ ? String(value[0] ?? '')
3744
+ : value == null
3745
+ ? ''
3746
+ : String(value)
3747
+ }
3748
+ onChange={(selected) => setValue(field.id, selected)}
3749
+ autoFocus={autoFocusField}
3750
+ />
3751
+ )}
3752
+ {field.type === 'custom' && (
3753
+ <>
3754
+ {field.component({
3755
+ id: field.id,
3756
+ value,
3757
+ error,
3758
+ setValue: fieldSetValue,
3759
+ setFormValue,
3760
+ values,
3761
+ entityId: entityIdForField,
3762
+ recordId,
3763
+ autoFocus,
3764
+ disabled,
3765
+ })}
3766
+ </>
3767
+ )}
3768
+ {field.description ? (
3769
+ <div className="flex items-start gap-2 text-xs text-muted-foreground">
3770
+ <Info className="mt-0.5 h-3.5 w-3.5 shrink-0" />
3771
+ <div>{field.description}</div>
3772
+ </div>
3773
+ ) : null}
3774
+ {error ? <div className="text-xs text-red-600">{error}</div> : null}
3775
+ </div>
3776
+ )
3777
+ },
3778
+ (prev, next) =>
3779
+ prev.field.id === next.field.id &&
3780
+ prev.field.type === next.field.type &&
3781
+ prev.field.label === next.field.label &&
3782
+ prev.field.description === next.field.description &&
3783
+ prev.field.required === next.field.required &&
3784
+ prev.value === next.value &&
3785
+ prev.error === next.error &&
3786
+ prev.options === next.options &&
3787
+ prev.loadFieldOptions === next.loadFieldOptions &&
3788
+ prev.autoFocus === next.autoFocus &&
3789
+ prev.onSubmitRequest === next.onSubmitRequest &&
3790
+ prev.wrapperClassName === next.wrapperClassName &&
3791
+ prev.entityIdForField === next.entityIdForField &&
3792
+ prev.recordId === next.recordId &&
3793
+ (prev.field.type !== 'custom' ||
3794
+ (prev.values === next.values &&
3795
+ prev.field.component === (next.field as CrudCustomField).component))
3796
+ )