@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.
- package/dist/action-guard.d.ts +13 -0
- package/dist/action-guard.d.ts.map +1 -0
- package/dist/action-guard.js +15 -0
- package/dist/action-guard.js.map +1 -0
- package/dist/action-menu.d.ts +17 -0
- package/dist/action-menu.d.ts.map +1 -0
- package/dist/action-menu.jsx +80 -0
- package/dist/action-menu.jsx.map +1 -0
- package/dist/admin-app.d.ts +23 -0
- package/dist/admin-app.d.ts.map +1 -0
- package/dist/admin-app.jsx +407 -0
- package/dist/admin-app.jsx.map +1 -0
- package/dist/admin-router.d.ts +29 -0
- package/dist/admin-router.d.ts.map +1 -0
- package/dist/admin-router.jsx +215 -0
- package/dist/admin-router.jsx.map +1 -0
- package/dist/breadcrumbs.d.ts +17 -0
- package/dist/breadcrumbs.d.ts.map +1 -0
- package/dist/breadcrumbs.jsx +40 -0
- package/dist/breadcrumbs.jsx.map +1 -0
- package/dist/client.d.ts +526 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +582 -0
- package/dist/client.js.map +1 -0
- package/dist/component-loader.d.ts +10 -0
- package/dist/component-loader.d.ts.map +1 -0
- package/dist/component-loader.js +23 -0
- package/dist/component-loader.js.map +1 -0
- package/dist/components/ai-assistant-widget.d.ts +3 -0
- package/dist/components/ai-assistant-widget.d.ts.map +1 -0
- package/dist/components/ai-assistant-widget.jsx +390 -0
- package/dist/components/ai-assistant-widget.jsx.map +1 -0
- package/dist/components/ai-fill-dialog.d.ts +9 -0
- package/dist/components/ai-fill-dialog.d.ts.map +1 -0
- package/dist/components/ai-fill-dialog.jsx +105 -0
- package/dist/components/ai-fill-dialog.jsx.map +1 -0
- package/dist/components/chart-builder-dialog.d.ts +10 -0
- package/dist/components/chart-builder-dialog.d.ts.map +1 -0
- package/dist/components/chart-builder-dialog.jsx +433 -0
- package/dist/components/chart-builder-dialog.jsx.map +1 -0
- package/dist/components/chart-widget.d.ts +12 -0
- package/dist/components/chart-widget.d.ts.map +1 -0
- package/dist/components/chart-widget.jsx +365 -0
- package/dist/components/chart-widget.jsx.map +1 -0
- package/dist/components/global-search-dialog.d.ts +7 -0
- package/dist/components/global-search-dialog.d.ts.map +1 -0
- package/dist/components/global-search-dialog.jsx +187 -0
- package/dist/components/global-search-dialog.jsx.map +1 -0
- package/dist/components/group-settings-dialog.d.ts +13 -0
- package/dist/components/group-settings-dialog.d.ts.map +1 -0
- package/dist/components/group-settings-dialog.jsx +53 -0
- package/dist/components/group-settings-dialog.jsx.map +1 -0
- package/dist/components/move-chart-dialog.d.ts +18 -0
- package/dist/components/move-chart-dialog.d.ts.map +1 -0
- package/dist/components/move-chart-dialog.jsx +68 -0
- package/dist/components/move-chart-dialog.jsx.map +1 -0
- package/dist/components/reference-multi-table-dialog.d.ts +12 -0
- package/dist/components/reference-multi-table-dialog.d.ts.map +1 -0
- package/dist/components/reference-multi-table-dialog.jsx +126 -0
- package/dist/components/reference-multi-table-dialog.jsx.map +1 -0
- package/dist/components/related-records-tabs.d.ts +8 -0
- package/dist/components/related-records-tabs.d.ts.map +1 -0
- package/dist/components/related-records-tabs.jsx +75 -0
- package/dist/components/related-records-tabs.jsx.map +1 -0
- package/dist/components/revisions-button.d.ts +7 -0
- package/dist/components/revisions-button.d.ts.map +1 -0
- package/dist/components/revisions-button.jsx +152 -0
- package/dist/components/revisions-button.jsx.map +1 -0
- package/dist/components/wizard-form.d.ts +43 -0
- package/dist/components/wizard-form.d.ts.map +1 -0
- package/dist/components/wizard-form.jsx +136 -0
- package/dist/components/wizard-form.jsx.map +1 -0
- package/dist/dashboard/time-series.d.ts +20 -0
- package/dist/dashboard/time-series.d.ts.map +1 -0
- package/dist/dashboard/time-series.js +108 -0
- package/dist/dashboard/time-series.js.map +1 -0
- package/dist/dialogs.d.ts +35 -0
- package/dist/dialogs.d.ts.map +1 -0
- package/dist/dialogs.jsx +152 -0
- package/dist/dialogs.jsx.map +1 -0
- package/dist/export.d.ts +39 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/export.js +114 -0
- package/dist/export.js.map +1 -0
- package/dist/extension-registry.d.ts +122 -0
- package/dist/extension-registry.d.ts.map +1 -0
- package/dist/extension-registry.js +93 -0
- package/dist/extension-registry.js.map +1 -0
- package/dist/header-controls.d.ts +4 -0
- package/dist/header-controls.d.ts.map +1 -0
- package/dist/header-controls.jsx +70 -0
- package/dist/header-controls.jsx.map +1 -0
- package/dist/hooks.d.ts +104 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +374 -0
- package/dist/hooks.js.map +1 -0
- package/dist/hotkey-help.d.ts +3 -0
- package/dist/hotkey-help.d.ts.map +1 -0
- package/dist/hotkey-help.jsx +32 -0
- package/dist/hotkey-help.jsx.map +1 -0
- package/dist/hotkey-registry.d.ts +18 -0
- package/dist/hotkey-registry.d.ts.map +1 -0
- package/dist/hotkey-registry.jsx +34 -0
- package/dist/hotkey-registry.jsx.map +1 -0
- package/dist/i18n.d.ts +74 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.jsx +127 -0
- package/dist/i18n.jsx.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/notify.d.ts +41 -0
- package/dist/notify.d.ts.map +1 -0
- package/dist/notify.jsx +58 -0
- package/dist/notify.jsx.map +1 -0
- package/dist/pages/ai-assistant-settings-section.d.ts +3 -0
- package/dist/pages/ai-assistant-settings-section.d.ts.map +1 -0
- package/dist/pages/ai-assistant-settings-section.jsx +126 -0
- package/dist/pages/ai-assistant-settings-section.jsx.map +1 -0
- package/dist/pages/audit-log-page.d.ts +3 -0
- package/dist/pages/audit-log-page.d.ts.map +1 -0
- package/dist/pages/audit-log-page.jsx +354 -0
- package/dist/pages/audit-log-page.jsx.map +1 -0
- package/dist/pages/edit-page.d.ts +7 -0
- package/dist/pages/edit-page.d.ts.map +1 -0
- package/dist/pages/edit-page.jsx +614 -0
- package/dist/pages/edit-page.jsx.map +1 -0
- package/dist/pages/export-dialog.d.ts +11 -0
- package/dist/pages/export-dialog.d.ts.map +1 -0
- package/dist/pages/export-dialog.jsx +102 -0
- package/dist/pages/export-dialog.jsx.map +1 -0
- package/dist/pages/home-page.d.ts +3 -0
- package/dist/pages/home-page.d.ts.map +1 -0
- package/dist/pages/home-page.jsx +211 -0
- package/dist/pages/home-page.jsx.map +1 -0
- package/dist/pages/list-page.d.ts +42 -0
- package/dist/pages/list-page.d.ts.map +1 -0
- package/dist/pages/list-page.jsx +1596 -0
- package/dist/pages/list-page.jsx.map +1 -0
- package/dist/pages/login-page.d.ts +11 -0
- package/dist/pages/login-page.d.ts.map +1 -0
- package/dist/pages/login-page.jsx +157 -0
- package/dist/pages/login-page.jsx.map +1 -0
- package/dist/pages/settings-page.d.ts +5 -0
- package/dist/pages/settings-page.d.ts.map +1 -0
- package/dist/pages/settings-page.jsx +787 -0
- package/dist/pages/settings-page.jsx.map +1 -0
- package/dist/pages/settings-shared.d.ts +51 -0
- package/dist/pages/settings-shared.d.ts.map +1 -0
- package/dist/pages/settings-shared.jsx +66 -0
- package/dist/pages/settings-shared.jsx.map +1 -0
- package/dist/pages/show-page.d.ts +7 -0
- package/dist/pages/show-page.d.ts.map +1 -0
- package/dist/pages/show-page.jsx +147 -0
- package/dist/pages/show-page.jsx.map +1 -0
- package/dist/pages/wizard-create-page.d.ts +14 -0
- package/dist/pages/wizard-create-page.d.ts.map +1 -0
- package/dist/pages/wizard-create-page.jsx +106 -0
- package/dist/pages/wizard-create-page.jsx.map +1 -0
- package/dist/property-renderer.d.ts +8 -0
- package/dist/property-renderer.d.ts.map +1 -0
- package/dist/property-renderer.jsx +690 -0
- package/dist/property-renderer.jsx.map +1 -0
- package/dist/provider.d.ts +20 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.jsx +32 -0
- package/dist/provider.jsx.map +1 -0
- package/dist/realtime.d.ts +22 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +38 -0
- package/dist/realtime.js.map +1 -0
- package/dist/reference.d.ts +52 -0
- package/dist/reference.d.ts.map +1 -0
- package/dist/reference.jsx +224 -0
- package/dist/reference.jsx.map +1 -0
- package/dist/relations.d.ts +11 -0
- package/dist/relations.d.ts.map +1 -0
- package/dist/relations.js +36 -0
- package/dist/relations.js.map +1 -0
- package/dist/router.d.ts +82 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.jsx +187 -0
- package/dist/router.jsx.map +1 -0
- package/dist/show-when.d.ts +7 -0
- package/dist/show-when.d.ts.map +1 -0
- package/dist/show-when.js +77 -0
- package/dist/show-when.js.map +1 -0
- package/dist/types.d.ts +194 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +18 -0
- package/dist/types.js.map +1 -0
- package/dist/use-dashboard-charts.d.ts +93 -0
- package/dist/use-dashboard-charts.d.ts.map +1 -0
- package/dist/use-dashboard-charts.js +263 -0
- package/dist/use-dashboard-charts.js.map +1 -0
- package/dist/use-hotkey.d.ts +17 -0
- package/dist/use-hotkey.d.ts.map +1 -0
- package/dist/use-hotkey.js +103 -0
- package/dist/use-hotkey.js.map +1 -0
- package/dist/user-directory.d.ts +18 -0
- package/dist/user-directory.d.ts.map +1 -0
- package/dist/user-directory.js +51 -0
- package/dist/user-directory.js.map +1 -0
- package/dist/validation.d.ts +22 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +338 -0
- package/dist/validation.js.map +1 -0
- package/package.json +59 -0
- package/src/action-guard.ts +20 -0
- package/src/action-menu.tsx +161 -0
- package/src/admin-app.tsx +630 -0
- package/src/admin-router.tsx +273 -0
- package/src/breadcrumbs.tsx +75 -0
- package/src/client.ts +1093 -0
- package/src/component-loader.ts +33 -0
- package/src/components/ai-assistant-widget.tsx +565 -0
- package/src/components/ai-fill-dialog.tsx +143 -0
- package/src/components/chart-builder-dialog.tsx +618 -0
- package/src/components/chart-widget.tsx +654 -0
- package/src/components/global-search-dialog.tsx +272 -0
- package/src/components/group-settings-dialog.tsx +93 -0
- package/src/components/move-chart-dialog.tsx +130 -0
- package/src/components/reference-multi-table-dialog.tsx +196 -0
- package/src/components/related-records-tabs.tsx +130 -0
- package/src/components/revisions-button.tsx +237 -0
- package/src/components/wizard-form.tsx +302 -0
- package/src/dashboard/time-series.ts +125 -0
- package/src/dialogs.tsx +265 -0
- package/src/export.ts +140 -0
- package/src/extension-registry.ts +195 -0
- package/src/header-controls.tsx +125 -0
- package/src/hooks.ts +509 -0
- package/src/hotkey-help.tsx +56 -0
- package/src/hotkey-registry.tsx +60 -0
- package/src/i18n.tsx +267 -0
- package/src/index.ts +192 -0
- package/src/notify.tsx +94 -0
- package/src/pages/ai-assistant-settings-section.tsx +167 -0
- package/src/pages/audit-log-page.tsx +580 -0
- package/src/pages/edit-page.tsx +743 -0
- package/src/pages/export-dialog.tsx +154 -0
- package/src/pages/home-page.tsx +318 -0
- package/src/pages/list-page.tsx +2645 -0
- package/src/pages/login-page.tsx +242 -0
- package/src/pages/settings-page.tsx +1143 -0
- package/src/pages/settings-shared.tsx +134 -0
- package/src/pages/show-page.tsx +223 -0
- package/src/pages/wizard-create-page.tsx +164 -0
- package/src/property-renderer.tsx +1143 -0
- package/src/provider.tsx +70 -0
- package/src/realtime.ts +55 -0
- package/src/reference.tsx +386 -0
- package/src/relations.ts +55 -0
- package/src/router.tsx +211 -0
- package/src/show-when.ts +76 -0
- package/src/types.ts +198 -0
- package/src/use-dashboard-charts.ts +362 -0
- package/src/use-hotkey.ts +128 -0
- package/src/user-directory.ts +56 -0
- 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
|
+
}
|