@modern-admin/react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/dist/action-guard.d.ts +13 -0
  2. package/dist/action-guard.d.ts.map +1 -0
  3. package/dist/action-guard.js +15 -0
  4. package/dist/action-guard.js.map +1 -0
  5. package/dist/action-menu.d.ts +17 -0
  6. package/dist/action-menu.d.ts.map +1 -0
  7. package/dist/action-menu.jsx +80 -0
  8. package/dist/action-menu.jsx.map +1 -0
  9. package/dist/admin-app.d.ts +23 -0
  10. package/dist/admin-app.d.ts.map +1 -0
  11. package/dist/admin-app.jsx +407 -0
  12. package/dist/admin-app.jsx.map +1 -0
  13. package/dist/admin-router.d.ts +29 -0
  14. package/dist/admin-router.d.ts.map +1 -0
  15. package/dist/admin-router.jsx +215 -0
  16. package/dist/admin-router.jsx.map +1 -0
  17. package/dist/breadcrumbs.d.ts +17 -0
  18. package/dist/breadcrumbs.d.ts.map +1 -0
  19. package/dist/breadcrumbs.jsx +40 -0
  20. package/dist/breadcrumbs.jsx.map +1 -0
  21. package/dist/client.d.ts +526 -0
  22. package/dist/client.d.ts.map +1 -0
  23. package/dist/client.js +582 -0
  24. package/dist/client.js.map +1 -0
  25. package/dist/component-loader.d.ts +10 -0
  26. package/dist/component-loader.d.ts.map +1 -0
  27. package/dist/component-loader.js +23 -0
  28. package/dist/component-loader.js.map +1 -0
  29. package/dist/components/ai-assistant-widget.d.ts +3 -0
  30. package/dist/components/ai-assistant-widget.d.ts.map +1 -0
  31. package/dist/components/ai-assistant-widget.jsx +390 -0
  32. package/dist/components/ai-assistant-widget.jsx.map +1 -0
  33. package/dist/components/ai-fill-dialog.d.ts +9 -0
  34. package/dist/components/ai-fill-dialog.d.ts.map +1 -0
  35. package/dist/components/ai-fill-dialog.jsx +105 -0
  36. package/dist/components/ai-fill-dialog.jsx.map +1 -0
  37. package/dist/components/chart-builder-dialog.d.ts +10 -0
  38. package/dist/components/chart-builder-dialog.d.ts.map +1 -0
  39. package/dist/components/chart-builder-dialog.jsx +433 -0
  40. package/dist/components/chart-builder-dialog.jsx.map +1 -0
  41. package/dist/components/chart-widget.d.ts +12 -0
  42. package/dist/components/chart-widget.d.ts.map +1 -0
  43. package/dist/components/chart-widget.jsx +365 -0
  44. package/dist/components/chart-widget.jsx.map +1 -0
  45. package/dist/components/global-search-dialog.d.ts +7 -0
  46. package/dist/components/global-search-dialog.d.ts.map +1 -0
  47. package/dist/components/global-search-dialog.jsx +187 -0
  48. package/dist/components/global-search-dialog.jsx.map +1 -0
  49. package/dist/components/group-settings-dialog.d.ts +13 -0
  50. package/dist/components/group-settings-dialog.d.ts.map +1 -0
  51. package/dist/components/group-settings-dialog.jsx +53 -0
  52. package/dist/components/group-settings-dialog.jsx.map +1 -0
  53. package/dist/components/move-chart-dialog.d.ts +18 -0
  54. package/dist/components/move-chart-dialog.d.ts.map +1 -0
  55. package/dist/components/move-chart-dialog.jsx +68 -0
  56. package/dist/components/move-chart-dialog.jsx.map +1 -0
  57. package/dist/components/reference-multi-table-dialog.d.ts +12 -0
  58. package/dist/components/reference-multi-table-dialog.d.ts.map +1 -0
  59. package/dist/components/reference-multi-table-dialog.jsx +126 -0
  60. package/dist/components/reference-multi-table-dialog.jsx.map +1 -0
  61. package/dist/components/related-records-tabs.d.ts +8 -0
  62. package/dist/components/related-records-tabs.d.ts.map +1 -0
  63. package/dist/components/related-records-tabs.jsx +75 -0
  64. package/dist/components/related-records-tabs.jsx.map +1 -0
  65. package/dist/components/revisions-button.d.ts +7 -0
  66. package/dist/components/revisions-button.d.ts.map +1 -0
  67. package/dist/components/revisions-button.jsx +152 -0
  68. package/dist/components/revisions-button.jsx.map +1 -0
  69. package/dist/components/wizard-form.d.ts +43 -0
  70. package/dist/components/wizard-form.d.ts.map +1 -0
  71. package/dist/components/wizard-form.jsx +136 -0
  72. package/dist/components/wizard-form.jsx.map +1 -0
  73. package/dist/dashboard/time-series.d.ts +20 -0
  74. package/dist/dashboard/time-series.d.ts.map +1 -0
  75. package/dist/dashboard/time-series.js +108 -0
  76. package/dist/dashboard/time-series.js.map +1 -0
  77. package/dist/dialogs.d.ts +35 -0
  78. package/dist/dialogs.d.ts.map +1 -0
  79. package/dist/dialogs.jsx +152 -0
  80. package/dist/dialogs.jsx.map +1 -0
  81. package/dist/export.d.ts +39 -0
  82. package/dist/export.d.ts.map +1 -0
  83. package/dist/export.js +114 -0
  84. package/dist/export.js.map +1 -0
  85. package/dist/extension-registry.d.ts +122 -0
  86. package/dist/extension-registry.d.ts.map +1 -0
  87. package/dist/extension-registry.js +93 -0
  88. package/dist/extension-registry.js.map +1 -0
  89. package/dist/header-controls.d.ts +4 -0
  90. package/dist/header-controls.d.ts.map +1 -0
  91. package/dist/header-controls.jsx +70 -0
  92. package/dist/header-controls.jsx.map +1 -0
  93. package/dist/hooks.d.ts +104 -0
  94. package/dist/hooks.d.ts.map +1 -0
  95. package/dist/hooks.js +374 -0
  96. package/dist/hooks.js.map +1 -0
  97. package/dist/hotkey-help.d.ts +3 -0
  98. package/dist/hotkey-help.d.ts.map +1 -0
  99. package/dist/hotkey-help.jsx +32 -0
  100. package/dist/hotkey-help.jsx.map +1 -0
  101. package/dist/hotkey-registry.d.ts +18 -0
  102. package/dist/hotkey-registry.d.ts.map +1 -0
  103. package/dist/hotkey-registry.jsx +34 -0
  104. package/dist/hotkey-registry.jsx.map +1 -0
  105. package/dist/i18n.d.ts +74 -0
  106. package/dist/i18n.d.ts.map +1 -0
  107. package/dist/i18n.jsx +127 -0
  108. package/dist/i18n.jsx.map +1 -0
  109. package/dist/index.d.ts +35 -0
  110. package/dist/index.d.ts.map +1 -0
  111. package/dist/index.js +36 -0
  112. package/dist/index.js.map +1 -0
  113. package/dist/notify.d.ts +41 -0
  114. package/dist/notify.d.ts.map +1 -0
  115. package/dist/notify.jsx +58 -0
  116. package/dist/notify.jsx.map +1 -0
  117. package/dist/pages/ai-assistant-settings-section.d.ts +3 -0
  118. package/dist/pages/ai-assistant-settings-section.d.ts.map +1 -0
  119. package/dist/pages/ai-assistant-settings-section.jsx +126 -0
  120. package/dist/pages/ai-assistant-settings-section.jsx.map +1 -0
  121. package/dist/pages/audit-log-page.d.ts +3 -0
  122. package/dist/pages/audit-log-page.d.ts.map +1 -0
  123. package/dist/pages/audit-log-page.jsx +354 -0
  124. package/dist/pages/audit-log-page.jsx.map +1 -0
  125. package/dist/pages/edit-page.d.ts +7 -0
  126. package/dist/pages/edit-page.d.ts.map +1 -0
  127. package/dist/pages/edit-page.jsx +614 -0
  128. package/dist/pages/edit-page.jsx.map +1 -0
  129. package/dist/pages/export-dialog.d.ts +11 -0
  130. package/dist/pages/export-dialog.d.ts.map +1 -0
  131. package/dist/pages/export-dialog.jsx +102 -0
  132. package/dist/pages/export-dialog.jsx.map +1 -0
  133. package/dist/pages/home-page.d.ts +3 -0
  134. package/dist/pages/home-page.d.ts.map +1 -0
  135. package/dist/pages/home-page.jsx +211 -0
  136. package/dist/pages/home-page.jsx.map +1 -0
  137. package/dist/pages/list-page.d.ts +42 -0
  138. package/dist/pages/list-page.d.ts.map +1 -0
  139. package/dist/pages/list-page.jsx +1596 -0
  140. package/dist/pages/list-page.jsx.map +1 -0
  141. package/dist/pages/login-page.d.ts +11 -0
  142. package/dist/pages/login-page.d.ts.map +1 -0
  143. package/dist/pages/login-page.jsx +157 -0
  144. package/dist/pages/login-page.jsx.map +1 -0
  145. package/dist/pages/settings-page.d.ts +5 -0
  146. package/dist/pages/settings-page.d.ts.map +1 -0
  147. package/dist/pages/settings-page.jsx +787 -0
  148. package/dist/pages/settings-page.jsx.map +1 -0
  149. package/dist/pages/settings-shared.d.ts +51 -0
  150. package/dist/pages/settings-shared.d.ts.map +1 -0
  151. package/dist/pages/settings-shared.jsx +66 -0
  152. package/dist/pages/settings-shared.jsx.map +1 -0
  153. package/dist/pages/show-page.d.ts +7 -0
  154. package/dist/pages/show-page.d.ts.map +1 -0
  155. package/dist/pages/show-page.jsx +147 -0
  156. package/dist/pages/show-page.jsx.map +1 -0
  157. package/dist/pages/wizard-create-page.d.ts +14 -0
  158. package/dist/pages/wizard-create-page.d.ts.map +1 -0
  159. package/dist/pages/wizard-create-page.jsx +106 -0
  160. package/dist/pages/wizard-create-page.jsx.map +1 -0
  161. package/dist/property-renderer.d.ts +8 -0
  162. package/dist/property-renderer.d.ts.map +1 -0
  163. package/dist/property-renderer.jsx +690 -0
  164. package/dist/property-renderer.jsx.map +1 -0
  165. package/dist/provider.d.ts +20 -0
  166. package/dist/provider.d.ts.map +1 -0
  167. package/dist/provider.jsx +32 -0
  168. package/dist/provider.jsx.map +1 -0
  169. package/dist/realtime.d.ts +22 -0
  170. package/dist/realtime.d.ts.map +1 -0
  171. package/dist/realtime.js +38 -0
  172. package/dist/realtime.js.map +1 -0
  173. package/dist/reference.d.ts +52 -0
  174. package/dist/reference.d.ts.map +1 -0
  175. package/dist/reference.jsx +224 -0
  176. package/dist/reference.jsx.map +1 -0
  177. package/dist/relations.d.ts +11 -0
  178. package/dist/relations.d.ts.map +1 -0
  179. package/dist/relations.js +36 -0
  180. package/dist/relations.js.map +1 -0
  181. package/dist/router.d.ts +82 -0
  182. package/dist/router.d.ts.map +1 -0
  183. package/dist/router.jsx +187 -0
  184. package/dist/router.jsx.map +1 -0
  185. package/dist/show-when.d.ts +7 -0
  186. package/dist/show-when.d.ts.map +1 -0
  187. package/dist/show-when.js +77 -0
  188. package/dist/show-when.js.map +1 -0
  189. package/dist/types.d.ts +194 -0
  190. package/dist/types.d.ts.map +1 -0
  191. package/dist/types.js +18 -0
  192. package/dist/types.js.map +1 -0
  193. package/dist/use-dashboard-charts.d.ts +93 -0
  194. package/dist/use-dashboard-charts.d.ts.map +1 -0
  195. package/dist/use-dashboard-charts.js +263 -0
  196. package/dist/use-dashboard-charts.js.map +1 -0
  197. package/dist/use-hotkey.d.ts +17 -0
  198. package/dist/use-hotkey.d.ts.map +1 -0
  199. package/dist/use-hotkey.js +103 -0
  200. package/dist/use-hotkey.js.map +1 -0
  201. package/dist/user-directory.d.ts +18 -0
  202. package/dist/user-directory.d.ts.map +1 -0
  203. package/dist/user-directory.js +51 -0
  204. package/dist/user-directory.js.map +1 -0
  205. package/dist/validation.d.ts +22 -0
  206. package/dist/validation.d.ts.map +1 -0
  207. package/dist/validation.js +338 -0
  208. package/dist/validation.js.map +1 -0
  209. package/package.json +59 -0
  210. package/src/action-guard.ts +20 -0
  211. package/src/action-menu.tsx +161 -0
  212. package/src/admin-app.tsx +630 -0
  213. package/src/admin-router.tsx +273 -0
  214. package/src/breadcrumbs.tsx +75 -0
  215. package/src/client.ts +1093 -0
  216. package/src/component-loader.ts +33 -0
  217. package/src/components/ai-assistant-widget.tsx +565 -0
  218. package/src/components/ai-fill-dialog.tsx +143 -0
  219. package/src/components/chart-builder-dialog.tsx +618 -0
  220. package/src/components/chart-widget.tsx +654 -0
  221. package/src/components/global-search-dialog.tsx +272 -0
  222. package/src/components/group-settings-dialog.tsx +93 -0
  223. package/src/components/move-chart-dialog.tsx +130 -0
  224. package/src/components/reference-multi-table-dialog.tsx +196 -0
  225. package/src/components/related-records-tabs.tsx +130 -0
  226. package/src/components/revisions-button.tsx +237 -0
  227. package/src/components/wizard-form.tsx +302 -0
  228. package/src/dashboard/time-series.ts +125 -0
  229. package/src/dialogs.tsx +265 -0
  230. package/src/export.ts +140 -0
  231. package/src/extension-registry.ts +195 -0
  232. package/src/header-controls.tsx +125 -0
  233. package/src/hooks.ts +509 -0
  234. package/src/hotkey-help.tsx +56 -0
  235. package/src/hotkey-registry.tsx +60 -0
  236. package/src/i18n.tsx +267 -0
  237. package/src/index.ts +192 -0
  238. package/src/notify.tsx +94 -0
  239. package/src/pages/ai-assistant-settings-section.tsx +167 -0
  240. package/src/pages/audit-log-page.tsx +580 -0
  241. package/src/pages/edit-page.tsx +743 -0
  242. package/src/pages/export-dialog.tsx +154 -0
  243. package/src/pages/home-page.tsx +318 -0
  244. package/src/pages/list-page.tsx +2645 -0
  245. package/src/pages/login-page.tsx +242 -0
  246. package/src/pages/settings-page.tsx +1143 -0
  247. package/src/pages/settings-shared.tsx +134 -0
  248. package/src/pages/show-page.tsx +223 -0
  249. package/src/pages/wizard-create-page.tsx +164 -0
  250. package/src/property-renderer.tsx +1143 -0
  251. package/src/provider.tsx +70 -0
  252. package/src/realtime.ts +55 -0
  253. package/src/reference.tsx +386 -0
  254. package/src/relations.ts +55 -0
  255. package/src/router.tsx +211 -0
  256. package/src/show-when.ts +76 -0
  257. package/src/types.ts +198 -0
  258. package/src/use-dashboard-charts.ts +362 -0
  259. package/src/use-hotkey.ts +128 -0
  260. package/src/user-directory.ts +56 -0
  261. package/src/validation.ts +361 -0
