@asteby/metacore-runtime-react 49.2.0 → 49.3.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/CHANGELOG.md +35 -0
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +12 -3
- package/dist/business/document-editor-model.d.ts +33 -1
- package/dist/business/document-editor-model.d.ts.map +1 -1
- package/dist/business/document-editor-model.js +105 -3
- package/dist/business/document-editor.d.ts.map +1 -1
- package/dist/business/document-editor.js +54 -25
- package/dist/dynamic-form-schema.d.ts +3 -26
- package/dist/dynamic-form-schema.d.ts.map +1 -1
- package/dist/dynamic-form-schema.js +4 -56
- package/dist/dynamic-row-actions.d.ts +6 -0
- package/dist/dynamic-row-actions.d.ts.map +1 -1
- package/dist/dynamic-row-actions.js +9 -4
- package/dist/option-when.d.ts +27 -0
- package/dist/option-when.d.ts.map +1 -0
- package/dist/option-when.js +56 -0
- package/dist/validator.d.ts.map +1 -1
- package/dist/validator.js +17 -7
- package/package.json +1 -1
- package/src/__tests__/document-editor-model.test.ts +14 -1
- package/src/__tests__/document-editor.test.tsx +23 -1
- package/src/__tests__/invoice-document-editor.test.tsx +4 -3
- package/src/__tests__/option-when-validation.test.tsx +87 -0
- package/src/__tests__/r6-editor-option-when.test.tsx +104 -0
- package/src/__tests__/r6-editor-source-seeds.test.tsx +137 -0
- package/src/__tests__/row-action-link-navigation.test.ts +10 -6
- package/src/action-modal-dispatcher.tsx +14 -5
- package/src/business/document-editor-model.ts +116 -3
- package/src/business/document-editor.tsx +59 -19
- package/src/dynamic-form-schema.ts +5 -55
- package/src/dynamic-row-actions.tsx +12 -4
- package/src/option-when.ts +61 -0
- package/src/validator.ts +17 -5
|
@@ -44,12 +44,20 @@ import type { TableMetadata, ActionMetadata, RequiresAddon } from './types'
|
|
|
44
44
|
* Destino de una acción link. La query (`?create=…&from_id=…`) va como `search`:
|
|
45
45
|
* metida en `to` el router la tomaba como parte del path y la ruta no casaba.
|
|
46
46
|
*/
|
|
47
|
-
export function linkNavigation(
|
|
47
|
+
export function linkNavigation(
|
|
48
|
+
url: string,
|
|
49
|
+
): { to: '/m/$model'; params: { model: string }; search?: Record<string, string> } | { to: string; search?: Record<string, string> } {
|
|
48
50
|
const i = url.indexOf('?')
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
const path = i < 0 ? url : url.slice(0, i)
|
|
52
|
+
const search = i < 0 ? undefined : Object.fromEntries(new URLSearchParams(url.slice(i + 1)))
|
|
53
|
+
// /m/<tabla>?create=&from=&from_id= → ruta parametrizada (retest r6b: el menú
|
|
54
|
+
// «Nota de crédito» de la factura aterrizaba en la lista sin abrir el editor).
|
|
55
|
+
const m = path.match(/^\/m\/([^/?#]+)\/?$/)
|
|
56
|
+
if (m) return { to: '/m/$model', params: { model: m[1] }, search }
|
|
57
|
+
return search ? { to: path, search } : { to: path }
|
|
51
58
|
}
|
|
52
59
|
|
|
60
|
+
|
|
53
61
|
export interface UseDynamicRowActionsParams {
|
|
54
62
|
/** Model key as registered on the backend (e.g. "issue"). */
|
|
55
63
|
model: string
|
|
@@ -138,7 +146,7 @@ export function useDynamicRowActions({
|
|
|
138
146
|
}
|
|
139
147
|
const linkDef = metadata?.actions?.find((a) => a.key === action && a.type === 'link')
|
|
140
148
|
if (linkDef?.linkUrl) {
|
|
141
|
-
const url = linkDef.linkUrl.replace(/\{(\w+)\}/g, (_: string, field: string) =>
|
|
149
|
+
const url = linkDef.linkUrl.replace(/\{(\w+)\}/g, (_: string, field: string) => String(row[field] ?? ''))
|
|
142
150
|
navigate(linkNavigation(url) as any)
|
|
143
151
|
return
|
|
144
152
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// option-when.ts — opciones estáticas de un select condicionadas por el valor
|
|
2
|
+
// de un campo hermano (`options[].when`) y la dependencia declarada del campo
|
|
3
|
+
// (`dependsOn`). Módulo hoja: lo usan el formulario (dynamic-form-schema), el
|
|
4
|
+
// render compartido de campos (renderField) y el validador, sin ciclos.
|
|
5
|
+
import type { ActionFieldDef, OptionDef } from './types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolves a field's cascade dependency — the key of another form field whose
|
|
9
|
+
* current value scopes this picker's options (`filter_value`). Tolerates the
|
|
10
|
+
* camelCase `dependsOn` (authored SDK shape) and the snake_case `depends_on`
|
|
11
|
+
* the kernel manifest serves. Returns the trimmed field key, or `undefined`
|
|
12
|
+
* when the field declares no dependency.
|
|
13
|
+
*/
|
|
14
|
+
export function getDependsOn(field: ActionFieldDef): string | undefined {
|
|
15
|
+
const dep = field.dependsOn ?? field.depends_on
|
|
16
|
+
if (typeof dep === 'string' && dep.trim() !== '') return dep.trim()
|
|
17
|
+
return undefined
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Filters a STATIC enum's `options[]` by each option's `when` gate against the
|
|
22
|
+
* current form values. Pure — no React, no side effects.
|
|
23
|
+
*
|
|
24
|
+
* Rule per option:
|
|
25
|
+
* - No `when` → always included (retrocompat; existing enums untouched).
|
|
26
|
+
* - With `when`: the gating field is `when.field ?? dependsOn`. If neither is
|
|
27
|
+
* present the option is included (nothing to gate on). Otherwise the form's
|
|
28
|
+
* value for that field is compared AS STRING: included when (no `in`, or
|
|
29
|
+
* value ∈ `in`) AND (no `not_in`, or value ∉ `not_in`). Tolerates the
|
|
30
|
+
* snake_case `not_in` the kernel serves alongside camelCase `notIn`.
|
|
31
|
+
*
|
|
32
|
+
* `formValues` is the flat map of the surrounding form/row values the gating
|
|
33
|
+
* field is read from; `dependsOn` is the containing field's declared dependency
|
|
34
|
+
* used as the default gating field.
|
|
35
|
+
*/
|
|
36
|
+
export function applyOptionWhen(
|
|
37
|
+
options: OptionDef[] | undefined,
|
|
38
|
+
formValues: Record<string, any> | null | undefined,
|
|
39
|
+
dependsOn?: string,
|
|
40
|
+
): OptionDef[] {
|
|
41
|
+
if (!Array.isArray(options)) return []
|
|
42
|
+
return options.filter((opt) => {
|
|
43
|
+
const when = opt?.when
|
|
44
|
+
if (!when) return true
|
|
45
|
+
const gate = (typeof when.field === 'string' && when.field.trim() !== '')
|
|
46
|
+
? when.field.trim()
|
|
47
|
+
: dependsOn
|
|
48
|
+
if (!gate) return true
|
|
49
|
+
const raw = formValues ? formValues[gate] : undefined
|
|
50
|
+
const current = raw == null ? '' : String(raw)
|
|
51
|
+
const inList = when.in
|
|
52
|
+
const notIn = when.notIn ?? when.not_in
|
|
53
|
+
if (Array.isArray(inList) && inList.length > 0) {
|
|
54
|
+
if (!inList.some((v) => String(v) === current)) return false
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(notIn) && notIn.length > 0) {
|
|
57
|
+
if (notIn.some((v) => String(v) === current)) return false
|
|
58
|
+
}
|
|
59
|
+
return true
|
|
60
|
+
})
|
|
61
|
+
}
|
package/src/validator.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// the same keys.
|
|
5
5
|
import type { ActionFieldDef, FieldValidation } from './types'
|
|
6
6
|
import type { FieldIssue } from './server-error'
|
|
7
|
+
import { applyOptionWhen, getDependsOn } from './option-when'
|
|
7
8
|
|
|
8
9
|
const NIL_UUID = '00000000-0000-0000-0000-000000000000'
|
|
9
10
|
const NUMERIC_TYPES = new Set(['int', 'integer', 'bigint', 'decimal', 'numeric', 'number', 'float', 'double'])
|
|
@@ -207,7 +208,13 @@ function checkBuiltin(slug: string, value: unknown): FieldIssue | undefined {
|
|
|
207
208
|
}
|
|
208
209
|
}
|
|
209
210
|
|
|
210
|
-
|
|
211
|
+
/**
|
|
212
|
+
* `context` son los valores hermanos contra los que se evalúa el `when` de
|
|
213
|
+
* cada opción: una opción que su `when` descarta con el valor actual (forma de
|
|
214
|
+
* pago «99» con método PUE) es `invalid_option`, igual que una fuera de catálogo
|
|
215
|
+
* — y como la rechaza el kernel al guardar.
|
|
216
|
+
*/
|
|
217
|
+
function specFromField(field: ActionFieldDef, context?: Record<string, unknown>): ValidationSpec {
|
|
211
218
|
const v = fieldValidationOf(field)
|
|
212
219
|
const spec: ValidationSpec = {
|
|
213
220
|
required: !!field.required,
|
|
@@ -217,7 +224,10 @@ function specFromField(field: ActionFieldDef): ValidationSpec {
|
|
|
217
224
|
max: v.max,
|
|
218
225
|
custom: v.custom,
|
|
219
226
|
}
|
|
220
|
-
if (field.options?.length)
|
|
227
|
+
if (field.options?.length) {
|
|
228
|
+
const applicable = context ? applyOptionWhen(field.options, context, getDependsOn(field)) : field.options
|
|
229
|
+
spec.options = applicable.map(o => String(o.value))
|
|
230
|
+
}
|
|
221
231
|
return spec
|
|
222
232
|
}
|
|
223
233
|
|
|
@@ -238,7 +248,7 @@ export function validateValues(
|
|
|
238
248
|
values: Record<string, unknown>,
|
|
239
249
|
): Record<string, FieldIssue[]> {
|
|
240
250
|
const bag: Record<string, FieldIssue[]> = {}
|
|
241
|
-
walk(fields, values ?? {}, '', bag)
|
|
251
|
+
walk(fields, values ?? {}, '', bag, values ?? {})
|
|
242
252
|
return bag
|
|
243
253
|
}
|
|
244
254
|
|
|
@@ -247,6 +257,7 @@ function walk(
|
|
|
247
257
|
values: Record<string, unknown>,
|
|
248
258
|
prefix: string,
|
|
249
259
|
bag: Record<string, FieldIssue[]>,
|
|
260
|
+
context: Record<string, unknown>,
|
|
250
261
|
): void {
|
|
251
262
|
for (const field of fields) {
|
|
252
263
|
const key = field.key
|
|
@@ -261,11 +272,12 @@ function walk(
|
|
|
261
272
|
}
|
|
262
273
|
rows.forEach((row, i) => {
|
|
263
274
|
const obj = row && typeof row === 'object' ? (row as Record<string, unknown>) : {}
|
|
264
|
-
|
|
275
|
+
// Una celda puede depender de otra del renglón o del encabezado.
|
|
276
|
+
walk(itemFieldsOf(field), obj, `${path}.${i}`, bag, { ...context, ...obj })
|
|
265
277
|
})
|
|
266
278
|
continue
|
|
267
279
|
}
|
|
268
|
-
const issues = checkValue(raw, specFromField(field))
|
|
280
|
+
const issues = checkValue(raw, specFromField(field, context))
|
|
269
281
|
if (issues.length) bag[path] = issues
|
|
270
282
|
}
|
|
271
283
|
}
|