@asteby/metacore-runtime-react 37.0.3 → 37.0.5
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/CHANGELOG.md +12 -0
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +41 -23
- package/dist/dialogs/dynamic-record.d.ts +10 -2
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +70 -36
- package/dist/dynamic-form.js +12 -2
- package/dist/dynamic-line-items.d.ts +6 -1
- package/dist/dynamic-line-items.d.ts.map +1 -1
- package/dist/dynamic-line-items.js +26 -11
- package/dist/dynamic-select-field.d.ts +3 -1
- package/dist/dynamic-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +3 -2
- package/dist/field-validation-ui.d.ts +22 -0
- package/dist/field-validation-ui.d.ts.map +1 -0
- package/dist/field-validation-ui.js +123 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +1 -1
- package/src/__tests__/dynamic-record-field-validation.test.tsx +110 -0
- package/src/__tests__/field-validation-ui.test.ts +57 -0
- package/src/action-modal-dispatcher.tsx +60 -19
- package/src/dialogs/dynamic-record.tsx +89 -32
- package/src/dynamic-form.tsx +12 -4
- package/src/dynamic-line-items.tsx +58 -9
- package/src/dynamic-select-field.tsx +8 -1
- package/src/field-validation-ui.ts +140 -0
- package/src/index.ts +7 -0
|
@@ -204,6 +204,8 @@ export interface DynamicSelectFieldProps {
|
|
|
204
204
|
* host's create modal renders them locked. Pairs with `createDefaults`.
|
|
205
205
|
*/
|
|
206
206
|
createLockedFields?: string[]
|
|
207
|
+
/** Paint trigger with destructive border when validation failed. */
|
|
208
|
+
invalid?: boolean
|
|
207
209
|
}
|
|
208
210
|
|
|
209
211
|
export function DynamicSelectField({
|
|
@@ -219,6 +221,7 @@ export function DynamicSelectField({
|
|
|
219
221
|
hideCreate = false,
|
|
220
222
|
createDefaults,
|
|
221
223
|
createLockedFields,
|
|
224
|
+
invalid = false,
|
|
222
225
|
}: DynamicSelectFieldProps) {
|
|
223
226
|
const { t } = useTranslation()
|
|
224
227
|
const ph = (fallback: string) =>
|
|
@@ -398,7 +401,11 @@ export function DynamicSelectField({
|
|
|
398
401
|
aria-expanded={open}
|
|
399
402
|
id={field.key}
|
|
400
403
|
disabled={blockedByDependency}
|
|
401
|
-
|
|
404
|
+
aria-invalid={invalid || undefined}
|
|
405
|
+
className={
|
|
406
|
+
'min-w-0 flex-1 justify-between font-normal' +
|
|
407
|
+
(invalid ? ' border-destructive ring-1 ring-destructive/30' : '')
|
|
408
|
+
}
|
|
402
409
|
data-empty={!value}
|
|
403
410
|
data-depends-blocked={blockedByDependency ? '' : undefined}
|
|
404
411
|
>
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Shared helpers for painting + toasting client/server field validation so
|
|
2
|
+
// action modals and DynamicRecordDialog stay in lockstep.
|
|
3
|
+
import type { ActionFieldDef } from './types'
|
|
4
|
+
import type { Translate } from './server-error'
|
|
5
|
+
|
|
6
|
+
function itemFieldsOf(field: ActionFieldDef): ActionFieldDef[] {
|
|
7
|
+
const raw = field.itemFields ?? (field as { item_fields?: ActionFieldDef[] }).item_fields
|
|
8
|
+
return Array.isArray(raw) ? raw : []
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function translateLabel(raw: string | undefined, t?: Translate): string {
|
|
12
|
+
if (!raw) return ''
|
|
13
|
+
if (!t) return raw
|
|
14
|
+
return t(raw, { defaultValue: raw })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function humanizeToken(key: string): string {
|
|
18
|
+
return key.replace(/[._-]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a validation path (`customer_id`, `lines.0.unit_price`) to a
|
|
23
|
+
* human label using the action/modal field schema. Nested line-item paths
|
|
24
|
+
* become "Renglones → Precio unitario (fila 1)".
|
|
25
|
+
*/
|
|
26
|
+
export function labelForValidationPath(
|
|
27
|
+
path: string,
|
|
28
|
+
fields: readonly ActionFieldDef[] | undefined,
|
|
29
|
+
t?: Translate,
|
|
30
|
+
): string {
|
|
31
|
+
if (!path) return ''
|
|
32
|
+
const parts = path.split('.')
|
|
33
|
+
if (!fields?.length) return humanizeToken(path)
|
|
34
|
+
|
|
35
|
+
let cursor: readonly ActionFieldDef[] | undefined = fields
|
|
36
|
+
const bits: string[] = []
|
|
37
|
+
let i = 0
|
|
38
|
+
while (i < parts.length && cursor) {
|
|
39
|
+
const seg = parts[i]!
|
|
40
|
+
if (/^\d+$/.test(seg)) {
|
|
41
|
+
const row = Number(seg) + 1
|
|
42
|
+
const nextKey = parts[i + 1]
|
|
43
|
+
if (nextKey && cursor) {
|
|
44
|
+
const col = cursor.find((f) => f.key === nextKey)
|
|
45
|
+
const colLabel = translateLabel(col?.label, t) || humanizeToken(nextKey)
|
|
46
|
+
bits.push(`${colLabel} (fila ${row})`)
|
|
47
|
+
i += 2
|
|
48
|
+
cursor = undefined
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
bits.push(`fila ${row}`)
|
|
52
|
+
i += 1
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
const field = cursor.find((f) => f.key === seg)
|
|
56
|
+
if (!field) {
|
|
57
|
+
bits.push(humanizeToken(parts.slice(i).join('.')))
|
|
58
|
+
break
|
|
59
|
+
}
|
|
60
|
+
bits.push(translateLabel(field.label, t) || humanizeToken(seg))
|
|
61
|
+
const nested = itemFieldsOf(field)
|
|
62
|
+
cursor = nested.length ? nested : undefined
|
|
63
|
+
i += 1
|
|
64
|
+
if (!cursor && i < parts.length) {
|
|
65
|
+
bits.push(humanizeToken(parts.slice(i).join('.')))
|
|
66
|
+
break
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return bits.join(' → ')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Build `{path: label}` covering scalar fields and `parent.N.child` templates
|
|
73
|
+
* used by localizeFieldErrorMap (exact keys win when present). */
|
|
74
|
+
export function labelsForValidationFields(
|
|
75
|
+
fields: readonly ActionFieldDef[] | undefined,
|
|
76
|
+
t?: Translate,
|
|
77
|
+
): Record<string, string> {
|
|
78
|
+
const out: Record<string, string> = {}
|
|
79
|
+
if (!fields) return out
|
|
80
|
+
for (const f of fields) {
|
|
81
|
+
if (!f.key) continue
|
|
82
|
+
out[f.key] = translateLabel(f.label, t) || humanizeToken(f.key)
|
|
83
|
+
for (const child of itemFieldsOf(f)) {
|
|
84
|
+
if (!child.key) continue
|
|
85
|
+
out[`${f.key}.${child.key}`] = translateLabel(child.label, t) || humanizeToken(child.key)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Toast description: one "Label: message" line per error. Re-resolves labels
|
|
93
|
+
* for dotted line-item paths so operators see e.g.
|
|
94
|
+
* "Precio unitario (fila 1): es obligatorio" instead of a bare headline.
|
|
95
|
+
*/
|
|
96
|
+
export function formatFieldErrorsDescription(
|
|
97
|
+
errors: Record<string, string>,
|
|
98
|
+
fields?: readonly ActionFieldDef[],
|
|
99
|
+
t?: Translate,
|
|
100
|
+
): string | undefined {
|
|
101
|
+
const entries = Object.entries(errors)
|
|
102
|
+
if (!entries.length) return undefined
|
|
103
|
+
return entries
|
|
104
|
+
.map(([path, msg]) => {
|
|
105
|
+
const label = labelForValidationPath(path, fields, t)
|
|
106
|
+
// localizeFieldIssue already prefixes "{{label}}: …" — avoid
|
|
107
|
+
// "Label: Label: msg" when the message already starts with the label.
|
|
108
|
+
if (label && msg.toLowerCase().startsWith(label.toLowerCase())) return msg
|
|
109
|
+
return label ? `${label}: ${msg}` : msg
|
|
110
|
+
})
|
|
111
|
+
.join(' · ')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Drop `key` and every `key.*` / `key.N.*` entry after the operator edits that field. */
|
|
115
|
+
export function clearFieldErrorTree(
|
|
116
|
+
prev: Record<string, string>,
|
|
117
|
+
key: string,
|
|
118
|
+
): Record<string, string> {
|
|
119
|
+
if (!prev[key] && !Object.keys(prev).some((k) => k.startsWith(`${key}.`))) return prev
|
|
120
|
+
const next: Record<string, string> = {}
|
|
121
|
+
for (const [k, v] of Object.entries(prev)) {
|
|
122
|
+
if (k === key || k.startsWith(`${key}.`)) continue
|
|
123
|
+
next[k] = v
|
|
124
|
+
}
|
|
125
|
+
return next
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Slice `parent.0.child` errors into `{ "0.child": msg }` for DynamicLineItems. */
|
|
129
|
+
export function lineItemErrorsFor(
|
|
130
|
+
parentKey: string,
|
|
131
|
+
errors: Record<string, string> | undefined,
|
|
132
|
+
): Record<string, string> {
|
|
133
|
+
if (!errors) return {}
|
|
134
|
+
const prefix = `${parentKey}.`
|
|
135
|
+
const out: Record<string, string> = {}
|
|
136
|
+
for (const [k, v] of Object.entries(errors)) {
|
|
137
|
+
if (k.startsWith(prefix)) out[k.slice(prefix.length)] = v
|
|
138
|
+
}
|
|
139
|
+
return out
|
|
140
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,13 @@ export {
|
|
|
40
40
|
type ValidationSpec,
|
|
41
41
|
} from './validator'
|
|
42
42
|
export { VALIDATION_CATALOGS, validationCatalog, validationMessageKey } from './validation-catalog'
|
|
43
|
+
export {
|
|
44
|
+
labelForValidationPath,
|
|
45
|
+
labelsForValidationFields,
|
|
46
|
+
formatFieldErrorsDescription,
|
|
47
|
+
clearFieldErrorTree,
|
|
48
|
+
lineItemErrorsFor,
|
|
49
|
+
} from './field-validation-ui'
|
|
43
50
|
export * from './dynamic-table'
|
|
44
51
|
export {
|
|
45
52
|
DynamicKanban,
|