@@ -0,0 +1,361 @@
1
+ // Per-property Zod schema builder with localized error messages.
2
+ //
3
+ // Each PropertyJSON.type maps to a typed validator: numerics get coerced from
4
+ // string inputs, dates parsed via Date, references checked for non-empty,
5
+ // arrays validated as collections of FK-shaped values, and enums (anything
6
+ // with `availableValues`) restricted to the declared set. Required vs
7
+ // optional is honoured uniformly. All error messages route through the
8
+ // passed translator so the active locale wins.
9
+ //
10
+ // The builder returns plain Zod schemas — RHF + zodResolver consume them
11
+ // directly. Keep the builder pure (no React/i18n imports) so it stays
12
+ // testable and reusable from non-React contexts.
13
+
14
+ import { z, type ZodType } from 'zod'
15
+ import type { PropertyJSON } from './types.js'
16
+ import { evaluateShowWhen } from './show-when.js'
17
+
18
+ /**
19
+ * Lazy form-snapshot reader. Passed by the caller (the edit page) so the
20
+ * schema can consult the live form values at validation time without taking
21
+ * a hard dependency on RHF. Returning `{}` is fine — every property defaults
22
+ * to "visible" then.
23
+ */
24
+ export type FormValuesGetter = () => Record<string, unknown>
25
+
26
+ export type Translator = (key: string, params?: Record<string, unknown>) => string
27
+
28
+ /** Email pattern: deliberately loose, matches Better Auth / common usage. */
29
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
30
+ const URL_RE = /^https?:\/\/.+/i
31
+ const UUID_RE =
32
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
33
+
34
+ const isBlank = (v: unknown): boolean =>
35
+ v === undefined || v === null || (typeof v === 'string' && v.trim() === '')
36
+
37
+ /** Build a string validator with optional/required + format checks. */
38
+ function stringSchema(p: PropertyJSON, t: Translator, format?: 'email' | 'url' | 'uuid'): ZodType {
39
+ const label = p.label
40
+ const checkFormat = (v: string): boolean => {
41
+ if (format === 'email') return EMAIL_RE.test(v)
42
+ if (format === 'url') return URL_RE.test(v)
43
+ if (format === 'uuid') return UUID_RE.test(v)
44
+ return true
45
+ }
46
+ const formatKey =
47
+ format === 'email'
48
+ ? 'validation:invalidEmail'
49
+ : format === 'url'
50
+ ? 'validation:invalidUrl'
51
+ : format === 'uuid'
52
+ ? 'validation:invalidUuid'
53
+ : null
54
+
55
+ return z
56
+ .union([z.string(), z.null(), z.undefined()])
57
+ .transform((v) => (typeof v === 'string' ? v : v == null ? '' : String(v)))
58
+ .superRefine((value, ctx) => {
59
+ const blank = value.trim() === ''
60
+ if (blank) {
61
+ if (p.isRequired) {
62
+ ctx.addIssue({ code: 'custom', message: t('validation:required', { label }) })
63
+ }
64
+ return
65
+ }
66
+ if (formatKey && !checkFormat(value)) {
67
+ ctx.addIssue({ code: 'custom', message: t(formatKey, { label }) })
68
+ }
69
+ })
70
+ .transform((v) => (v.trim() === '' ? null : v))
71
+ }
72
+
73
+ /** Number validator with coercion from string inputs (HTML inputs ship strings). */
74
+ function numberSchema(p: PropertyJSON, t: Translator, integer = false): ZodType {
75
+ const label = p.label
76
+ // Validate at the unknown layer because Zod 4's z.number() rejects NaN
77
+ // before our refinement can produce a localized message. We coerce here,
78
+ // then assert the type ourselves so all error paths flow through superRefine.
79
+ return z.unknown().superRefine((raw, ctx) => {
80
+ if (isBlank(raw)) {
81
+ if (p.isRequired) {
82
+ ctx.addIssue({ code: 'custom', message: t('validation:required', { label }) })
83
+ }
84
+ return
85
+ }
86
+ const value = typeof raw === 'number' ? raw : Number(raw)
87
+ if (!Number.isFinite(value)) {
88
+ ctx.addIssue({ code: 'custom', message: t('validation:invalidNumber', { label }) })
89
+ return
90
+ }
91
+ if (integer && !Number.isInteger(value)) {
92
+ ctx.addIssue({ code: 'custom', message: t('validation:invalidInteger', { label }) })
93
+ }
94
+ }).transform((raw) => {
95
+ if (isBlank(raw)) return null
96
+ const n = typeof raw === 'number' ? raw : Number(raw)
97
+ return Number.isFinite(n) ? n : null
98
+ })
99
+ }
100
+
101
+ /** Boolean validator; missing value → false (HTML default for unchecked). */
102
+ function booleanSchema(_p: PropertyJSON, _t: Translator): ZodType {
103
+ return z.preprocess((v) => (typeof v === 'boolean' ? v : Boolean(v)), z.boolean())
104
+ }
105
+
106
+ /** Date validator: accepts ISO strings or `Date`; rejects unparseable input. */
107
+ function dateSchema(p: PropertyJSON, t: Translator): ZodType {
108
+ const label = p.label
109
+ return z
110
+ .union([z.string(), z.date(), z.null(), z.undefined()])
111
+ .superRefine((value, ctx) => {
112
+ if (isBlank(value)) {
113
+ if (p.isRequired) {
114
+ ctx.addIssue({ code: 'custom', message: t('validation:required', { label }) })
115
+ }
116
+ return
117
+ }
118
+ const parsed = value instanceof Date ? value : new Date(String(value))
119
+ if (Number.isNaN(parsed.getTime())) {
120
+ ctx.addIssue({ code: 'custom', message: t('validation:invalidDate', { label }) })
121
+ }
122
+ })
123
+ .transform((v) => (isBlank(v) ? null : v))
124
+ }
125
+
126
+ /** Single reference validator: requires a non-empty FK when isRequired. */
127
+ function referenceSchema(p: PropertyJSON, t: Translator): ZodType {
128
+ const label = p.label
129
+ return z
130
+ .union([z.string(), z.number(), z.null(), z.undefined()])
131
+ .superRefine((value, ctx) => {
132
+ if (isBlank(value) && p.isRequired) {
133
+ ctx.addIssue({ code: 'custom', message: t('validation:required', { label }) })
134
+ }
135
+ })
136
+ .transform((v) => (isBlank(v) ? null : v))
137
+ }
138
+
139
+ /** Multi-reference validator: array of FKs, must be non-empty when required.
140
+ *
141
+ * Pre-processes the input to flatten any accidentally-nested arrays and drop
142
+ * blank items, so a stale form state like `[["3","4"]]` still validates as
143
+ * the expected `["3","4"]` rather than tripping the inner string check. */
144
+ function multiReferenceSchema(p: PropertyJSON, t: Translator): ZodType {
145
+ const label = p.label
146
+ const normalize = (raw: unknown): Array<string | number> => {
147
+ if (raw == null) return []
148
+ const items = Array.isArray(raw) ? raw : [raw]
149
+ const out: Array<string | number> = []
150
+ for (const item of items) {
151
+ if (Array.isArray(item)) {
152
+ for (const sub of item) {
153
+ if (sub != null && sub !== '') out.push(sub as string | number)
154
+ }
155
+ } else if (item != null && item !== '') {
156
+ out.push(item as string | number)
157
+ }
158
+ }
159
+ return out
160
+ }
161
+ return z
162
+ .preprocess(normalize, z.array(z.union([z.string(), z.number()])))
163
+ .superRefine((value, ctx) => {
164
+ if (p.isRequired && value.length === 0) {
165
+ ctx.addIssue({ code: 'custom', message: t('validation:emptySelection', { label }) })
166
+ }
167
+ })
168
+ }
169
+
170
+ /** File validator: single-file values are storage keys (string/null), while
171
+ * multi-file values are arrays of storage keys. */
172
+ function fileSchema(p: PropertyJSON, t: Translator): ZodType {
173
+ if (p.isArray) {
174
+ return multiReferenceSchema(p, t)
175
+ }
176
+ return stringSchema(p, t)
177
+ }
178
+
179
+ /** Enum validator from `availableValues`. Unmatched value → notInList. */
180
+ function enumSchema(p: PropertyJSON, t: Translator): ZodType {
181
+ const label = p.label
182
+ const allowed = new Set((p.availableValues ?? []).map((v) => v.value))
183
+ return z
184
+ .union([z.string(), z.null(), z.undefined()])
185
+ .transform((v) => (v == null ? '' : String(v)))
186
+ .superRefine((value, ctx) => {
187
+ if (value === '') {
188
+ if (p.isRequired) {
189
+ ctx.addIssue({ code: 'custom', message: t('validation:required', { label }) })
190
+ }
191
+ return
192
+ }
193
+ if (!allowed.has(value)) {
194
+ ctx.addIssue({ code: 'custom', message: t('validation:notInList', { label }) })
195
+ }
196
+ })
197
+ .transform((v) => (v === '' ? null : v))
198
+ }
199
+
200
+ /** JSON validator: accepts any JSON-serializable value (object/array/null).
201
+ *
202
+ * Unlike string fields, json fields hold a parsed JavaScript value — the
203
+ * JsonEditor emits objects/arrays directly. The schema passes them through
204
+ * as-is; only the required check (null/undefined → error) is applied. */
205
+ function jsonSchema(p: PropertyJSON, t: Translator): ZodType {
206
+ const label = p.label
207
+ return z
208
+ .unknown()
209
+ .superRefine((v, ctx) => {
210
+ if ((v == null || v === '') && p.isRequired) {
211
+ ctx.addIssue({ code: 'custom', message: t('validation:required', { label }) })
212
+ }
213
+ })
214
+ .transform((v) => (v === '' ? null : v))
215
+ }
216
+
217
+ /** Many-to-many validator: array of `{ id, ...extras }` items.
218
+ *
219
+ * The M2M editor emits an array of objects (id of the referenced record plus
220
+ * arbitrary junction extra fields, e.g. `addedAt`, `position`). Bare ids are
221
+ * also accepted and normalized into `{ id }` objects so legacy form state
222
+ * doesn't trip the schema. Extra fields are passed through untouched —
223
+ * the backend's m2m feature persists whatever it recognises and ignores
224
+ * the rest. */
225
+ function m2mSchema(p: PropertyJSON, t: Translator): ZodType {
226
+ const label = p.label
227
+ const normalize = (raw: unknown): Array<Record<string, unknown>> => {
228
+ if (raw == null) return []
229
+ const items = Array.isArray(raw) ? raw : [raw]
230
+ const out: Array<Record<string, unknown>> = []
231
+ for (const item of items) {
232
+ if (item == null || item === '') continue
233
+ if (typeof item === 'string' || typeof item === 'number') {
234
+ out.push({ id: String(item) })
235
+ continue
236
+ }
237
+ if (typeof item === 'object') {
238
+ const obj = item as Record<string, unknown>
239
+ const id = obj.id ?? obj.value
240
+ if (id == null || id === '') continue
241
+ out.push({ ...obj, id: String(id) })
242
+ }
243
+ }
244
+ return out
245
+ }
246
+ return z
247
+ .preprocess(normalize, z.array(z.record(z.string(), z.unknown())))
248
+ .superRefine((value, ctx) => {
249
+ if (p.isRequired && value.length === 0) {
250
+ ctx.addIssue({ code: 'custom', message: t('validation:emptySelection', { label }) })
251
+ }
252
+ })
253
+ }
254
+
255
+ /** Build the Zod schema for a property without considering `showWhen`. */
256
+ function buildPropertySchemaInner(p: PropertyJSON, t: Translator): ZodType {
257
+ // M2M is structurally different from a multi-reference — its values are
258
+ // `{ id, ...extras }` objects, not bare FKs. Branch first so the
259
+ // `p.reference` check below doesn't capture it.
260
+ if (p.type === 'm2m') {
261
+ return m2mSchema(p, t)
262
+ }
263
+ // Enum-like properties always go through the availableValues path —
264
+ // overrides the raw type (e.g. a string with a fixed set of options).
265
+ if (p.availableValues && p.availableValues.length > 0) {
266
+ return enumSchema(p, t)
267
+ }
268
+ if (p.reference) {
269
+ return p.isArray ? multiReferenceSchema(p, t) : referenceSchema(p, t)
270
+ }
271
+ switch (p.type) {
272
+ case 'boolean':
273
+ return booleanSchema(p, t)
274
+ case 'number':
275
+ case 'float':
276
+ case 'currency':
277
+ case 'money':
278
+ return numberSchema(p, t, false)
279
+ case 'integer':
280
+ return numberSchema(p, t, true)
281
+ case 'date':
282
+ case 'datetime':
283
+ case 'datetime-local':
284
+ return dateSchema(p, t)
285
+ case 'email':
286
+ return stringSchema(p, t, 'email')
287
+ case 'url':
288
+ return stringSchema(p, t, 'url')
289
+ case 'uuid':
290
+ return stringSchema(p, t, 'uuid')
291
+ case 'json':
292
+ return jsonSchema(p, t)
293
+ case 'string':
294
+ case 'text':
295
+ case 'textarea':
296
+ case 'password':
297
+ case 'richtext':
298
+ case 'color':
299
+ case 'file':
300
+ return fileSchema(p, t)
301
+ default:
302
+ return stringSchema(p, t)
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Map a PropertyJSON to its Zod schema. When the property has a `showWhen`
308
+ * rule and a `getValues` getter is supplied, the schema short-circuits to
309
+ * a no-op while the rule does not match — letting hidden branches pass
310
+ * validation without their required/format checks tripping submission.
311
+ */
312
+ export function buildPropertySchema(
313
+ p: PropertyJSON,
314
+ t: Translator,
315
+ getValues?: FormValuesGetter,
316
+ ): ZodType {
317
+ const inner = buildPropertySchemaInner(p, t)
318
+ if (!p.showWhen || !getValues) return inner
319
+
320
+ // Wrap: only forward to `inner` when visible. Hidden → accept anything,
321
+ // pass it through unchanged. We re-emit issues from `inner` so error
322
+ // messages and paths stay identical to the non-conditional case.
323
+ return z
324
+ .any()
325
+ .superRefine((value, ctx) => {
326
+ if (!evaluateShowWhen(p.showWhen, getValues())) return
327
+ const result = inner.safeParse(value)
328
+ if (!result.success) {
329
+ // Zod 4's `RefinementCtx.addIssue` accepts a structurally looser
330
+ // shape than `$ZodIssue`; spread into a plain object to satisfy
331
+ // the inferred parameter type.
332
+ for (const issue of result.error.issues) {
333
+ ctx.addIssue({ ...issue } as Parameters<typeof ctx.addIssue>[0])
334
+ }
335
+ }
336
+ })
337
+ .transform((value) => {
338
+ if (!evaluateShowWhen(p.showWhen, getValues())) return value
339
+ const result = inner.safeParse(value)
340
+ return result.success ? result.data : value
341
+ })
342
+ }
343
+
344
+ /** Build a Zod object schema covering every editable property in a resource. */
345
+ export function buildValidationSchema(
346
+ properties: PropertyJSON[],
347
+ t: Translator,
348
+ getValues?: FormValuesGetter,
349
+ ): z.ZodObject<Record<string, ZodType>> {
350
+ const shape: Record<string, ZodType> = {}
351
+ for (const p of properties) shape[p.path] = buildPropertySchema(p, t, getValues)
352
+ return z.object(shape)
353
+ }
354
+
355
+ /** Sensible empty default per type so RHF stays controlled from first render. */
356
+ export function defaultValueFor(p: PropertyJSON): unknown {
357
+ if (p.type === 'boolean') return false
358
+ if (p.type === 'json') return null
359
+ if (p.isArray) return []
360
+ return ''
361
+ }