@open-mercato/ui 0.4.2-canary-c02407ff85

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 (319) hide show
  1. package/build.mjs +62 -0
  2. package/dist/backend/AppShell.js +902 -0
  3. package/dist/backend/AppShell.js.map +7 -0
  4. package/dist/backend/ConfirmDialog.js +17 -0
  5. package/dist/backend/ConfirmDialog.js.map +7 -0
  6. package/dist/backend/ContextHelp.js +31 -0
  7. package/dist/backend/ContextHelp.js.map +7 -0
  8. package/dist/backend/CrudForm.js +2028 -0
  9. package/dist/backend/CrudForm.js.map +7 -0
  10. package/dist/backend/DataTable.js +1363 -0
  11. package/dist/backend/DataTable.js.map +7 -0
  12. package/dist/backend/EmptyState.js +52 -0
  13. package/dist/backend/EmptyState.js.map +7 -0
  14. package/dist/backend/FilterBar.js +140 -0
  15. package/dist/backend/FilterBar.js.map +7 -0
  16. package/dist/backend/FilterOverlay.js +279 -0
  17. package/dist/backend/FilterOverlay.js.map +7 -0
  18. package/dist/backend/FlashMessages.js +66 -0
  19. package/dist/backend/FlashMessages.js.map +7 -0
  20. package/dist/backend/JsonBuilder.js +322 -0
  21. package/dist/backend/JsonBuilder.js.map +7 -0
  22. package/dist/backend/JsonDisplay.js +203 -0
  23. package/dist/backend/JsonDisplay.js.map +7 -0
  24. package/dist/backend/Page.js +27 -0
  25. package/dist/backend/Page.js.map +7 -0
  26. package/dist/backend/PerspectiveSidebar.js +282 -0
  27. package/dist/backend/PerspectiveSidebar.js.map +7 -0
  28. package/dist/backend/RowActions.js +148 -0
  29. package/dist/backend/RowActions.js.map +7 -0
  30. package/dist/backend/TruncatedCell.js +92 -0
  31. package/dist/backend/TruncatedCell.js.map +7 -0
  32. package/dist/backend/UserMenu.js +107 -0
  33. package/dist/backend/UserMenu.js.map +7 -0
  34. package/dist/backend/ValueIcons.js +34 -0
  35. package/dist/backend/ValueIcons.js.map +7 -0
  36. package/dist/backend/custom-fields/FieldDefinitionsEditor.js +1264 -0
  37. package/dist/backend/custom-fields/FieldDefinitionsEditor.js.map +7 -0
  38. package/dist/backend/custom-fields/FieldDefinitionsManager.js +332 -0
  39. package/dist/backend/custom-fields/FieldDefinitionsManager.js.map +7 -0
  40. package/dist/backend/dashboard/DashboardScreen.js +578 -0
  41. package/dist/backend/dashboard/DashboardScreen.js.map +7 -0
  42. package/dist/backend/dashboard/index.js +5 -0
  43. package/dist/backend/dashboard/index.js.map +7 -0
  44. package/dist/backend/dashboard/widgetRegistry.js +55 -0
  45. package/dist/backend/dashboard/widgetRegistry.js.map +7 -0
  46. package/dist/backend/detail/ActivitiesSection.js +962 -0
  47. package/dist/backend/detail/ActivitiesSection.js.map +7 -0
  48. package/dist/backend/detail/AddressEditor.js +413 -0
  49. package/dist/backend/detail/AddressEditor.js.map +7 -0
  50. package/dist/backend/detail/AddressTiles.js +437 -0
  51. package/dist/backend/detail/AddressTiles.js.map +7 -0
  52. package/dist/backend/detail/AddressesSection.js +264 -0
  53. package/dist/backend/detail/AddressesSection.js.map +7 -0
  54. package/dist/backend/detail/AttachmentDeleteDialog.js +41 -0
  55. package/dist/backend/detail/AttachmentDeleteDialog.js.map +7 -0
  56. package/dist/backend/detail/AttachmentMetadataDialog.js +517 -0
  57. package/dist/backend/detail/AttachmentMetadataDialog.js.map +7 -0
  58. package/dist/backend/detail/AttachmentsSection.js +367 -0
  59. package/dist/backend/detail/AttachmentsSection.js.map +7 -0
  60. package/dist/backend/detail/CustomDataSection.js +433 -0
  61. package/dist/backend/detail/CustomDataSection.js.map +7 -0
  62. package/dist/backend/detail/DetailFieldsSection.js +75 -0
  63. package/dist/backend/detail/DetailFieldsSection.js.map +7 -0
  64. package/dist/backend/detail/ErrorMessage.js +28 -0
  65. package/dist/backend/detail/ErrorMessage.js.map +7 -0
  66. package/dist/backend/detail/InlineEditors.js +681 -0
  67. package/dist/backend/detail/InlineEditors.js.map +7 -0
  68. package/dist/backend/detail/LoadingMessage.js +14 -0
  69. package/dist/backend/detail/LoadingMessage.js.map +7 -0
  70. package/dist/backend/detail/NotesSection.js +1032 -0
  71. package/dist/backend/detail/NotesSection.js.map +7 -0
  72. package/dist/backend/detail/TabEmptyState.js +25 -0
  73. package/dist/backend/detail/TabEmptyState.js.map +7 -0
  74. package/dist/backend/detail/TagsSection.js +254 -0
  75. package/dist/backend/detail/TagsSection.js.map +7 -0
  76. package/dist/backend/detail/addressFormat.js +77 -0
  77. package/dist/backend/detail/addressFormat.js.map +7 -0
  78. package/dist/backend/detail/index.js +34 -0
  79. package/dist/backend/detail/index.js.map +7 -0
  80. package/dist/backend/fields/registry.generated.js +8 -0
  81. package/dist/backend/fields/registry.generated.js.map +7 -0
  82. package/dist/backend/fields/registry.js +29 -0
  83. package/dist/backend/fields/registry.js.map +7 -0
  84. package/dist/backend/indexes/PartialIndexBanner.js +58 -0
  85. package/dist/backend/indexes/PartialIndexBanner.js.map +7 -0
  86. package/dist/backend/indexes/store.js +62 -0
  87. package/dist/backend/indexes/store.js.map +7 -0
  88. package/dist/backend/injection/InjectionSpot.js +179 -0
  89. package/dist/backend/injection/InjectionSpot.js.map +7 -0
  90. package/dist/backend/injection/PageInjectionBoundary.js +26 -0
  91. package/dist/backend/injection/PageInjectionBoundary.js.map +7 -0
  92. package/dist/backend/injection/helpers.js +26 -0
  93. package/dist/backend/injection/helpers.js.map +7 -0
  94. package/dist/backend/injection/widgetRegistry.js +55 -0
  95. package/dist/backend/injection/widgetRegistry.js.map +7 -0
  96. package/dist/backend/inputs/ComboboxInput.js +225 -0
  97. package/dist/backend/inputs/ComboboxInput.js.map +7 -0
  98. package/dist/backend/inputs/LookupSelect.js +191 -0
  99. package/dist/backend/inputs/LookupSelect.js.map +7 -0
  100. package/dist/backend/inputs/PhoneNumberField.js +100 -0
  101. package/dist/backend/inputs/PhoneNumberField.js.map +7 -0
  102. package/dist/backend/inputs/SwitchableMarkdownInput.js +92 -0
  103. package/dist/backend/inputs/SwitchableMarkdownInput.js.map +7 -0
  104. package/dist/backend/inputs/TagsInput.js +222 -0
  105. package/dist/backend/inputs/TagsInput.js.map +7 -0
  106. package/dist/backend/inputs/index.js +6 -0
  107. package/dist/backend/inputs/index.js.map +7 -0
  108. package/dist/backend/operations/LastOperationBanner.js +80 -0
  109. package/dist/backend/operations/LastOperationBanner.js.map +7 -0
  110. package/dist/backend/operations/store.js +183 -0
  111. package/dist/backend/operations/store.js.map +7 -0
  112. package/dist/backend/schedule/ScheduleAgenda.js +107 -0
  113. package/dist/backend/schedule/ScheduleAgenda.js.map +7 -0
  114. package/dist/backend/schedule/ScheduleGrid.js +107 -0
  115. package/dist/backend/schedule/ScheduleGrid.js.map +7 -0
  116. package/dist/backend/schedule/ScheduleToolbar.js +166 -0
  117. package/dist/backend/schedule/ScheduleToolbar.js.map +7 -0
  118. package/dist/backend/schedule/ScheduleView.js +165 -0
  119. package/dist/backend/schedule/ScheduleView.js.map +7 -0
  120. package/dist/backend/schedule/index.js +6 -0
  121. package/dist/backend/schedule/index.js.map +7 -0
  122. package/dist/backend/schedule/recurrence.js +83 -0
  123. package/dist/backend/schedule/recurrence.js.map +7 -0
  124. package/dist/backend/schedule/types.js +1 -0
  125. package/dist/backend/schedule/types.js.map +7 -0
  126. package/dist/backend/upgrades/UpgradeActionBanner.js +91 -0
  127. package/dist/backend/upgrades/UpgradeActionBanner.js.map +7 -0
  128. package/dist/backend/utils/api.js +127 -0
  129. package/dist/backend/utils/api.js.map +7 -0
  130. package/dist/backend/utils/apiCall.js +48 -0
  131. package/dist/backend/utils/apiCall.js.map +7 -0
  132. package/dist/backend/utils/crud.js +126 -0
  133. package/dist/backend/utils/crud.js.map +7 -0
  134. package/dist/backend/utils/customFieldColumns.js +56 -0
  135. package/dist/backend/utils/customFieldColumns.js.map +7 -0
  136. package/dist/backend/utils/customFieldDefs.js +143 -0
  137. package/dist/backend/utils/customFieldDefs.js.map +7 -0
  138. package/dist/backend/utils/customFieldFilters.js +126 -0
  139. package/dist/backend/utils/customFieldFilters.js.map +7 -0
  140. package/dist/backend/utils/customFieldForms.js +162 -0
  141. package/dist/backend/utils/customFieldForms.js.map +7 -0
  142. package/dist/backend/utils/customFieldValues.js +26 -0
  143. package/dist/backend/utils/customFieldValues.js.map +7 -0
  144. package/dist/backend/utils/flash.js +16 -0
  145. package/dist/backend/utils/flash.js.map +7 -0
  146. package/dist/backend/utils/nav.js +185 -0
  147. package/dist/backend/utils/nav.js.map +7 -0
  148. package/dist/backend/utils/serverErrors.js +230 -0
  149. package/dist/backend/utils/serverErrors.js.map +7 -0
  150. package/dist/frontend/AuthFooter.js +23 -0
  151. package/dist/frontend/AuthFooter.js.map +7 -0
  152. package/dist/frontend/LanguageSwitcher.js +57 -0
  153. package/dist/frontend/LanguageSwitcher.js.map +7 -0
  154. package/dist/frontend/Layout.js +14 -0
  155. package/dist/frontend/Layout.js.map +7 -0
  156. package/dist/index.js +32 -0
  157. package/dist/index.js.map +7 -0
  158. package/dist/primitives/DataLoader.js +67 -0
  159. package/dist/primitives/DataLoader.js.map +7 -0
  160. package/dist/primitives/ErrorNotice.js +20 -0
  161. package/dist/primitives/ErrorNotice.js.map +7 -0
  162. package/dist/primitives/alert.js +38 -0
  163. package/dist/primitives/alert.js.map +7 -0
  164. package/dist/primitives/badge.js +28 -0
  165. package/dist/primitives/badge.js.map +7 -0
  166. package/dist/primitives/button.js +44 -0
  167. package/dist/primitives/button.js.map +7 -0
  168. package/dist/primitives/card.js +91 -0
  169. package/dist/primitives/card.js.map +7 -0
  170. package/dist/primitives/checkbox.js +28 -0
  171. package/dist/primitives/checkbox.js.map +7 -0
  172. package/dist/primitives/dialog.js +90 -0
  173. package/dist/primitives/dialog.js.map +7 -0
  174. package/dist/primitives/input.js +22 -0
  175. package/dist/primitives/input.js.map +7 -0
  176. package/dist/primitives/label.js +21 -0
  177. package/dist/primitives/label.js.map +7 -0
  178. package/dist/primitives/separator.js +9 -0
  179. package/dist/primitives/separator.js.map +7 -0
  180. package/dist/primitives/spinner.js +24 -0
  181. package/dist/primitives/spinner.js.map +7 -0
  182. package/dist/primitives/switch.js +80 -0
  183. package/dist/primitives/switch.js.map +7 -0
  184. package/dist/primitives/table.js +29 -0
  185. package/dist/primitives/table.js.map +7 -0
  186. package/dist/primitives/tabs.js +87 -0
  187. package/dist/primitives/tabs.js.map +7 -0
  188. package/dist/primitives/textarea.js +21 -0
  189. package/dist/primitives/textarea.js.map +7 -0
  190. package/dist/primitives/tooltip.js +60 -0
  191. package/dist/primitives/tooltip.js.map +7 -0
  192. package/dist/theme/QueryProvider.js +44 -0
  193. package/dist/theme/QueryProvider.js.map +7 -0
  194. package/dist/theme/ThemeProvider.js +95 -0
  195. package/dist/theme/ThemeProvider.js.map +7 -0
  196. package/dist/theme/ThemeToggle.js +88 -0
  197. package/dist/theme/ThemeToggle.js.map +7 -0
  198. package/dist/theme/index.js +10 -0
  199. package/dist/theme/index.js.map +7 -0
  200. package/dist/types/react-big-calendar.d.js +1 -0
  201. package/dist/types/react-big-calendar.d.js.map +7 -0
  202. package/jest.config.cjs +23 -0
  203. package/jest.setup.ts +55 -0
  204. package/package.json +105 -0
  205. package/src/backend/AppShell.tsx +1096 -0
  206. package/src/backend/ConfirmDialog.tsx +19 -0
  207. package/src/backend/ContextHelp.tsx +38 -0
  208. package/src/backend/CrudForm.tsx +2503 -0
  209. package/src/backend/DataTable.tsx +1730 -0
  210. package/src/backend/EmptyState.tsx +65 -0
  211. package/src/backend/FilterBar.tsx +161 -0
  212. package/src/backend/FilterOverlay.tsx +328 -0
  213. package/src/backend/FlashMessages.tsx +82 -0
  214. package/src/backend/JsonBuilder.tsx +362 -0
  215. package/src/backend/JsonDisplay.tsx +254 -0
  216. package/src/backend/Page.tsx +30 -0
  217. package/src/backend/PerspectiveSidebar.tsx +337 -0
  218. package/src/backend/RowActions.tsx +151 -0
  219. package/src/backend/TruncatedCell.tsx +133 -0
  220. package/src/backend/UserMenu.tsx +118 -0
  221. package/src/backend/ValueIcons.tsx +48 -0
  222. package/src/backend/__tests__/AppShell.test.tsx +115 -0
  223. package/src/backend/__tests__/CrudForm.render.test.tsx +30 -0
  224. package/src/backend/__tests__/DataTable.render.test.tsx +48 -0
  225. package/src/backend/__tests__/custom-field-filters.test.ts +72 -0
  226. package/src/backend/__tests__/custom-field-forms.test.ts +54 -0
  227. package/src/backend/__tests__/serverErrors.test.ts +83 -0
  228. package/src/backend/custom-fields/FieldDefinitionsEditor.tsx +1292 -0
  229. package/src/backend/custom-fields/FieldDefinitionsManager.tsx +381 -0
  230. package/src/backend/dashboard/DashboardScreen.tsx +684 -0
  231. package/src/backend/dashboard/__tests__/DashboardScreen.test.tsx +112 -0
  232. package/src/backend/dashboard/index.ts +1 -0
  233. package/src/backend/dashboard/widgetRegistry.ts +68 -0
  234. package/src/backend/detail/ActivitiesSection.tsx +1284 -0
  235. package/src/backend/detail/AddressEditor.tsx +472 -0
  236. package/src/backend/detail/AddressTiles.tsx +587 -0
  237. package/src/backend/detail/AddressesSection.tsx +346 -0
  238. package/src/backend/detail/AttachmentDeleteDialog.tsx +56 -0
  239. package/src/backend/detail/AttachmentMetadataDialog.tsx +672 -0
  240. package/src/backend/detail/AttachmentsSection.tsx +414 -0
  241. package/src/backend/detail/CustomDataSection.tsx +530 -0
  242. package/src/backend/detail/DetailFieldsSection.tsx +147 -0
  243. package/src/backend/detail/ErrorMessage.tsx +32 -0
  244. package/src/backend/detail/InlineEditors.tsx +877 -0
  245. package/src/backend/detail/LoadingMessage.tsx +14 -0
  246. package/src/backend/detail/NotesSection.tsx +1275 -0
  247. package/src/backend/detail/TabEmptyState.tsx +48 -0
  248. package/src/backend/detail/TagsSection.tsx +314 -0
  249. package/src/backend/detail/addressFormat.tsx +121 -0
  250. package/src/backend/detail/index.ts +44 -0
  251. package/src/backend/fields/registry.generated.ts +8 -0
  252. package/src/backend/fields/registry.ts +38 -0
  253. package/src/backend/indexes/PartialIndexBanner.tsx +68 -0
  254. package/src/backend/indexes/store.ts +88 -0
  255. package/src/backend/injection/InjectionSpot.tsx +236 -0
  256. package/src/backend/injection/PageInjectionBoundary.tsx +31 -0
  257. package/src/backend/injection/helpers.ts +35 -0
  258. package/src/backend/injection/widgetRegistry.ts +68 -0
  259. package/src/backend/inputs/ComboboxInput.tsx +269 -0
  260. package/src/backend/inputs/LookupSelect.tsx +247 -0
  261. package/src/backend/inputs/PhoneNumberField.tsx +129 -0
  262. package/src/backend/inputs/SwitchableMarkdownInput.tsx +128 -0
  263. package/src/backend/inputs/TagsInput.tsx +259 -0
  264. package/src/backend/inputs/index.ts +5 -0
  265. package/src/backend/operations/LastOperationBanner.tsx +85 -0
  266. package/src/backend/operations/__tests__/LastOperationBanner.test.tsx +99 -0
  267. package/src/backend/operations/store.ts +230 -0
  268. package/src/backend/schedule/ScheduleAgenda.tsx +136 -0
  269. package/src/backend/schedule/ScheduleGrid.tsx +136 -0
  270. package/src/backend/schedule/ScheduleToolbar.tsx +178 -0
  271. package/src/backend/schedule/ScheduleView.tsx +198 -0
  272. package/src/backend/schedule/index.ts +5 -0
  273. package/src/backend/schedule/recurrence.ts +99 -0
  274. package/src/backend/schedule/types.ts +26 -0
  275. package/src/backend/upgrades/UpgradeActionBanner.tsx +128 -0
  276. package/src/backend/utils/__tests__/apiCall.test.ts +109 -0
  277. package/src/backend/utils/__tests__/crud.test.ts +87 -0
  278. package/src/backend/utils/__tests__/customFieldDefs.test.ts +25 -0
  279. package/src/backend/utils/__tests__/customFieldValues.test.ts +35 -0
  280. package/src/backend/utils/api.ts +149 -0
  281. package/src/backend/utils/apiCall.ts +96 -0
  282. package/src/backend/utils/crud.ts +174 -0
  283. package/src/backend/utils/customFieldColumns.ts +71 -0
  284. package/src/backend/utils/customFieldDefs.ts +245 -0
  285. package/src/backend/utils/customFieldFilters.ts +145 -0
  286. package/src/backend/utils/customFieldForms.ts +196 -0
  287. package/src/backend/utils/customFieldValues.ts +41 -0
  288. package/src/backend/utils/flash.ts +17 -0
  289. package/src/backend/utils/nav.ts +238 -0
  290. package/src/backend/utils/serverErrors.ts +302 -0
  291. package/src/frontend/AuthFooter.tsx +29 -0
  292. package/src/frontend/LanguageSwitcher.tsx +66 -0
  293. package/src/frontend/Layout.tsx +13 -0
  294. package/src/index.ts +32 -0
  295. package/src/primitives/DataLoader.tsx +92 -0
  296. package/src/primitives/ErrorNotice.tsx +26 -0
  297. package/src/primitives/alert.tsx +52 -0
  298. package/src/primitives/badge.tsx +31 -0
  299. package/src/primitives/button.tsx +47 -0
  300. package/src/primitives/card.tsx +92 -0
  301. package/src/primitives/checkbox.tsx +28 -0
  302. package/src/primitives/dialog.tsx +110 -0
  303. package/src/primitives/input.tsx +20 -0
  304. package/src/primitives/label.tsx +18 -0
  305. package/src/primitives/separator.tsx +7 -0
  306. package/src/primitives/spinner.tsx +27 -0
  307. package/src/primitives/switch.tsx +86 -0
  308. package/src/primitives/table.tsx +27 -0
  309. package/src/primitives/tabs.tsx +128 -0
  310. package/src/primitives/textarea.tsx +20 -0
  311. package/src/primitives/tooltip.tsx +85 -0
  312. package/src/theme/QueryProvider.tsx +46 -0
  313. package/src/theme/ThemeProvider.tsx +120 -0
  314. package/src/theme/ThemeToggle.tsx +88 -0
  315. package/src/theme/index.ts +3 -0
  316. package/src/types/react-big-calendar.d.ts +16 -0
  317. package/tsconfig.build.json +11 -0
  318. package/tsconfig.json +9 -0
  319. package/watch.mjs +6 -0
@@ -0,0 +1,2028 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import Link from "next/link";
5
+ import { useRouter } from "next/navigation";
6
+ import { Button } from "../primitives/button.js";
7
+ import { DataLoader } from "../primitives/DataLoader.js";
8
+ import { flash } from "./FlashMessages.js";
9
+ import dynamic from "next/dynamic";
10
+ import remarkGfm from "remark-gfm";
11
+ import {
12
+ Trash2,
13
+ Save,
14
+ Settings,
15
+ Layers,
16
+ Tag,
17
+ Sparkles,
18
+ Package,
19
+ Shirt,
20
+ Grid,
21
+ ShoppingBag,
22
+ ShoppingCart,
23
+ Store,
24
+ Users,
25
+ Briefcase,
26
+ Building,
27
+ BookOpen,
28
+ Bookmark,
29
+ Camera,
30
+ Car,
31
+ Clock,
32
+ Cloud,
33
+ Compass,
34
+ CreditCard,
35
+ Database,
36
+ Flame,
37
+ Gift,
38
+ Globe,
39
+ Heart,
40
+ Key,
41
+ Map as MapIcon,
42
+ Palette,
43
+ Shield,
44
+ Star,
45
+ Truck,
46
+ Zap,
47
+ Coins
48
+ } from "lucide-react";
49
+ import { loadGeneratedFieldRegistrations } from "./fields/registry.js";
50
+ import { buildFormFieldFromCustomFieldDef } from "./utils/customFieldForms.js";
51
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
52
+ import { TagsInput } from "./inputs/TagsInput.js";
53
+ import { ComboboxInput } from "./inputs/ComboboxInput.js";
54
+ import { mapCrudServerErrorToFormErrors, parseServerMessage } from "./utils/serverErrors.js";
55
+ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../primitives/dialog.js";
56
+ import { FieldDefinitionsManager } from "./custom-fields/FieldDefinitionsManager.js";
57
+ import { useInjectionSpotEvents, InjectionSpot, useInjectionWidgets } from "./injection/InjectionSpot.js";
58
+ const EMPTY_OPTIONS = [];
59
+ const FOCUSABLE_SELECTOR = '[data-crud-focus-target], input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
60
+ const FIELDSET_ICON_COMPONENTS = {
61
+ layers: Layers,
62
+ tag: Tag,
63
+ sparkles: Sparkles,
64
+ package: Package,
65
+ shirt: Shirt,
66
+ grid: Grid,
67
+ shoppingBag: ShoppingBag,
68
+ shoppingCart: ShoppingCart,
69
+ store: Store,
70
+ users: Users,
71
+ briefcase: Briefcase,
72
+ building: Building,
73
+ bookOpen: BookOpen,
74
+ bookmark: Bookmark,
75
+ camera: Camera,
76
+ car: Car,
77
+ clock: Clock,
78
+ cloud: Cloud,
79
+ compass: Compass,
80
+ creditCard: CreditCard,
81
+ database: Database,
82
+ flame: Flame,
83
+ gift: Gift,
84
+ globe: Globe,
85
+ heart: Heart,
86
+ key: Key,
87
+ map: MapIcon,
88
+ palette: Palette,
89
+ shield: Shield,
90
+ star: Star,
91
+ truck: Truck,
92
+ zap: Zap,
93
+ coins: Coins
94
+ };
95
+ function CrudForm({
96
+ schema,
97
+ fields,
98
+ initialValues,
99
+ submitLabel,
100
+ customFieldsLoadingMessage,
101
+ cancelHref,
102
+ successRedirect,
103
+ deleteRedirect,
104
+ onSubmit,
105
+ onDelete,
106
+ deleteVisible,
107
+ twoColumn = false,
108
+ title,
109
+ backHref,
110
+ entityId,
111
+ entityIds,
112
+ groups,
113
+ isLoading = false,
114
+ loadingMessage,
115
+ customEntity = false,
116
+ embedded = false,
117
+ extraActions,
118
+ contentHeader,
119
+ customFieldsetBindings,
120
+ injectionSpotId
121
+ }) {
122
+ React.useEffect(() => {
123
+ loadGeneratedFieldRegistrations().catch(() => {
124
+ });
125
+ }, []);
126
+ const router = useRouter();
127
+ const t = useT();
128
+ const resolvedSubmitLabel = submitLabel ?? t("ui.forms.actions.save");
129
+ const resolvedLoadingMessage = loadingMessage ?? t("ui.forms.loading");
130
+ const resolvedCustomFieldsLoadingMessage = customFieldsLoadingMessage ?? resolvedLoadingMessage;
131
+ const cancelLabel = t("ui.forms.actions.cancel");
132
+ const deleteLabel = t("ui.forms.actions.delete");
133
+ const savingLabel = t("ui.forms.status.saving");
134
+ const backLabel = t("ui.navigation.back");
135
+ const customFieldsLabel = t("entities.customFields.title");
136
+ const fieldsetSelectorLabel = t("entities.customFields.fieldsetSelectorLabel", "Fieldset");
137
+ const emptyFieldsetMessage = t("entities.customFields.emptyFieldset", "No fields defined for this fieldset.");
138
+ const defaultFieldsetLabel = t("entities.customFields.defaultFieldset", "Default");
139
+ const manageFieldsetLabel = t("entities.customFields.manageFieldset", "Manage fields");
140
+ const fieldsetDialogTitle = t("entities.customFields.manageDialogTitle", "Edit custom fields");
141
+ const fieldsetDialogUnavailable = t("entities.customFields.manageDialogUnavailable", "Field definitions page is unavailable.");
142
+ const deleteConfirmMessage = t("ui.forms.confirmDelete");
143
+ const deleteSuccessMessage = t("ui.forms.flash.deleteSuccess");
144
+ const deleteErrorMessage = t("ui.forms.flash.deleteError");
145
+ const saveErrorMessage = t("ui.forms.flash.saveError");
146
+ const formId = React.useId();
147
+ const [values, setValues] = React.useState(
148
+ () => ({ ...initialValues ?? {} })
149
+ );
150
+ const [errors, setErrors] = React.useState({});
151
+ const [pending, setPending] = React.useState(false);
152
+ const [formError, setFormError] = React.useState(null);
153
+ const [dynamicOptions, setDynamicOptions] = React.useState({});
154
+ const [cfDefinitions, setCfDefinitions] = React.useState([]);
155
+ const [cfMetadata, setCfMetadata] = React.useState(null);
156
+ const [cfFieldsetSelections, setCfFieldsetSelections] = React.useState({});
157
+ const [isLoadingCustomFields, setIsLoadingCustomFields] = React.useState(false);
158
+ const [customFieldDefsVersion, setCustomFieldDefsVersion] = React.useState(0);
159
+ const [fieldsetEditorTarget, setFieldsetEditorTarget] = React.useState(null);
160
+ const [isInDialog, setIsInDialog] = React.useState(false);
161
+ const rootRef = React.useRef(null);
162
+ const fieldsetManagerRef = React.useRef(null);
163
+ const resolvedEntityIds = React.useMemo(() => {
164
+ if (Array.isArray(entityIds) && entityIds.length) {
165
+ const dedup = /* @__PURE__ */ new Set();
166
+ const list = [];
167
+ entityIds.forEach((id) => {
168
+ const trimmed = typeof id === "string" ? id.trim() : "";
169
+ if (!trimmed || dedup.has(trimmed)) return;
170
+ dedup.add(trimmed);
171
+ list.push(trimmed);
172
+ });
173
+ return list;
174
+ }
175
+ if (typeof entityId === "string" && entityId.trim().length > 0) {
176
+ return [entityId.trim()];
177
+ }
178
+ return [];
179
+ }, [entityId, entityIds]);
180
+ const primaryEntityId = resolvedEntityIds.length ? resolvedEntityIds[0] : null;
181
+ const resolvedInjectionSpotId = React.useMemo(() => {
182
+ if (injectionSpotId) return injectionSpotId;
183
+ if (resolvedEntityIds.length) {
184
+ const normalized = resolvedEntityIds[0].replace(/[:]+/g, ".");
185
+ return `crud-form:${normalized}`;
186
+ }
187
+ return void 0;
188
+ }, [injectionSpotId, resolvedEntityIds]);
189
+ const injectionContext = React.useMemo(() => ({
190
+ formId,
191
+ entityId: primaryEntityId,
192
+ isLoading,
193
+ pending
194
+ }), [formId, primaryEntityId, isLoading, pending]);
195
+ const { widgets: injectionWidgets } = useInjectionWidgets(resolvedInjectionSpotId, {
196
+ context: injectionContext,
197
+ triggerOnLoad: true
198
+ });
199
+ const { triggerEvent: triggerInjectionEvent } = useInjectionSpotEvents(resolvedInjectionSpotId ?? "", injectionWidgets);
200
+ React.useEffect(() => {
201
+ const root = rootRef.current;
202
+ if (!root) return;
203
+ setIsInDialog(Boolean(root.closest("[data-dialog-content]")));
204
+ }, []);
205
+ const dialogFooterClass = isInDialog ? "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" : "";
206
+ const dialogFormPadding = isInDialog ? "pb-4" : "";
207
+ const buildCustomFieldsManageHref = React.useCallback(
208
+ (targetEntityId) => {
209
+ if (!targetEntityId) return null;
210
+ try {
211
+ const encoded = encodeURIComponent(targetEntityId);
212
+ return customEntity ? `/backend/entities/user/${encoded}` : `/backend/entities/system/${encoded}`;
213
+ } catch {
214
+ return null;
215
+ }
216
+ },
217
+ [customEntity]
218
+ );
219
+ const refreshCustomFieldDefinitions = React.useCallback(() => {
220
+ setCustomFieldDefsVersion((prev) => prev + 1);
221
+ }, []);
222
+ const recordId = React.useMemo(() => {
223
+ const raw = values.id;
224
+ if (typeof raw === "string") return raw;
225
+ if (typeof raw === "number") return String(raw);
226
+ return void 0;
227
+ }, [values]);
228
+ const handleDelete = React.useCallback(async () => {
229
+ if (!onDelete) return;
230
+ try {
231
+ const ok = typeof window !== "undefined" ? window.confirm(deleteConfirmMessage) : true;
232
+ if (!ok) return;
233
+ await onDelete();
234
+ try {
235
+ flash(deleteSuccessMessage, "success");
236
+ } catch {
237
+ }
238
+ if (typeof deleteRedirect === "string" && deleteRedirect) {
239
+ router.push(deleteRedirect);
240
+ }
241
+ } catch (err) {
242
+ const message = err instanceof Error && err.message ? err.message : deleteErrorMessage;
243
+ try {
244
+ flash(message, "error");
245
+ } catch {
246
+ }
247
+ }
248
+ }, [onDelete, deleteRedirect, router, deleteConfirmMessage, deleteSuccessMessage, deleteErrorMessage]);
249
+ const isNewRecord = React.useMemo(() => {
250
+ const rawId = values.id;
251
+ if (rawId === void 0 || rawId === null) return true;
252
+ return typeof rawId === "string" ? rawId.trim().length === 0 : false;
253
+ }, [values]);
254
+ const showDelete = Boolean(onDelete) && (typeof deleteVisible === "boolean" ? deleteVisible : !isNewRecord);
255
+ React.useEffect(() => {
256
+ let cancelled = false;
257
+ async function load() {
258
+ if (!resolvedEntityIds.length) {
259
+ setCfDefinitions([]);
260
+ setCfMetadata(null);
261
+ setIsLoadingCustomFields(false);
262
+ return;
263
+ }
264
+ setIsLoadingCustomFields(true);
265
+ try {
266
+ const mod = await import("./utils/customFieldForms.js");
267
+ const { definitions, metadata } = await mod.fetchCustomFieldFormStructure(resolvedEntityIds, void 0, { bareIds: customEntity });
268
+ if (!cancelled) {
269
+ setCfDefinitions(definitions);
270
+ setCfMetadata(metadata);
271
+ setCfFieldsetSelections((prev) => {
272
+ const next = {};
273
+ let changed = false;
274
+ resolvedEntityIds.forEach((entityId2) => {
275
+ const existing = prev[entityId2];
276
+ const fieldsets = metadata.fieldsetsByEntity?.[entityId2] ?? [];
277
+ const defaultSelection = fieldsets[0]?.code ?? null;
278
+ const value = existing !== void 0 ? existing : defaultSelection;
279
+ next[entityId2] = value;
280
+ if (existing !== value) changed = true;
281
+ });
282
+ if (Object.keys(prev).length !== Object.keys(next).length) changed = true;
283
+ return changed ? next : prev;
284
+ });
285
+ setIsLoadingCustomFields(false);
286
+ }
287
+ } catch {
288
+ if (!cancelled) {
289
+ setCfDefinitions([]);
290
+ setCfMetadata(null);
291
+ setIsLoadingCustomFields(false);
292
+ }
293
+ }
294
+ }
295
+ load();
296
+ return () => {
297
+ cancelled = true;
298
+ };
299
+ }, [resolvedEntityIds, customEntity, customFieldDefsVersion]);
300
+ React.useEffect(() => {
301
+ if (!customFieldsetBindings) return;
302
+ setCfFieldsetSelections((prev) => {
303
+ let changed = false;
304
+ const next = { ...prev };
305
+ resolvedEntityIds.forEach((entityId2) => {
306
+ const binding = customFieldsetBindings[entityId2];
307
+ if (!binding) return;
308
+ const raw = values[binding.valueKey];
309
+ if (typeof raw === "string" && raw.trim().length > 0) {
310
+ const normalized = raw.trim();
311
+ if (next[entityId2] !== normalized) {
312
+ next[entityId2] = normalized;
313
+ changed = true;
314
+ }
315
+ }
316
+ });
317
+ return changed ? next : prev;
318
+ });
319
+ }, [customFieldsetBindings, resolvedEntityIds, values]);
320
+ const fieldsetsByEntity = cfMetadata?.fieldsetsByEntity ?? {};
321
+ const entitySettings = cfMetadata?.entitySettings ?? {};
322
+ const { cfFields, customFieldLayout } = React.useMemo(() => {
323
+ if (!cfDefinitions.length) return { cfFields: [], customFieldLayout: [] };
324
+ const aggregated = [];
325
+ const layout = [];
326
+ const defsByEntity = new globalThis.Map();
327
+ cfDefinitions.forEach((def) => {
328
+ const entityId2 = typeof def.entityId === "string" && def.entityId.trim().length ? def.entityId.trim() : resolvedEntityIds[0];
329
+ if (!entityId2) return;
330
+ const bucket = defsByEntity.get(entityId2) ?? [];
331
+ bucket.push(def);
332
+ defsByEntity.set(entityId2, bucket);
333
+ });
334
+ const buildSection = (entityId2, fieldsetCode, defList, fieldset) => {
335
+ if (!defList.length) return null;
336
+ const groupsMap = new globalThis.Map();
337
+ const order = [];
338
+ const fieldsetGroupMap = new globalThis.Map();
339
+ if (Array.isArray(fieldset?.groups)) {
340
+ fieldset.groups.forEach((group) => {
341
+ if (!group?.code) return;
342
+ fieldsetGroupMap.set(group.code, { code: group.code, title: group.title, hint: group.hint });
343
+ });
344
+ }
345
+ const sortedDefs = [...defList].sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
346
+ const ensureBucket = (code, def) => {
347
+ const key = code ?? "__default__";
348
+ let bucket = groupsMap.get(key);
349
+ if (!bucket) {
350
+ const fallbackMeta = code ? fieldsetGroupMap.get(code) : void 0;
351
+ const directMeta = code ? def.group : void 0;
352
+ const label = code === null ? void 0 : directMeta?.title || fallbackMeta?.title || directMeta?.code || fallbackMeta?.code || code;
353
+ const hint = directMeta?.hint || fallbackMeta?.hint;
354
+ bucket = { code, label, hint, fields: [] };
355
+ groupsMap.set(key, bucket);
356
+ order.push(key);
357
+ } else if (code && !bucket.label) {
358
+ const fallbackMeta = fieldsetGroupMap.get(code);
359
+ const directMeta = def.group ?? void 0;
360
+ bucket.label = directMeta?.title || fallbackMeta?.title || directMeta?.code || fallbackMeta?.code || bucket.label;
361
+ bucket.hint = directMeta?.hint || fallbackMeta?.hint || bucket.hint;
362
+ }
363
+ return bucket;
364
+ };
365
+ sortedDefs.forEach((definition) => {
366
+ const field = buildFormFieldFromCustomFieldDef(definition, { bareIds: customEntity });
367
+ if (!field) return;
368
+ aggregated.push(field);
369
+ const bucket = ensureBucket(definition.group?.code ?? null, definition);
370
+ bucket.fields.push(field);
371
+ });
372
+ const groups2 = order.map((key) => groupsMap.get(key)).filter((group) => group.fields.length > 0);
373
+ if (!groups2.length) return null;
374
+ return {
375
+ entityId: entityId2,
376
+ fieldsetCode,
377
+ fieldset,
378
+ title: fieldset?.label ?? customFieldsLabel,
379
+ description: fieldset?.description,
380
+ groups: groups2
381
+ };
382
+ };
383
+ const entityIds2 = resolvedEntityIds.length ? resolvedEntityIds : Array.from(defsByEntity.keys());
384
+ entityIds2.forEach((entityId2) => {
385
+ const defsForEntity = defsByEntity.get(entityId2) ?? [];
386
+ if (!defsForEntity.length) return;
387
+ const availableFieldsets = fieldsetsByEntity[entityId2] ?? [];
388
+ const hasFieldsets = availableFieldsets.length > 0;
389
+ const singleFieldsetPerRecord = entitySettings[entityId2]?.singleFieldsetPerRecord !== false;
390
+ const defsByFieldset = new globalThis.Map();
391
+ defsForEntity.forEach((def) => {
392
+ const code = typeof def.fieldset === "string" && def.fieldset.trim().length > 0 ? def.fieldset.trim() : null;
393
+ const bucket = defsByFieldset.get(code) ?? [];
394
+ bucket.push(def);
395
+ defsByFieldset.set(code, bucket);
396
+ });
397
+ const sections = [];
398
+ const createEmptySection = (code) => {
399
+ const fieldset = code ? availableFieldsets.find((fs) => fs.code === code) : void 0;
400
+ return {
401
+ entityId: entityId2,
402
+ fieldsetCode: code,
403
+ fieldset,
404
+ title: fieldset?.label ?? customFieldsLabel,
405
+ description: fieldset?.description,
406
+ groups: []
407
+ };
408
+ };
409
+ if (!hasFieldsets) {
410
+ const fallbackDefs = defsByFieldset.get(null) ?? Array.from(defsByFieldset.values()).flat();
411
+ const section = buildSection(entityId2, null, fallbackDefs, void 0);
412
+ if (section) sections.push(section);
413
+ } else if (singleFieldsetPerRecord) {
414
+ const availableCodes = availableFieldsets.map((fs) => fs.code);
415
+ const activeFieldset = cfFieldsetSelections[entityId2] && availableCodes.includes(cfFieldsetSelections[entityId2]) ? cfFieldsetSelections[entityId2] : availableFieldsets[0]?.code ?? null;
416
+ const targetDefs = activeFieldset ? defsByFieldset.get(activeFieldset) ?? [] : defsByFieldset.get(null) ?? [];
417
+ const targetSection = activeFieldset ? buildSection(
418
+ entityId2,
419
+ activeFieldset,
420
+ targetDefs,
421
+ availableFieldsets.find((fs) => fs.code === activeFieldset)
422
+ ) : buildSection(entityId2, null, targetDefs, void 0);
423
+ if (targetSection) {
424
+ sections.push(targetSection);
425
+ } else if (activeFieldset) {
426
+ sections.push(createEmptySection(activeFieldset));
427
+ }
428
+ const unassigned = defsByFieldset.get(null);
429
+ if (unassigned?.length && activeFieldset) {
430
+ const generalSection = buildSection(entityId2, null, unassigned, void 0);
431
+ if (generalSection) sections.push(generalSection);
432
+ }
433
+ } else {
434
+ availableFieldsets.forEach((fieldset) => {
435
+ const list = defsByFieldset.get(fieldset.code) ?? [];
436
+ const section = buildSection(entityId2, fieldset.code, list, fieldset);
437
+ if (section) sections.push(section);
438
+ });
439
+ const unassigned = defsByFieldset.get(null);
440
+ if (unassigned?.length) {
441
+ const section = buildSection(entityId2, null, unassigned, void 0);
442
+ if (section) sections.push(section);
443
+ }
444
+ }
445
+ if (!sections.length && hasFieldsets) {
446
+ const fallbackCode = availableFieldsets[0]?.code ?? null;
447
+ sections.push(createEmptySection(fallbackCode));
448
+ }
449
+ layout.push({
450
+ entityId: entityId2,
451
+ sections,
452
+ availableFieldsets,
453
+ singleFieldsetPerRecord,
454
+ hasFieldsets,
455
+ activeFieldset: cfFieldsetSelections[entityId2] ?? availableFieldsets[0]?.code ?? null
456
+ });
457
+ });
458
+ return { cfFields: aggregated, customFieldLayout: layout };
459
+ }, [
460
+ cfDefinitions,
461
+ cfFieldsetSelections,
462
+ customEntity,
463
+ customFieldsLabel,
464
+ entitySettings,
465
+ fieldsetsByEntity,
466
+ resolvedEntityIds
467
+ ]);
468
+ const allFields = React.useMemo(() => {
469
+ if (!cfFields.length) return fields;
470
+ const provided = new Set(fields.map((f) => f.id));
471
+ const extras = cfFields.filter((f) => !provided.has(f.id));
472
+ return [...fields, ...extras];
473
+ }, [fields, cfFields]);
474
+ const fieldById = React.useMemo(() => {
475
+ return new globalThis.Map(allFields.map((f) => [f.id, f]));
476
+ }, [allFields]);
477
+ const injectionGroupCards = React.useMemo(() => {
478
+ if (!injectionWidgets || injectionWidgets.length === 0) return [];
479
+ const pairs = injectionWidgets.filter((widget) => (widget.placement?.kind ?? "stack") === "group").map((widget) => {
480
+ const priority = typeof widget.placement?.priority === "number" ? widget.placement.priority : 0;
481
+ const group = {
482
+ id: `widget:${widget.widgetId}`,
483
+ title: widget.placement?.groupLabel ?? widget.module.metadata.title,
484
+ description: widget.placement?.groupDescription ?? widget.module.metadata.description,
485
+ column: widget.placement?.column === 2 ? 2 : 1,
486
+ component: () => /* @__PURE__ */ jsx(
487
+ widget.module.Widget,
488
+ {
489
+ context: injectionContext,
490
+ data: values,
491
+ onDataChange: (next) => setValues(next),
492
+ disabled: pending
493
+ }
494
+ )
495
+ };
496
+ return { group, priority };
497
+ });
498
+ pairs.sort((a, b) => b.priority - a.priority);
499
+ return pairs.map((p) => p.group);
500
+ }, [injectionWidgets, injectionContext, pending, setValues, values]);
501
+ const shouldAutoGroup = (!groups || groups.length === 0) && injectionGroupCards.length > 0;
502
+ const resolvedGroupsForLayout = React.useMemo(() => {
503
+ const baseGroups = groups && groups.length ? groups : [];
504
+ const autoGroup = shouldAutoGroup ? [{ id: "__auto-fields__", fields: allFields }] : [];
505
+ return [...baseGroups.length ? baseGroups : autoGroup, ...injectionGroupCards];
506
+ }, [allFields, groups, injectionGroupCards, shouldAutoGroup]);
507
+ const useGroupedLayout = resolvedGroupsForLayout.length > 0;
508
+ const stackedInjectionWidgets = React.useMemo(
509
+ () => (injectionWidgets ?? []).filter((widget) => (widget.placement?.kind ?? "stack") === "stack"),
510
+ [injectionWidgets]
511
+ );
512
+ const resolveGroupFields = React.useCallback((g) => {
513
+ if (g.kind === "customFields") {
514
+ return cfFields;
515
+ }
516
+ const src = g.fields || [];
517
+ const result = [];
518
+ for (const item of src) {
519
+ if (typeof item === "string") {
520
+ const found = fieldById.get(item);
521
+ if (found) result.push(found);
522
+ } else if (item) {
523
+ result.push(item);
524
+ }
525
+ }
526
+ return result;
527
+ }, [cfFields, fieldById]);
528
+ const customFieldsManageHref = React.useMemo(() => buildCustomFieldsManageHref(primaryEntityId), [buildCustomFieldsManageHref, primaryEntityId]);
529
+ const customFieldsEmptyState = React.useMemo(() => {
530
+ const text = t("entities.customFields.empty");
531
+ const action = t("entities.customFields.addFirst");
532
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-md border border-dashed border-muted-foreground/50 bg-muted/10 px-3 py-4 text-sm text-muted-foreground", children: [
533
+ /* @__PURE__ */ jsxs("span", { children: [
534
+ text,
535
+ " "
536
+ ] }),
537
+ customFieldsManageHref ? /* @__PURE__ */ jsx(Link, { href: customFieldsManageHref, className: "font-medium text-primary hover:underline", children: action }) : /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: action })
538
+ ] });
539
+ }, [customFieldsManageHref, t]);
540
+ const firstFieldId = React.useMemo(() => {
541
+ if (useGroupedLayout) {
542
+ const col1 = [];
543
+ const col2 = [];
544
+ for (const g of resolvedGroupsForLayout) {
545
+ if ((g.column ?? 1) === 2) col2.push(g);
546
+ else col1.push(g);
547
+ }
548
+ const scan = (list) => {
549
+ for (const group of list) {
550
+ const resolved = resolveGroupFields(group);
551
+ for (const field of resolved) {
552
+ if (field?.id && !field.disabled) return field.id;
553
+ }
554
+ }
555
+ return null;
556
+ };
557
+ const fromCol1 = scan(col1);
558
+ if (fromCol1) return fromCol1;
559
+ const fromCol2 = scan(col2);
560
+ if (fromCol2) return fromCol2;
561
+ }
562
+ for (const field of allFields) {
563
+ if (field?.id && !field.disabled) return field.id;
564
+ }
565
+ return null;
566
+ }, [allFields, resolveGroupFields, resolvedGroupsForLayout, useGroupedLayout]);
567
+ const requestSubmit = React.useCallback(() => {
568
+ if (typeof document === "undefined") return;
569
+ const form = document.getElementById(formId);
570
+ form?.requestSubmit();
571
+ }, [formId]);
572
+ const lastFocusedFieldRef = React.useRef(null);
573
+ const lastErrorFieldRef = React.useRef(null);
574
+ React.useEffect(() => {
575
+ if (typeof window === "undefined" || typeof document === "undefined") return;
576
+ if (isLoading || isLoadingCustomFields) {
577
+ lastFocusedFieldRef.current = null;
578
+ return;
579
+ }
580
+ if (!firstFieldId) return;
581
+ if (lastFocusedFieldRef.current === firstFieldId) return;
582
+ const run = () => {
583
+ const form = document.getElementById(formId);
584
+ if (!form) return;
585
+ const active = typeof document !== "undefined" ? document.activeElement : null;
586
+ if (active && form.contains(active)) {
587
+ return;
588
+ }
589
+ const container = form.querySelector(`[data-crud-field-id="${firstFieldId}"]`);
590
+ const target = container?.querySelector(FOCUSABLE_SELECTOR) ?? form.querySelector(FOCUSABLE_SELECTOR);
591
+ if (target && typeof target.focus === "function") {
592
+ target.focus();
593
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
594
+ try {
595
+ target.select();
596
+ } catch {
597
+ }
598
+ }
599
+ lastFocusedFieldRef.current = firstFieldId;
600
+ }
601
+ };
602
+ const frame = typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame(run) : window.setTimeout(run, 0);
603
+ return () => {
604
+ if (typeof window === "undefined") return;
605
+ if (typeof window.cancelAnimationFrame === "function") {
606
+ window.cancelAnimationFrame(frame);
607
+ } else {
608
+ window.clearTimeout(frame);
609
+ }
610
+ };
611
+ }, [firstFieldId, formId, isLoading, isLoadingCustomFields]);
612
+ React.useEffect(() => {
613
+ if (typeof window === "undefined" || typeof document === "undefined") return;
614
+ const entries = Object.entries(errors);
615
+ if (!entries.length) {
616
+ lastErrorFieldRef.current = null;
617
+ return;
618
+ }
619
+ const [fieldId] = entries[0];
620
+ if (!fieldId || lastErrorFieldRef.current === fieldId) return;
621
+ const form = document.getElementById(formId);
622
+ if (!form) return;
623
+ const container = form.querySelector(`[data-crud-field-id="${fieldId}"]`);
624
+ const target = container?.querySelector(FOCUSABLE_SELECTOR) ?? form.querySelector(`[name="${fieldId}"]`) ?? container ?? null;
625
+ if (target && typeof target.focus === "function") {
626
+ target.focus();
627
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
628
+ try {
629
+ target.select();
630
+ } catch {
631
+ }
632
+ }
633
+ lastErrorFieldRef.current = fieldId;
634
+ }
635
+ }, [errors, formId]);
636
+ const setValue = React.useCallback((id, nextValue) => {
637
+ setValues((prev) => {
638
+ if (Object.is(prev[id], nextValue)) return prev;
639
+ return { ...prev, [id]: nextValue };
640
+ });
641
+ }, []);
642
+ const handleFieldsetSelectionChange = React.useCallback(
643
+ (entityId2, nextCode) => {
644
+ setCfFieldsetSelections((prev) => ({ ...prev, [entityId2]: nextCode }));
645
+ const bindingKey = customFieldsetBindings?.[entityId2]?.valueKey;
646
+ if (bindingKey) {
647
+ setValue(bindingKey, nextCode ?? void 0);
648
+ }
649
+ },
650
+ [customFieldsetBindings, setValue]
651
+ );
652
+ const handleOpenFieldsetEditor = React.useCallback(
653
+ (entityId2, fieldsetCode, view = "entity") => {
654
+ const href = buildCustomFieldsManageHref(entityId2);
655
+ if (!href) return;
656
+ setFieldsetEditorTarget({ entityId: entityId2, fieldsetCode, view });
657
+ },
658
+ [buildCustomFieldsManageHref]
659
+ );
660
+ React.useEffect(() => {
661
+ if (!initialValues) return;
662
+ setValues((prev) => ({ ...prev, ...initialValues }));
663
+ }, [initialValues]);
664
+ const buildFieldsetEditorHref = React.useCallback(
665
+ (includeViewParam) => {
666
+ if (!fieldsetEditorTarget) return null;
667
+ const base = buildCustomFieldsManageHref(fieldsetEditorTarget.entityId);
668
+ if (!base) return null;
669
+ const params = [];
670
+ if (fieldsetEditorTarget.fieldsetCode) {
671
+ params.push(`fieldset=${encodeURIComponent(fieldsetEditorTarget.fieldsetCode)}`);
672
+ }
673
+ if (includeViewParam && fieldsetEditorTarget.view === "fieldset") {
674
+ params.push("view=fieldset");
675
+ }
676
+ if (!params.length) return base;
677
+ const connector = base.includes("?") ? "&" : "?";
678
+ return `${base}${connector}${params.join("&")}`;
679
+ },
680
+ [buildCustomFieldsManageHref, fieldsetEditorTarget]
681
+ );
682
+ const fieldsetEditorFullHref = React.useMemo(() => buildFieldsetEditorHref(false), [buildFieldsetEditorHref]);
683
+ const handleFieldsetDialogSave = React.useCallback(() => {
684
+ if (!fieldsetManagerRef.current) return;
685
+ void fieldsetManagerRef.current.submit();
686
+ }, []);
687
+ const handleSubmit = async (e) => {
688
+ e.preventDefault();
689
+ setFormError(null);
690
+ setErrors({});
691
+ const requiredMessage = t("ui.forms.errors.required");
692
+ const highlightedMessage = t("ui.forms.errors.highlighted");
693
+ try {
694
+ if (typeof document !== "undefined") {
695
+ const activeElement = document.activeElement;
696
+ if (activeElement instanceof HTMLElement) {
697
+ activeElement.blur();
698
+ await new Promise((resolve) => setTimeout(resolve, 0));
699
+ }
700
+ }
701
+ } catch {
702
+ }
703
+ const requiredErrors = {};
704
+ for (const field of allFields) {
705
+ if (!field.required) continue;
706
+ if (field.disabled) continue;
707
+ const v = values[field.id];
708
+ const isArray = Array.isArray(v);
709
+ const isString = typeof v === "string";
710
+ const empty = v === void 0 || v === null || isString && v.trim() === "" || isArray && v.length === 0 || field.type === "checkbox" && v !== true;
711
+ if (empty) requiredErrors[field.id] = requiredMessage;
712
+ }
713
+ if (Object.keys(requiredErrors).length) {
714
+ if (process.env.NODE_ENV !== "production") {
715
+ console.debug("[crud-form] Required field errors prevented submit", requiredErrors);
716
+ }
717
+ setErrors(requiredErrors);
718
+ flash(highlightedMessage, "error");
719
+ return;
720
+ }
721
+ if (resolvedEntityIds.length) {
722
+ try {
723
+ const mod = await import("./utils/customFieldDefs.js");
724
+ const defs = await mod.fetchCustomFieldDefs(resolvedEntityIds);
725
+ const { validateValuesAgainstDefs } = await import("@open-mercato/shared/modules/entities/validation");
726
+ const cfValues = {};
727
+ if (customEntity) {
728
+ for (const def of defs) {
729
+ if (Object.prototype.hasOwnProperty.call(values, def.key)) {
730
+ cfValues[def.key] = values[def.key];
731
+ }
732
+ }
733
+ } else {
734
+ for (const [k, v] of Object.entries(values)) {
735
+ if (k.startsWith("cf_")) cfValues[k.replace(/^cf_/, "")] = v;
736
+ }
737
+ }
738
+ const defsForValidation = defs;
739
+ const result = validateValuesAgainstDefs(cfValues, defsForValidation);
740
+ if (!result.ok) {
741
+ if (customEntity) {
742
+ const mapped = {};
743
+ for (const [ek, ev] of Object.entries(result.fieldErrors)) mapped[ek.replace(/^cf_/, "")] = String(ev);
744
+ setErrors((prev) => ({ ...prev, ...mapped }));
745
+ } else {
746
+ setErrors((prev) => ({ ...prev, ...result.fieldErrors }));
747
+ }
748
+ flash(highlightedMessage, "error");
749
+ return;
750
+ }
751
+ } catch {
752
+ }
753
+ }
754
+ let parsedValues;
755
+ if (schema) {
756
+ const res = schema.safeParse(values);
757
+ if (!res.success) {
758
+ const fieldErrors = {};
759
+ res.error.issues.forEach((issue) => {
760
+ if (issue.path && issue.path.length) fieldErrors[String(issue.path[0])] = issue.message;
761
+ });
762
+ if (process.env.NODE_ENV !== "production") {
763
+ console.debug("[crud-form] Schema validation failed", res.error.issues);
764
+ }
765
+ setErrors(fieldErrors);
766
+ flash(highlightedMessage, "error");
767
+ return;
768
+ }
769
+ parsedValues = res.data;
770
+ } else {
771
+ parsedValues = values;
772
+ }
773
+ if (resolvedInjectionSpotId) {
774
+ try {
775
+ const result = await triggerInjectionEvent("onBeforeSave", parsedValues, injectionContext);
776
+ if (!result.ok) {
777
+ if (result.fieldErrors && Object.keys(result.fieldErrors).length) {
778
+ setErrors(result.fieldErrors);
779
+ }
780
+ const message = result.message || t("ui.forms.flash.saveBlocked", "Save blocked by validation");
781
+ flash(message, "error");
782
+ setPending(false);
783
+ return;
784
+ }
785
+ } catch (err) {
786
+ console.error("[CrudForm] Error in onBeforeSave:", err);
787
+ flash(t("ui.forms.flash.saveBlocked", "Save blocked by validation"), "error");
788
+ setPending(false);
789
+ return;
790
+ }
791
+ }
792
+ setPending(true);
793
+ if (resolvedInjectionSpotId) {
794
+ try {
795
+ await triggerInjectionEvent("onSave", parsedValues, injectionContext);
796
+ } catch (err) {
797
+ console.error("[CrudForm] Error in onSave:", err);
798
+ flash(t("ui.forms.flash.saveBlocked", "Save blocked by validation"), "error");
799
+ setPending(false);
800
+ return;
801
+ }
802
+ }
803
+ try {
804
+ await onSubmit?.(parsedValues);
805
+ if (resolvedInjectionSpotId) {
806
+ try {
807
+ await triggerInjectionEvent("onAfterSave", parsedValues, injectionContext);
808
+ } catch (err) {
809
+ console.error("[CrudForm] Error in onAfterSave:", err);
810
+ }
811
+ }
812
+ if (successRedirect) router.push(successRedirect);
813
+ } catch (err) {
814
+ const { message: helperMessage, fieldErrors: serverFieldErrors } = mapCrudServerErrorToFormErrors(err, { customEntity });
815
+ const combinedFieldErrors = serverFieldErrors ?? {};
816
+ const hasFieldErrors = Object.keys(combinedFieldErrors).length > 0;
817
+ const firstFieldMessage = hasFieldErrors ? (() => {
818
+ const firstKey = Object.keys(combinedFieldErrors)[0];
819
+ if (!firstKey) return null;
820
+ const value = combinedFieldErrors[firstKey];
821
+ return typeof value === "string" && value.trim().length ? value.trim() : null;
822
+ })() : null;
823
+ if (hasFieldErrors) {
824
+ setErrors(combinedFieldErrors);
825
+ if (process.env.NODE_ENV !== "production") {
826
+ console.debug("[crud-form] Submission failed with field errors", combinedFieldErrors);
827
+ }
828
+ }
829
+ let displayMessage = typeof helperMessage === "string" && helperMessage.trim() ? helperMessage.trim() : "";
830
+ if (hasFieldErrors) {
831
+ const lowered = displayMessage.toLowerCase();
832
+ const highlightedLower = highlightedMessage.toLowerCase();
833
+ if (!displayMessage || lowered === "invalid input" || lowered === highlightedLower) {
834
+ displayMessage = firstFieldMessage ?? highlightedMessage;
835
+ }
836
+ }
837
+ if (!displayMessage && err instanceof Error && typeof err.message === "string" && err.message.trim()) {
838
+ displayMessage = err.message.trim();
839
+ }
840
+ if (!displayMessage) {
841
+ displayMessage = hasFieldErrors ? highlightedMessage : saveErrorMessage;
842
+ }
843
+ displayMessage = parseServerMessage(displayMessage);
844
+ flash(displayMessage, "error");
845
+ setFormError(displayMessage);
846
+ } finally {
847
+ setPending(false);
848
+ }
849
+ };
850
+ React.useEffect(() => {
851
+ let cancelled = false;
852
+ const loadAll = async () => {
853
+ const loaders = allFields.filter(
854
+ (f) => f.type !== "custom" && typeof f.loadOptions === "function"
855
+ ).map(async (f) => {
856
+ try {
857
+ const opts = await f.loadOptions();
858
+ if (!cancelled) setDynamicOptions((prev) => ({ ...prev, [f.id]: opts }));
859
+ } catch {
860
+ }
861
+ });
862
+ await Promise.all(loaders);
863
+ };
864
+ loadAll();
865
+ return () => {
866
+ cancelled = true;
867
+ };
868
+ }, [allFields]);
869
+ const loadFieldOptions = React.useCallback(async (field, query) => {
870
+ if (!("type" in field) || field.type === "custom") return EMPTY_OPTIONS;
871
+ const builtin = field;
872
+ const loader = builtin.loadOptions;
873
+ if (typeof loader === "function") {
874
+ if (query === void 0 && Array.isArray(dynamicOptions[field.id])) return dynamicOptions[field.id];
875
+ try {
876
+ const fetched = await loader(query);
877
+ if (query === void 0) {
878
+ setDynamicOptions((prev) => ({
879
+ ...prev,
880
+ [field.id]: fetched
881
+ }));
882
+ }
883
+ return fetched;
884
+ } catch {
885
+ return builtin.options ?? EMPTY_OPTIONS;
886
+ }
887
+ }
888
+ return dynamicOptions[field.id] || builtin.options || EMPTY_OPTIONS;
889
+ }, [dynamicOptions]);
890
+ const fieldOptionsById = React.useMemo(() => {
891
+ const map = new globalThis.Map();
892
+ for (const f of allFields) {
893
+ if (!("type" in f) || f.type === "custom") continue;
894
+ const builtin = f;
895
+ const staticOptions = builtin.options ?? EMPTY_OPTIONS;
896
+ const dynamic2 = dynamicOptions[f.id];
897
+ if (dynamic2 && dynamic2.length) {
898
+ const merged = [];
899
+ const seen = /* @__PURE__ */ new Set();
900
+ for (const opt of staticOptions) {
901
+ if (seen.has(opt.value)) continue;
902
+ seen.add(opt.value);
903
+ merged.push(opt);
904
+ }
905
+ for (const opt of dynamic2) {
906
+ if (seen.has(opt.value)) continue;
907
+ seen.add(opt.value);
908
+ merged.push(opt);
909
+ }
910
+ map.set(f.id, merged);
911
+ } else if (staticOptions.length) {
912
+ map.set(f.id, staticOptions);
913
+ } else if (dynamic2) {
914
+ map.set(f.id, dynamic2);
915
+ }
916
+ }
917
+ return map;
918
+ }, [allFields, dynamicOptions]);
919
+ const usesResponsiveLayout = allFields.some(
920
+ (field) => field.layout === "half" || field.layout === "third"
921
+ );
922
+ const grid = twoColumn ? "grid grid-cols-1 lg:grid-cols-[7fr_3fr] gap-4" : usesResponsiveLayout ? "grid grid-cols-1 gap-4 md:grid-cols-6" : "grid grid-cols-1 gap-4";
923
+ const resolveLayoutClass = (layout) => {
924
+ switch (layout) {
925
+ case "half":
926
+ return "md:col-span-3";
927
+ case "third":
928
+ return "md:col-span-2";
929
+ default:
930
+ return "md:col-span-6";
931
+ }
932
+ };
933
+ const renderFields = (fieldList) => {
934
+ const usesResponsive = fieldList.some(
935
+ (field) => field.layout === "half" || field.layout === "third"
936
+ );
937
+ const gridClass = usesResponsive ? "grid grid-cols-1 gap-4 md:grid-cols-6" : "grid grid-cols-1 gap-4";
938
+ return /* @__PURE__ */ jsx("div", { className: gridClass, children: fieldList.map((f) => {
939
+ const layout = f.layout ?? "full";
940
+ const wrapperClassName = usesResponsive ? resolveLayoutClass(layout) : void 0;
941
+ return /* @__PURE__ */ jsx(
942
+ FieldControl,
943
+ {
944
+ field: f,
945
+ value: values[f.id],
946
+ error: errors[f.id],
947
+ options: fieldOptionsById.get(f.id) || EMPTY_OPTIONS,
948
+ setValue,
949
+ values,
950
+ loadFieldOptions,
951
+ autoFocus: Boolean(firstFieldId && f.id === firstFieldId),
952
+ onSubmitRequest: requestSubmit,
953
+ wrapperClassName,
954
+ entityIdForField: primaryEntityId ?? void 0,
955
+ recordId
956
+ },
957
+ f.id
958
+ );
959
+ }) });
960
+ };
961
+ const renderCustomFieldsContent = React.useCallback(() => {
962
+ if (!customFieldLayout.length) {
963
+ return [
964
+ /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-4", children: customFieldsEmptyState }, "custom-fields-empty")
965
+ ];
966
+ }
967
+ const nodes = [];
968
+ const multipleEntities = customFieldLayout.length > 1;
969
+ customFieldLayout.forEach((entityLayout) => {
970
+ const manageHref = buildCustomFieldsManageHref(entityLayout.entityId);
971
+ const showSelector = entityLayout.hasFieldsets && entityLayout.singleFieldsetPerRecord && entityLayout.availableFieldsets.length > 0;
972
+ if (multipleEntities) {
973
+ nodes.push(
974
+ /* @__PURE__ */ jsx(
975
+ "div",
976
+ {
977
+ className: "px-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground",
978
+ children: entityLayout.entityId
979
+ },
980
+ `custom-fields-entity-${entityLayout.entityId}`
981
+ )
982
+ );
983
+ }
984
+ if (showSelector) {
985
+ nodes.push(
986
+ /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-4", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2 text-sm", children: [
987
+ /* @__PURE__ */ jsx("label", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: fieldsetSelectorLabel }),
988
+ /* @__PURE__ */ jsxs(
989
+ "select",
990
+ {
991
+ className: "h-9 rounded border px-2 text-sm",
992
+ value: entityLayout.activeFieldset ?? "",
993
+ onChange: (event) => handleFieldsetSelectionChange(
994
+ entityLayout.entityId,
995
+ event.target.value || null
996
+ ),
997
+ children: [
998
+ /* @__PURE__ */ jsx("option", { value: "", children: defaultFieldsetLabel }),
999
+ entityLayout.availableFieldsets.map((fs) => /* @__PURE__ */ jsx("option", { value: fs.code, children: fs.label }, fs.code))
1000
+ ]
1001
+ }
1002
+ ),
1003
+ /* @__PURE__ */ jsxs(
1004
+ "button",
1005
+ {
1006
+ type: "button",
1007
+ className: "inline-flex h-8 w-8 items-center justify-center rounded border text-muted-foreground hover:text-foreground",
1008
+ onClick: () => handleOpenFieldsetEditor(entityLayout.entityId, entityLayout.activeFieldset ?? null, "fieldset"),
1009
+ disabled: !manageHref,
1010
+ title: manageFieldsetLabel,
1011
+ children: [
1012
+ /* @__PURE__ */ jsx(Settings, { className: "size-4" }),
1013
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: manageFieldsetLabel })
1014
+ ]
1015
+ }
1016
+ )
1017
+ ] }) }, `custom-fields-selector-${entityLayout.entityId}`)
1018
+ );
1019
+ }
1020
+ if (entityLayout.sections.length) {
1021
+ entityLayout.sections.forEach((section) => {
1022
+ const FieldsetIcon = section.fieldset?.icon ? FIELDSET_ICON_COMPONENTS[section.fieldset.icon] : null;
1023
+ const sectionKey = `${entityLayout.entityId}:${section.fieldsetCode ?? "default"}`;
1024
+ const manageDisabled = !manageHref;
1025
+ nodes.push(
1026
+ /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card p-4 space-y-4", children: [
1027
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
1028
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
1029
+ FieldsetIcon ? /* @__PURE__ */ jsx(FieldsetIcon, { className: "size-5 text-muted-foreground" }) : null,
1030
+ /* @__PURE__ */ jsxs("div", { children: [
1031
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: section.title }),
1032
+ section.description ? /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: section.description }) : null
1033
+ ] })
1034
+ ] }),
1035
+ /* @__PURE__ */ jsxs(
1036
+ "button",
1037
+ {
1038
+ type: "button",
1039
+ className: "inline-flex items-center gap-1 rounded border px-2 py-1 text-xs text-muted-foreground hover:text-foreground disabled:opacity-50",
1040
+ onClick: () => handleOpenFieldsetEditor(entityLayout.entityId, section.fieldsetCode, "fieldset"),
1041
+ disabled: manageDisabled,
1042
+ children: [
1043
+ /* @__PURE__ */ jsx(Settings, { className: "size-4" }),
1044
+ manageFieldsetLabel
1045
+ ]
1046
+ }
1047
+ )
1048
+ ] }),
1049
+ section.groups.map((group) => {
1050
+ const groupKey = `${section.fieldsetCode ?? "default"}:${group.code ?? "default"}`;
1051
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1052
+ group.label ? /* @__PURE__ */ jsxs("div", { children: [
1053
+ /* @__PURE__ */ jsx("div", { className: "text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: group.label }),
1054
+ group.hint ? /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: group.hint }) : null
1055
+ ] }) : null,
1056
+ renderFields(group.fields)
1057
+ ] }, groupKey);
1058
+ }),
1059
+ !section.groups.length ? /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: emptyFieldsetMessage }) : null
1060
+ ] }, sectionKey)
1061
+ );
1062
+ });
1063
+ } else {
1064
+ nodes.push(
1065
+ /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-4", children: customFieldsEmptyState }, `custom-fields-empty-${entityLayout.entityId}`)
1066
+ );
1067
+ }
1068
+ });
1069
+ return nodes;
1070
+ }, [
1071
+ buildCustomFieldsManageHref,
1072
+ customFieldLayout,
1073
+ customFieldsEmptyState,
1074
+ defaultFieldsetLabel,
1075
+ emptyFieldsetMessage,
1076
+ fieldsetSelectorLabel,
1077
+ handleFieldsetSelectionChange,
1078
+ handleOpenFieldsetEditor,
1079
+ manageFieldsetLabel,
1080
+ renderFields
1081
+ ]);
1082
+ const fieldsetManagerDialog = /* @__PURE__ */ jsx(Dialog, { open: fieldsetEditorTarget !== null, onOpenChange: (open) => {
1083
+ if (!open) setFieldsetEditorTarget(null);
1084
+ }, children: /* @__PURE__ */ jsxs(
1085
+ DialogContent,
1086
+ {
1087
+ className: "max-w-5xl w-full",
1088
+ onKeyDown: (event) => {
1089
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
1090
+ event.preventDefault();
1091
+ handleFieldsetDialogSave();
1092
+ }
1093
+ },
1094
+ children: [
1095
+ /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: fieldsetDialogTitle }) }),
1096
+ fieldsetEditorTarget ? /* @__PURE__ */ jsx(
1097
+ FieldDefinitionsManager,
1098
+ {
1099
+ ref: fieldsetManagerRef,
1100
+ entityId: fieldsetEditorTarget.entityId,
1101
+ initialFieldset: fieldsetEditorTarget.fieldsetCode,
1102
+ fullEditorHref: fieldsetEditorFullHref ?? void 0,
1103
+ onSaved: refreshCustomFieldDefinitions,
1104
+ onClose: () => setFieldsetEditorTarget(null)
1105
+ }
1106
+ ) : /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center text-sm text-muted-foreground px-4 text-center", children: fieldsetDialogUnavailable })
1107
+ ]
1108
+ }
1109
+ ) });
1110
+ if (useGroupedLayout) {
1111
+ const col1 = [];
1112
+ const col2 = [];
1113
+ for (const g of resolvedGroupsForLayout) {
1114
+ if ((g.column ?? 1) === 2) col2.push(g);
1115
+ else col1.push(g);
1116
+ }
1117
+ const renderGroupedCards = (items) => {
1118
+ const nodes = [];
1119
+ for (const g of items) {
1120
+ const isCustomFieldsGroup = g.kind === "customFields";
1121
+ if (isCustomFieldsGroup) {
1122
+ if (isLoadingCustomFields) {
1123
+ nodes.push(
1124
+ /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card p-4", children: /* @__PURE__ */ jsx(
1125
+ DataLoader,
1126
+ {
1127
+ isLoading: true,
1128
+ loadingMessage: resolvedCustomFieldsLoadingMessage,
1129
+ spinnerSize: "md",
1130
+ className: "min-h-[1px]",
1131
+ children: /* @__PURE__ */ jsx("div", {})
1132
+ }
1133
+ ) }, `${g.id}-loading`)
1134
+ );
1135
+ continue;
1136
+ }
1137
+ if (g.component) {
1138
+ nodes.push(
1139
+ /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-card px-4 py-3", children: g.component({ values, setValue, errors }) }, `${g.id}-component`)
1140
+ );
1141
+ }
1142
+ const renderedSections = renderCustomFieldsContent();
1143
+ if (renderedSections.length) nodes.push(...renderedSections);
1144
+ continue;
1145
+ }
1146
+ const componentNode = g.component ? g.component({ values, setValue, errors }) : null;
1147
+ if (g.bare) {
1148
+ if (componentNode) {
1149
+ nodes.push(/* @__PURE__ */ jsx(React.Fragment, { children: componentNode }, g.id));
1150
+ }
1151
+ continue;
1152
+ }
1153
+ const groupFields = resolveGroupFields(g);
1154
+ nodes.push(
1155
+ /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card px-4 py-3 space-y-3", children: [
1156
+ g.title ? /* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: t(g.title, g.title) }) : null,
1157
+ g.description ? /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: t(g.description, g.description) }) : null,
1158
+ componentNode ? /* @__PURE__ */ jsx("div", { children: componentNode }) : null,
1159
+ /* @__PURE__ */ jsx(
1160
+ DataLoader,
1161
+ {
1162
+ isLoading: false,
1163
+ loadingMessage: resolvedLoadingMessage,
1164
+ spinnerSize: "md",
1165
+ className: "min-h-[1px]",
1166
+ children: groupFields.length > 0 ? renderFields(groupFields) : /* @__PURE__ */ jsx("div", { className: "min-h-[1px]" })
1167
+ }
1168
+ )
1169
+ ] }, g.id)
1170
+ );
1171
+ }
1172
+ return nodes;
1173
+ };
1174
+ const col1Content = renderGroupedCards(col1);
1175
+ const col2Content = renderGroupedCards(col2);
1176
+ const hasSecondaryColumn = col2Content.length > 0;
1177
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", ref: rootRef, children: [
1178
+ !embedded ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
1179
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
1180
+ backHref ? /* @__PURE__ */ jsxs(Link, { href: backHref, className: "text-sm text-muted-foreground hover:text-foreground", children: [
1181
+ "\u2190 ",
1182
+ backLabel
1183
+ ] }) : null,
1184
+ title ? /* @__PURE__ */ jsx("div", { className: "text-base font-medium", children: title }) : null
1185
+ ] }),
1186
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1187
+ extraActions,
1188
+ showDelete ? /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", onClick: handleDelete, className: "text-red-600 border-red-200 hover:bg-red-50 rounded", children: [
1189
+ /* @__PURE__ */ jsx(Trash2, { className: "size-4 mr-2" }),
1190
+ deleteLabel
1191
+ ] }) : null,
1192
+ cancelHref ? /* @__PURE__ */ jsx(Link, { href: cancelHref, className: "h-9 inline-flex items-center rounded border px-3 text-sm", children: cancelLabel }) : null,
1193
+ /* @__PURE__ */ jsxs(Button, { type: "submit", form: formId, disabled: pending, children: [
1194
+ /* @__PURE__ */ jsx(Save, { className: "size-4 mr-2" }),
1195
+ pending ? savingLabel : resolvedSubmitLabel
1196
+ ] })
1197
+ ] })
1198
+ ] }) : null,
1199
+ contentHeader,
1200
+ /* @__PURE__ */ jsx(
1201
+ DataLoader,
1202
+ {
1203
+ isLoading,
1204
+ loadingMessage: resolvedLoadingMessage,
1205
+ spinnerSize: "md",
1206
+ className: "min-h-[400px]",
1207
+ children: /* @__PURE__ */ jsxs("form", { id: formId, onSubmit: handleSubmit, className: `space-y-4 ${dialogFormPadding}`, children: [
1208
+ resolvedInjectionSpotId ? /* @__PURE__ */ jsx(
1209
+ InjectionSpot,
1210
+ {
1211
+ spotId: resolvedInjectionSpotId,
1212
+ context: injectionContext,
1213
+ data: values,
1214
+ onDataChange: (newData) => setValues(newData),
1215
+ disabled: pending,
1216
+ widgetsOverride: stackedInjectionWidgets
1217
+ }
1218
+ ) : null,
1219
+ /* @__PURE__ */ jsxs(
1220
+ "div",
1221
+ {
1222
+ className: hasSecondaryColumn ? "grid grid-cols-1 lg:grid-cols-[7fr_3fr] gap-4" : "grid grid-cols-1 gap-4",
1223
+ children: [
1224
+ /* @__PURE__ */ jsx("div", { className: "space-y-3", children: col1Content }),
1225
+ hasSecondaryColumn ? /* @__PURE__ */ jsx("div", { className: "space-y-3", children: col2Content }) : null
1226
+ ]
1227
+ }
1228
+ ),
1229
+ formError ? /* @__PURE__ */ jsx("div", { className: "text-sm text-red-600", children: formError }) : null,
1230
+ /* @__PURE__ */ jsxs("div", { className: `flex items-center ${embedded ? "justify-end" : "justify-between"} gap-2 ${dialogFooterClass}`, children: [
1231
+ embedded ? null : /* @__PURE__ */ jsx("div", {}),
1232
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1233
+ extraActions,
1234
+ !embedded && showDelete ? /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", onClick: handleDelete, className: "text-red-600 border-red-200 hover:bg-red-50 rounded", children: [
1235
+ /* @__PURE__ */ jsx(Trash2, { className: "size-4 mr-2" }),
1236
+ deleteLabel
1237
+ ] }) : null,
1238
+ !embedded && cancelHref ? /* @__PURE__ */ jsx(Link, { href: cancelHref, className: "h-9 inline-flex items-center rounded border px-3 text-sm", children: cancelLabel }) : null,
1239
+ /* @__PURE__ */ jsxs(Button, { type: "submit", disabled: pending, children: [
1240
+ /* @__PURE__ */ jsx(Save, { className: "size-4 mr-2" }),
1241
+ pending ? savingLabel : resolvedSubmitLabel
1242
+ ] })
1243
+ ] })
1244
+ ] })
1245
+ ] })
1246
+ }
1247
+ ),
1248
+ fieldsetManagerDialog
1249
+ ] });
1250
+ }
1251
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", ref: rootRef, children: [
1252
+ !embedded ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
1253
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
1254
+ backHref ? /* @__PURE__ */ jsxs(Link, { href: backHref, className: "text-sm text-muted-foreground hover:text-foreground", children: [
1255
+ "\u2190 ",
1256
+ backLabel
1257
+ ] }) : null,
1258
+ title ? /* @__PURE__ */ jsx("div", { className: "text-base font-medium", children: title }) : null
1259
+ ] }),
1260
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1261
+ extraActions,
1262
+ showDelete ? /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", onClick: handleDelete, className: "text-red-600 border-red-200 hover:bg-red-50 rounded", children: [
1263
+ /* @__PURE__ */ jsx(Trash2, { className: "size-4 mr-2" }),
1264
+ deleteLabel
1265
+ ] }) : null,
1266
+ cancelHref ? /* @__PURE__ */ jsx(Link, { href: cancelHref, className: "h-9 inline-flex items-center rounded border px-3 text-sm", children: cancelLabel }) : null,
1267
+ /* @__PURE__ */ jsxs(Button, { type: "submit", form: formId, disabled: pending, children: [
1268
+ /* @__PURE__ */ jsx(Save, { className: "size-4 mr-2" }),
1269
+ pending ? savingLabel : resolvedSubmitLabel
1270
+ ] })
1271
+ ] })
1272
+ ] }) : null,
1273
+ contentHeader,
1274
+ /* @__PURE__ */ jsx(
1275
+ DataLoader,
1276
+ {
1277
+ isLoading,
1278
+ loadingMessage: resolvedLoadingMessage,
1279
+ spinnerSize: "md",
1280
+ className: "min-h-[400px]",
1281
+ children: /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs(
1282
+ "form",
1283
+ {
1284
+ id: formId,
1285
+ onSubmit: handleSubmit,
1286
+ className: `${embedded ? "space-y-4" : "rounded-lg border bg-card p-4 space-y-4"} ${dialogFormPadding}`,
1287
+ children: [
1288
+ resolvedInjectionSpotId ? /* @__PURE__ */ jsx(
1289
+ InjectionSpot,
1290
+ {
1291
+ spotId: resolvedInjectionSpotId,
1292
+ context: injectionContext,
1293
+ data: values,
1294
+ onDataChange: (newData) => setValues(newData),
1295
+ disabled: pending,
1296
+ widgetsOverride: stackedInjectionWidgets
1297
+ }
1298
+ ) : null,
1299
+ /* @__PURE__ */ jsx("div", { className: grid, children: allFields.map((f) => {
1300
+ const layout = f.layout ?? "full";
1301
+ const wrapperClassName = usesResponsiveLayout ? resolveLayoutClass(layout) : void 0;
1302
+ return /* @__PURE__ */ jsx(
1303
+ FieldControl,
1304
+ {
1305
+ field: f,
1306
+ value: values[f.id],
1307
+ error: errors[f.id],
1308
+ options: fieldOptionsById.get(f.id) || EMPTY_OPTIONS,
1309
+ setValue,
1310
+ values,
1311
+ loadFieldOptions,
1312
+ autoFocus: Boolean(firstFieldId && f.id === firstFieldId),
1313
+ onSubmitRequest: requestSubmit,
1314
+ wrapperClassName,
1315
+ entityIdForField: primaryEntityId ?? void 0,
1316
+ recordId
1317
+ },
1318
+ f.id
1319
+ );
1320
+ }) }),
1321
+ formError ? /* @__PURE__ */ jsx("div", { className: "text-sm text-red-600", children: formError }) : null,
1322
+ /* @__PURE__ */ jsxs("div", { className: `flex items-center ${embedded ? "justify-end" : "justify-end"} gap-2 ${dialogFooterClass}`, children: [
1323
+ extraActions,
1324
+ !embedded && showDelete ? /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", onClick: handleDelete, className: "text-red-600 border-red-200 hover:bg-red-50", children: [
1325
+ /* @__PURE__ */ jsx(Trash2, { className: "size-4 mr-2" }),
1326
+ deleteLabel
1327
+ ] }) : null,
1328
+ !embedded && cancelHref ? /* @__PURE__ */ jsx(Link, { href: cancelHref, className: "h-9 inline-flex items-center rounded border px-3 text-sm", children: cancelLabel }) : null,
1329
+ /* @__PURE__ */ jsxs(Button, { type: "submit", disabled: pending, children: [
1330
+ /* @__PURE__ */ jsx(Save, { className: "size-4 mr-2" }),
1331
+ pending ? savingLabel : resolvedSubmitLabel
1332
+ ] })
1333
+ ] })
1334
+ ]
1335
+ }
1336
+ ) })
1337
+ }
1338
+ ),
1339
+ fieldsetManagerDialog
1340
+ ] });
1341
+ }
1342
+ function RelationSelect({
1343
+ value,
1344
+ onChange,
1345
+ options,
1346
+ placeholder,
1347
+ autoFocus
1348
+ }) {
1349
+ const t = useT();
1350
+ const [query, setQuery] = React.useState("");
1351
+ const inputRef = React.useRef(null);
1352
+ const filtered = React.useMemo(() => {
1353
+ const q = query.toLowerCase().trim();
1354
+ if (!q) return options;
1355
+ return options.filter((o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q));
1356
+ }, [query, options]);
1357
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
1358
+ /* @__PURE__ */ jsx(
1359
+ "input",
1360
+ {
1361
+ ref: inputRef,
1362
+ className: "w-full h-9 rounded border px-2 text-sm",
1363
+ placeholder: placeholder || t("ui.forms.listbox.searchPlaceholder", "Search..."),
1364
+ value: query,
1365
+ onChange: (e) => setQuery(e.target.value),
1366
+ autoFocus,
1367
+ "data-crud-focus-target": ""
1368
+ }
1369
+ ),
1370
+ /* @__PURE__ */ jsxs("div", { className: "max-h-40 overflow-auto rounded border", children: [
1371
+ /* @__PURE__ */ jsx(
1372
+ "button",
1373
+ {
1374
+ type: "button",
1375
+ className: "block w-full text-left px-2 py-1 text-sm hover:bg-muted",
1376
+ onClick: () => onChange(""),
1377
+ children: "\u2014"
1378
+ }
1379
+ ),
1380
+ filtered.map((opt) => /* @__PURE__ */ jsx(
1381
+ "button",
1382
+ {
1383
+ type: "button",
1384
+ className: `block w-full text-left px-2 py-1 text-sm hover:bg-muted ${value === opt.value ? "bg-muted" : ""}`,
1385
+ onClick: () => onChange(opt.value),
1386
+ children: opt.label
1387
+ },
1388
+ opt.value
1389
+ ))
1390
+ ] })
1391
+ ] });
1392
+ }
1393
+ function TextInput({
1394
+ value,
1395
+ onChange,
1396
+ placeholder,
1397
+ autoFocus,
1398
+ onSubmit,
1399
+ disabled,
1400
+ suggestions
1401
+ }) {
1402
+ const [local, setLocal] = React.useState(value);
1403
+ const isFocusedRef = React.useRef(false);
1404
+ const userTypingRef = React.useRef(false);
1405
+ const datalistId = React.useId();
1406
+ React.useEffect(() => {
1407
+ if (!isFocusedRef.current || !userTypingRef.current) {
1408
+ setLocal(value);
1409
+ }
1410
+ }, [value]);
1411
+ const handleChange = React.useCallback((e) => {
1412
+ if (disabled) return;
1413
+ const next = e.target.value;
1414
+ userTypingRef.current = true;
1415
+ setLocal(next);
1416
+ onChange(next);
1417
+ }, [disabled, onChange]);
1418
+ const handleKeyDown = React.useCallback((e) => {
1419
+ if (disabled) return;
1420
+ if (e.key === "Enter" && !e.shiftKey) {
1421
+ e.preventDefault();
1422
+ onChange(local);
1423
+ onSubmit?.();
1424
+ }
1425
+ }, [disabled, local, onChange, onSubmit]);
1426
+ const handleFocus = React.useCallback(() => {
1427
+ isFocusedRef.current = true;
1428
+ }, []);
1429
+ const handleBlur = React.useCallback(() => {
1430
+ isFocusedRef.current = false;
1431
+ userTypingRef.current = false;
1432
+ onChange(local);
1433
+ }, [local, onChange]);
1434
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1435
+ /* @__PURE__ */ jsx(
1436
+ "input",
1437
+ {
1438
+ type: "text",
1439
+ className: "w-full h-9 rounded border px-2 text-sm",
1440
+ placeholder,
1441
+ value: local,
1442
+ onChange: handleChange,
1443
+ onKeyDown: handleKeyDown,
1444
+ onFocus: handleFocus,
1445
+ onBlur: handleBlur,
1446
+ spellCheck: false,
1447
+ autoFocus,
1448
+ "data-crud-focus-target": "",
1449
+ disabled,
1450
+ list: suggestions && suggestions.length > 0 ? datalistId : void 0
1451
+ }
1452
+ ),
1453
+ suggestions && suggestions.length > 0 && /* @__PURE__ */ jsx("datalist", { id: datalistId, children: suggestions.map((suggestion) => /* @__PURE__ */ jsx("option", { value: suggestion }, suggestion)) })
1454
+ ] });
1455
+ }
1456
+ function NumberInput({
1457
+ value,
1458
+ onChange,
1459
+ placeholder,
1460
+ autoFocus,
1461
+ onSubmit
1462
+ }) {
1463
+ const [local, setLocal] = React.useState(value !== void 0 && value !== null ? String(value) : "");
1464
+ const isFocusedRef = React.useRef(false);
1465
+ React.useEffect(() => {
1466
+ if (!isFocusedRef.current) {
1467
+ setLocal(value !== void 0 && value !== null ? String(value) : "");
1468
+ }
1469
+ }, [value]);
1470
+ const handleChange = React.useCallback((e) => {
1471
+ const next = e.target.value;
1472
+ setLocal(next);
1473
+ const numValue = next === "" ? void 0 : Number(next);
1474
+ onChange(numValue);
1475
+ }, [onChange]);
1476
+ const handleKeyDown = React.useCallback((e) => {
1477
+ if (e.key === "Enter" && !e.shiftKey) {
1478
+ e.preventDefault();
1479
+ const numValue = local === "" ? void 0 : Number(local);
1480
+ onChange(numValue);
1481
+ onSubmit?.();
1482
+ }
1483
+ }, [local, onChange, onSubmit]);
1484
+ const handleFocus = React.useCallback(() => {
1485
+ isFocusedRef.current = true;
1486
+ }, []);
1487
+ const handleBlur = React.useCallback(() => {
1488
+ isFocusedRef.current = false;
1489
+ const numValue = local === "" ? void 0 : Number(local);
1490
+ onChange(numValue);
1491
+ }, [local, onChange]);
1492
+ return /* @__PURE__ */ jsx(
1493
+ "input",
1494
+ {
1495
+ type: "number",
1496
+ className: "w-full h-9 rounded border px-2 text-sm",
1497
+ placeholder,
1498
+ value: local,
1499
+ onChange: handleChange,
1500
+ onKeyDown: handleKeyDown,
1501
+ onFocus: handleFocus,
1502
+ onBlur: handleBlur,
1503
+ autoFocus,
1504
+ "data-crud-focus-target": ""
1505
+ }
1506
+ );
1507
+ }
1508
+ function TextAreaInput({
1509
+ value,
1510
+ onChange,
1511
+ placeholder,
1512
+ autoFocus
1513
+ }) {
1514
+ const [local, setLocal] = React.useState(value);
1515
+ const isFocusedRef = React.useRef(false);
1516
+ React.useEffect(() => {
1517
+ if (!isFocusedRef.current) setLocal(value);
1518
+ }, [value]);
1519
+ const handleChange = React.useCallback((e) => {
1520
+ const next = e.target.value;
1521
+ setLocal(next);
1522
+ onChange(next);
1523
+ }, [onChange]);
1524
+ const handleFocus = React.useCallback(() => {
1525
+ isFocusedRef.current = true;
1526
+ }, []);
1527
+ const handleBlur = React.useCallback(() => {
1528
+ isFocusedRef.current = false;
1529
+ onChange(local);
1530
+ }, [local, onChange]);
1531
+ return /* @__PURE__ */ jsx(
1532
+ "textarea",
1533
+ {
1534
+ className: "w-full rounded border px-2 py-2 min-h-[120px] text-sm",
1535
+ placeholder,
1536
+ value: local,
1537
+ onChange: handleChange,
1538
+ onFocus: handleFocus,
1539
+ onBlur: handleBlur,
1540
+ autoFocus,
1541
+ "data-crud-focus-target": ""
1542
+ }
1543
+ );
1544
+ }
1545
+ const MDEditor = dynamic(async () => {
1546
+ const mod = await import("@uiw/react-md-editor");
1547
+ return mod.default;
1548
+ }, { ssr: false });
1549
+ const MarkdownEditor = React.memo(function MarkdownEditor2({ value = "", onChange }) {
1550
+ const containerRef = React.useRef(null);
1551
+ const [local, setLocal] = React.useState(value);
1552
+ const typingRef = React.useRef(false);
1553
+ React.useEffect(() => {
1554
+ if (!typingRef.current) setLocal(value);
1555
+ }, [value]);
1556
+ const handleChange = React.useCallback((v) => {
1557
+ typingRef.current = true;
1558
+ setLocal(v ?? "");
1559
+ }, []);
1560
+ const commit = React.useCallback(() => {
1561
+ if (!typingRef.current) return;
1562
+ typingRef.current = false;
1563
+ onChange(local);
1564
+ requestAnimationFrame(() => {
1565
+ const ta = containerRef.current?.querySelector("textarea");
1566
+ ta?.focus();
1567
+ });
1568
+ }, [local, onChange]);
1569
+ return /* @__PURE__ */ jsx("div", { ref: containerRef, "data-color-mode": "light", className: "w-full", onBlur: () => commit(), children: /* @__PURE__ */ jsx(
1570
+ MDEditor,
1571
+ {
1572
+ value: local,
1573
+ height: 220,
1574
+ onChange: handleChange,
1575
+ previewOptions: { remarkPlugins: [remarkGfm] }
1576
+ }
1577
+ ) });
1578
+ }, (prev, next) => prev.value === next.value);
1579
+ const HtmlRichTextEditor = React.memo(function HtmlRichTextEditor2({ value = "", onChange }) {
1580
+ const t = useT();
1581
+ const boldLabel = t("ui.forms.richtext.bold");
1582
+ const italicLabel = t("ui.forms.richtext.italic");
1583
+ const underlineLabel = t("ui.forms.richtext.underline");
1584
+ const listLabel = t("ui.forms.richtext.list");
1585
+ const heading3Label = t("ui.forms.richtext.heading3");
1586
+ const linkLabel = t("ui.forms.richtext.link");
1587
+ const linkUrlPrompt = t("ui.forms.richtext.linkUrlPrompt");
1588
+ const ref = React.useRef(null);
1589
+ const applyingExternal = React.useRef(false);
1590
+ const typingRef = React.useRef(false);
1591
+ React.useEffect(() => {
1592
+ const el = ref.current;
1593
+ if (!el) return;
1594
+ const current = el.innerHTML;
1595
+ if (!typingRef.current && current !== value) {
1596
+ applyingExternal.current = true;
1597
+ el.innerHTML = value || "";
1598
+ requestAnimationFrame(() => {
1599
+ applyingExternal.current = false;
1600
+ });
1601
+ }
1602
+ }, [value]);
1603
+ const exec = (cmd, arg) => {
1604
+ const el = ref.current;
1605
+ if (!el) return;
1606
+ el.focus();
1607
+ try {
1608
+ document.execCommand(cmd, false, arg);
1609
+ } catch {
1610
+ }
1611
+ };
1612
+ const onKeyDown = (e) => {
1613
+ const isMod = e.metaKey || e.ctrlKey;
1614
+ if (!isMod) return;
1615
+ const k = e.key.toLowerCase();
1616
+ if (k === "b") {
1617
+ e.preventDefault();
1618
+ exec("bold");
1619
+ }
1620
+ if (k === "i") {
1621
+ e.preventDefault();
1622
+ exec("italic");
1623
+ }
1624
+ if (k === "u") {
1625
+ e.preventDefault();
1626
+ exec("underline");
1627
+ }
1628
+ };
1629
+ return /* @__PURE__ */ jsxs("div", { className: "w-full rounded border", children: [
1630
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 px-2 py-1 border-b", children: [
1631
+ /* @__PURE__ */ jsx("button", { type: "button", className: "px-2 py-0.5 text-xs rounded hover:bg-muted", onMouseDown: (e) => e.preventDefault(), onClick: () => exec("bold"), children: boldLabel }),
1632
+ /* @__PURE__ */ jsx("button", { type: "button", className: "px-2 py-0.5 text-xs rounded hover:bg-muted", onMouseDown: (e) => e.preventDefault(), onClick: () => exec("italic"), children: italicLabel }),
1633
+ /* @__PURE__ */ jsx("button", { type: "button", className: "px-2 py-0.5 text-xs rounded hover:bg-muted", onMouseDown: (e) => e.preventDefault(), onClick: () => exec("underline"), children: underlineLabel }),
1634
+ /* @__PURE__ */ jsx("span", { className: "mx-2 text-muted-foreground", children: "|" }),
1635
+ /* @__PURE__ */ jsxs("button", { type: "button", className: "px-2 py-0.5 text-xs rounded hover:bg-muted", onMouseDown: (e) => e.preventDefault(), onClick: () => exec("insertUnorderedList"), children: [
1636
+ "\u2022 ",
1637
+ listLabel
1638
+ ] }),
1639
+ /* @__PURE__ */ jsx("button", { type: "button", className: "px-2 py-0.5 text-xs rounded hover:bg-muted", onMouseDown: (e) => e.preventDefault(), onClick: () => exec("formatBlock", "<h3>"), children: heading3Label }),
1640
+ /* @__PURE__ */ jsx(
1641
+ "button",
1642
+ {
1643
+ type: "button",
1644
+ className: "px-2 py-0.5 text-xs rounded hover:bg-muted",
1645
+ onMouseDown: (e) => e.preventDefault(),
1646
+ onClick: () => {
1647
+ const url = window.prompt(linkUrlPrompt)?.trim();
1648
+ if (url) exec("createLink", url);
1649
+ },
1650
+ children: linkLabel
1651
+ }
1652
+ )
1653
+ ] }),
1654
+ /* @__PURE__ */ jsx(
1655
+ "div",
1656
+ {
1657
+ ref,
1658
+ className: "w-full px-2 py-2 min-h-[160px] focus:outline-none prose prose-sm max-w-none",
1659
+ contentEditable: true,
1660
+ suppressContentEditableWarning: true,
1661
+ onKeyDown,
1662
+ onInput: () => {
1663
+ if (!applyingExternal.current) typingRef.current = true;
1664
+ },
1665
+ onBlur: () => {
1666
+ const el = ref.current;
1667
+ if (!el) return;
1668
+ typingRef.current = false;
1669
+ onChange(el.innerHTML);
1670
+ }
1671
+ }
1672
+ )
1673
+ ] });
1674
+ }, (prev, next) => prev.value === next.value);
1675
+ const SimpleMarkdownEditor = React.memo(function SimpleMarkdownEditor2({ value = "", onChange }) {
1676
+ const t = useT();
1677
+ const boldLabel = t("ui.forms.richtext.bold");
1678
+ const italicLabel = t("ui.forms.richtext.italic");
1679
+ const underlineLabel = t("ui.forms.richtext.underline");
1680
+ const markdownPlaceholder = t("ui.forms.richtext.placeholder");
1681
+ const sampleText = t("ui.forms.richtext.sampleText");
1682
+ const taRef = React.useRef(null);
1683
+ const [local, setLocal] = React.useState(value);
1684
+ const typingRef = React.useRef(false);
1685
+ React.useEffect(() => {
1686
+ if (!typingRef.current) setLocal(value);
1687
+ }, [value]);
1688
+ const wrap = (before, after = before) => {
1689
+ const el = taRef.current;
1690
+ if (!el) return;
1691
+ const start = el.selectionStart ?? 0;
1692
+ const end = el.selectionEnd ?? 0;
1693
+ const sel = value.slice(start, end) || sampleText;
1694
+ const next = value.slice(0, start) + before + sel + after + value.slice(end);
1695
+ onChange(next);
1696
+ queueMicrotask(() => {
1697
+ const caret = start + before.length + sel.length + after.length;
1698
+ el.focus();
1699
+ el.setSelectionRange(caret, caret);
1700
+ });
1701
+ };
1702
+ const onKeyDown = (e) => {
1703
+ const isMod = e.metaKey || e.ctrlKey;
1704
+ if (!isMod) return;
1705
+ const key = e.key.toLowerCase();
1706
+ if (key === "b") {
1707
+ e.preventDefault();
1708
+ wrap("**");
1709
+ }
1710
+ if (key === "i") {
1711
+ e.preventDefault();
1712
+ wrap("_");
1713
+ }
1714
+ if (key === "u") {
1715
+ e.preventDefault();
1716
+ wrap("__");
1717
+ }
1718
+ };
1719
+ return /* @__PURE__ */ jsxs("div", { className: "w-full rounded border", children: [
1720
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 px-2 py-1 border-b", children: [
1721
+ /* @__PURE__ */ jsx("button", { type: "button", className: "px-2 py-0.5 text-xs rounded hover:bg-muted", onMouseDown: (e) => e.preventDefault(), onClick: () => wrap("**"), children: boldLabel }),
1722
+ /* @__PURE__ */ jsx("button", { type: "button", className: "px-2 py-0.5 text-xs rounded hover:bg-muted", onMouseDown: (e) => e.preventDefault(), onClick: () => wrap("_"), children: italicLabel }),
1723
+ /* @__PURE__ */ jsx("button", { type: "button", className: "px-2 py-0.5 text-xs rounded hover:bg-muted", onMouseDown: (e) => e.preventDefault(), onClick: () => wrap("__"), children: underlineLabel })
1724
+ ] }),
1725
+ /* @__PURE__ */ jsx(
1726
+ "textarea",
1727
+ {
1728
+ ref: taRef,
1729
+ className: "w-full min-h-[160px] resize-y px-2 py-2 font-mono text-sm outline-none",
1730
+ spellCheck: false,
1731
+ value: local,
1732
+ onChange: (e) => {
1733
+ typingRef.current = true;
1734
+ setLocal(e.target.value);
1735
+ },
1736
+ onBlur: () => {
1737
+ if (typingRef.current) {
1738
+ typingRef.current = false;
1739
+ onChange(local);
1740
+ }
1741
+ },
1742
+ onKeyDown,
1743
+ placeholder: markdownPlaceholder
1744
+ }
1745
+ )
1746
+ ] });
1747
+ }, (prev, next) => prev.value === next.value);
1748
+ const ListboxMultiSelect = React.memo(function ListboxMultiSelect2({
1749
+ options,
1750
+ placeholder,
1751
+ value,
1752
+ onChange,
1753
+ autoFocus
1754
+ }) {
1755
+ const t = useT();
1756
+ const searchPlaceholder = placeholder || t("ui.forms.listbox.searchPlaceholder");
1757
+ const noMatchesLabel = t("ui.forms.listbox.noMatches");
1758
+ const [query, setQuery] = React.useState("");
1759
+ const filtered = React.useMemo(() => {
1760
+ const q = query.toLowerCase().trim();
1761
+ if (!q) return options;
1762
+ return options.filter((o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q));
1763
+ }, [options, query]);
1764
+ const toggle = React.useCallback(
1765
+ (val) => {
1766
+ const set = new Set(value);
1767
+ if (set.has(val)) set.delete(val);
1768
+ else set.add(val);
1769
+ onChange(Array.from(set));
1770
+ },
1771
+ [value, onChange]
1772
+ );
1773
+ return /* @__PURE__ */ jsxs("div", { className: "w-full", children: [
1774
+ /* @__PURE__ */ jsx(
1775
+ "input",
1776
+ {
1777
+ className: "mb-2 w-full h-8 rounded border px-2 text-sm",
1778
+ placeholder: searchPlaceholder,
1779
+ value: query,
1780
+ onChange: (e) => setQuery(e.target.value),
1781
+ autoFocus,
1782
+ "data-crud-focus-target": ""
1783
+ }
1784
+ ),
1785
+ /* @__PURE__ */ jsxs("div", { className: "rounded border max-h-48 overflow-auto divide-y", children: [
1786
+ filtered.map((opt) => {
1787
+ const isSel = value.includes(opt.value);
1788
+ return /* @__PURE__ */ jsx(
1789
+ "button",
1790
+ {
1791
+ type: "button",
1792
+ onClick: () => toggle(opt.value),
1793
+ className: `w-full text-left px-3 py-2 text-sm hover:bg-muted ${isSel ? "bg-muted" : ""}`,
1794
+ children: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-2", children: [
1795
+ /* @__PURE__ */ jsx("input", { type: "checkbox", className: "size-4", readOnly: true, checked: isSel }),
1796
+ /* @__PURE__ */ jsx("span", { children: opt.label })
1797
+ ] })
1798
+ },
1799
+ opt.value
1800
+ );
1801
+ }),
1802
+ !filtered.length ? /* @__PURE__ */ jsx("div", { className: "px-3 py-2 text-sm text-muted-foreground", children: noMatchesLabel }) : null
1803
+ ] })
1804
+ ] });
1805
+ });
1806
+ const FieldControl = React.memo(
1807
+ function FieldControlImpl({
1808
+ field,
1809
+ value,
1810
+ error,
1811
+ options,
1812
+ setValue,
1813
+ values,
1814
+ loadFieldOptions,
1815
+ autoFocus,
1816
+ onSubmitRequest,
1817
+ wrapperClassName,
1818
+ entityIdForField,
1819
+ recordId
1820
+ }) {
1821
+ const t = useT();
1822
+ const fieldSetValue = React.useCallback(
1823
+ (nextValue) => setValue(field.id, nextValue),
1824
+ [setValue, field.id]
1825
+ );
1826
+ const setFormValue = React.useCallback(
1827
+ (targetId, nextValue) => setValue(targetId, nextValue),
1828
+ [setValue]
1829
+ );
1830
+ const builtin = field.type === "custom" ? null : field;
1831
+ const hasLoader = typeof builtin?.loadOptions === "function";
1832
+ const disabled = Boolean(field.disabled);
1833
+ const autoFocusField = autoFocus && !disabled;
1834
+ React.useEffect(() => {
1835
+ if (!hasLoader || field.type === "custom") return;
1836
+ loadFieldOptions(field).catch(() => {
1837
+ });
1838
+ }, [field, hasLoader, loadFieldOptions]);
1839
+ const placeholder = builtin?.placeholder;
1840
+ const rootClassName = wrapperClassName ? `space-y-1 ${wrapperClassName}` : "space-y-1";
1841
+ return /* @__PURE__ */ jsxs("div", { className: rootClassName, "data-crud-field-id": field.id, children: [
1842
+ field.type !== "checkbox" && field.label.trim().length > 0 ? /* @__PURE__ */ jsxs("label", { className: "block text-sm font-medium", children: [
1843
+ field.label,
1844
+ field.required ? /* @__PURE__ */ jsx("span", { className: "text-red-600", children: " *" }) : null
1845
+ ] }) : null,
1846
+ field.type === "text" && /* @__PURE__ */ jsx(
1847
+ TextInput,
1848
+ {
1849
+ value: value == null ? "" : String(value),
1850
+ placeholder,
1851
+ onChange: (next) => fieldSetValue(next),
1852
+ autoFocus: autoFocusField,
1853
+ onSubmit: onSubmitRequest,
1854
+ disabled,
1855
+ suggestions: field.type === "text" ? field.suggestions : void 0
1856
+ }
1857
+ ),
1858
+ field.type === "number" && /* @__PURE__ */ jsx(
1859
+ NumberInput,
1860
+ {
1861
+ value: typeof value === "number" || typeof value === "string" ? value : null,
1862
+ placeholder,
1863
+ onChange: fieldSetValue,
1864
+ autoFocus: autoFocusField,
1865
+ onSubmit: onSubmitRequest
1866
+ }
1867
+ ),
1868
+ field.type === "date" && /* @__PURE__ */ jsx(
1869
+ "input",
1870
+ {
1871
+ type: "date",
1872
+ className: "w-full h-9 rounded border px-2 text-sm",
1873
+ value: typeof value === "string" ? value : "",
1874
+ onChange: (e) => setValue(field.id, e.target.value || void 0),
1875
+ autoFocus: autoFocusField,
1876
+ "data-crud-focus-target": "",
1877
+ disabled
1878
+ }
1879
+ ),
1880
+ field.type === "datetime-local" && /* @__PURE__ */ jsx(
1881
+ "input",
1882
+ {
1883
+ type: "datetime-local",
1884
+ className: "w-full h-9 rounded border px-2 text-sm",
1885
+ value: typeof value === "string" ? value : "",
1886
+ onChange: (e) => setValue(field.id, e.target.value || void 0),
1887
+ autoFocus: autoFocusField,
1888
+ "data-crud-focus-target": "",
1889
+ disabled
1890
+ }
1891
+ ),
1892
+ field.type === "textarea" && /* @__PURE__ */ jsx(
1893
+ TextAreaInput,
1894
+ {
1895
+ value: value == null ? "" : String(value),
1896
+ placeholder,
1897
+ onChange: (next) => fieldSetValue(next),
1898
+ autoFocus: autoFocusField
1899
+ }
1900
+ ),
1901
+ field.type === "richtext" && builtin?.editor === "simple" && /* @__PURE__ */ jsx(SimpleMarkdownEditor, { value: String(value ?? ""), onChange: fieldSetValue }),
1902
+ field.type === "richtext" && builtin?.editor === "html" && /* @__PURE__ */ jsx(HtmlRichTextEditor, { value: String(value ?? ""), onChange: fieldSetValue }),
1903
+ field.type === "richtext" && (!builtin?.editor || builtin.editor !== "simple" && builtin.editor !== "html") && /* @__PURE__ */ jsx(MarkdownEditor, { value: String(value ?? ""), onChange: fieldSetValue }),
1904
+ field.type === "tags" && /* @__PURE__ */ jsx(
1905
+ TagsInput,
1906
+ {
1907
+ value: Array.isArray(value) ? value.filter((item) => typeof item === "string") : [],
1908
+ onChange: (next) => fieldSetValue(next),
1909
+ placeholder,
1910
+ autoFocus: autoFocusField,
1911
+ suggestions: options.map((opt) => opt.label),
1912
+ loadSuggestions: typeof builtin?.loadOptions === "function" ? async (query) => {
1913
+ const opts = await loadFieldOptions(field, query);
1914
+ return opts.map((opt) => opt.label);
1915
+ } : void 0
1916
+ }
1917
+ ),
1918
+ field.type === "combobox" && /* @__PURE__ */ jsx(
1919
+ ComboboxInput,
1920
+ {
1921
+ value: typeof value === "string" ? value : String(value ?? ""),
1922
+ onChange: (next) => fieldSetValue(next),
1923
+ placeholder,
1924
+ autoFocus: autoFocusField,
1925
+ suggestions: builtin?.suggestions ? builtin.suggestions : options.map((opt) => ({ value: opt.value, label: opt.label })),
1926
+ loadSuggestions: typeof builtin?.loadOptions === "function" ? async (query) => {
1927
+ const opts = await loadFieldOptions(field, query);
1928
+ return opts.map((opt) => ({ value: opt.value, label: opt.label }));
1929
+ } : void 0,
1930
+ allowCustomValues: builtin?.allowCustomValues ?? true,
1931
+ disabled
1932
+ }
1933
+ ),
1934
+ field.type === "checkbox" && /* @__PURE__ */ jsxs("label", { className: "inline-flex items-center gap-2", children: [
1935
+ /* @__PURE__ */ jsx(
1936
+ "input",
1937
+ {
1938
+ type: "checkbox",
1939
+ className: "size-4",
1940
+ checked: value === true,
1941
+ onChange: (e) => setValue(field.id, e.target.checked),
1942
+ "data-crud-focus-target": "",
1943
+ disabled
1944
+ }
1945
+ ),
1946
+ /* @__PURE__ */ jsx("span", { className: "text-sm", children: field.label })
1947
+ ] }),
1948
+ field.type === "select" && !builtin?.multiple && /* @__PURE__ */ jsxs(
1949
+ "select",
1950
+ {
1951
+ className: "w-full h-9 rounded border px-2 text-sm",
1952
+ value: Array.isArray(value) ? String(value[0] ?? "") : value == null ? "" : String(value),
1953
+ onChange: (e) => setValue(field.id, e.target.value || void 0),
1954
+ "data-crud-focus-target": "",
1955
+ disabled,
1956
+ children: [
1957
+ /* @__PURE__ */ jsx("option", { value: "", children: t("ui.forms.select.emptyOption", "\u2014") }),
1958
+ options.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, opt.value))
1959
+ ]
1960
+ }
1961
+ ),
1962
+ field.type === "select" && builtin?.multiple && builtin.listbox === true && /* @__PURE__ */ jsx(
1963
+ ListboxMultiSelect,
1964
+ {
1965
+ options,
1966
+ placeholder,
1967
+ value: Array.isArray(value) ? value.filter((item) => typeof item === "string") : [],
1968
+ onChange: (vals) => setValue(field.id, vals),
1969
+ autoFocus: autoFocusField
1970
+ }
1971
+ ),
1972
+ field.type === "select" && builtin?.multiple && builtin.listbox !== true && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-3", children: options.map((opt) => {
1973
+ const arr = Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
1974
+ const checked = arr.includes(opt.value);
1975
+ return /* @__PURE__ */ jsxs("label", { className: "inline-flex items-center gap-2", children: [
1976
+ /* @__PURE__ */ jsx(
1977
+ "input",
1978
+ {
1979
+ type: "checkbox",
1980
+ className: "size-4",
1981
+ checked,
1982
+ onChange: (e) => {
1983
+ const next = new Set(arr);
1984
+ if (e.target.checked) {
1985
+ next.add(opt.value);
1986
+ } else {
1987
+ next.delete(opt.value);
1988
+ }
1989
+ setValue(field.id, Array.from(next));
1990
+ },
1991
+ disabled
1992
+ }
1993
+ ),
1994
+ /* @__PURE__ */ jsx("span", { className: "text-sm", children: opt.label })
1995
+ ] }, opt.value);
1996
+ }) }),
1997
+ field.type === "relation" && /* @__PURE__ */ jsx(
1998
+ RelationSelect,
1999
+ {
2000
+ options,
2001
+ placeholder,
2002
+ value: Array.isArray(value) ? String(value[0] ?? "") : value == null ? "" : String(value),
2003
+ onChange: (selected) => setValue(field.id, selected),
2004
+ autoFocus: autoFocusField
2005
+ }
2006
+ ),
2007
+ field.type === "custom" && /* @__PURE__ */ jsx(Fragment, { children: field.component({
2008
+ id: field.id,
2009
+ value,
2010
+ error,
2011
+ setValue: fieldSetValue,
2012
+ setFormValue,
2013
+ values,
2014
+ entityId: entityIdForField,
2015
+ recordId,
2016
+ autoFocus,
2017
+ disabled
2018
+ }) }),
2019
+ field.description ? /* @__PURE__ */ jsx("div", { className: "text-xs text-muted-foreground", children: field.description }) : null,
2020
+ error ? /* @__PURE__ */ jsx("div", { className: "text-xs text-red-600", children: error }) : null
2021
+ ] });
2022
+ },
2023
+ (prev, next) => prev.field.id === next.field.id && prev.field.type === next.field.type && prev.field.label === next.field.label && prev.field.required === next.field.required && prev.value === next.value && prev.error === next.error && prev.options === next.options && prev.loadFieldOptions === next.loadFieldOptions && prev.autoFocus === next.autoFocus && prev.onSubmitRequest === next.onSubmitRequest && prev.wrapperClassName === next.wrapperClassName && prev.entityIdForField === next.entityIdForField && prev.recordId === next.recordId && (prev.field.type !== "custom" || prev.values === next.values && prev.field.component === next.field.component)
2024
+ );
2025
+ export {
2026
+ CrudForm
2027
+ };
2028
+ //# sourceMappingURL=CrudForm.js.map