@asteby/metacore-runtime-react 49.2.1 → 49.3.1
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 +30 -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 +12 -1
- package/dist/business/document-editor-model.d.ts.map +1 -1
- package/dist/business/document-editor-model.js +49 -3
- package/dist/business/document-editor.d.ts.map +1 -1
- package/dist/business/document-editor.js +14 -5
- package/dist/document-form-dialog.d.ts.map +1 -1
- package/dist/document-form-dialog.js +20 -12
- 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/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__/document-form-dialog.no-stale-flash.test.tsx +169 -0
- 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/action-modal-dispatcher.tsx +14 -5
- package/src/business/document-editor-model.ts +48 -3
- package/src/business/document-editor.tsx +15 -4
- package/src/document-form-dialog.tsx +20 -11
- package/src/dynamic-form-schema.ts +5 -55
- package/src/option-when.ts +61 -0
- package/src/validator.ts +17 -5
|
@@ -48,6 +48,8 @@ import {
|
|
|
48
48
|
splitEditorFields,
|
|
49
49
|
toOpenDocuments,
|
|
50
50
|
friendlyOptionLabel,
|
|
51
|
+
gatedOptionFixes,
|
|
52
|
+
gatedOptions,
|
|
51
53
|
withFriendlyOptions,
|
|
52
54
|
type EditorIssue,
|
|
53
55
|
type SeedLabel,
|
|
@@ -215,6 +217,15 @@ export function DocumentEditor({
|
|
|
215
217
|
const fieldKeys = useRef<string[]>([])
|
|
216
218
|
fieldKeys.current = allFields.map((f) => f.key)
|
|
217
219
|
|
|
220
|
+
// Opciones condicionadas (`options[].when`): al cambiar el campo del que
|
|
221
|
+
// dependen —a mano, por la contraparte o por «Cargar desde…»— el valor que
|
|
222
|
+
// dejó de aplicar se ajusta solo (método PPD → forma de pago 99) y viaja así
|
|
223
|
+
// en el alta, no solo en el diálogo de timbrado.
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
const fixes = gatedOptionFixes(allFields, header)
|
|
226
|
+
if (Object.keys(fixes).length > 0) setHeader((h) => ({ ...h, ...gatedOptionFixes(allFields, h) }))
|
|
227
|
+
}, [allFields, header])
|
|
228
|
+
|
|
218
229
|
// Contraparte: tarjeta + defaults de las extensiones con la misma clave.
|
|
219
230
|
const partyId = type.party ? header[type.party.field] : undefined
|
|
220
231
|
useEffect(() => {
|
|
@@ -435,11 +446,11 @@ export function DocumentEditor({
|
|
|
435
446
|
setSourceNote(
|
|
436
447
|
loaded.length === 0 && covered > 0
|
|
437
448
|
? t('documentEditor.source_all_covered', {
|
|
438
|
-
defaultValue: '
|
|
449
|
+
defaultValue: 'Ese documento ya se registró completo; no queda nada pendiente.',
|
|
439
450
|
})
|
|
440
451
|
: covered > 0
|
|
441
452
|
? t('documentEditor.source_loaded_partial', {
|
|
442
|
-
defaultValue: '{{count}} renglones con lo
|
|
453
|
+
defaultValue: '{{count}} renglones con lo que falta · {{covered}} ya completos (omitidos)',
|
|
443
454
|
count: loaded.length,
|
|
444
455
|
covered,
|
|
445
456
|
})
|
|
@@ -579,7 +590,7 @@ export function DocumentEditor({
|
|
|
579
590
|
<FieldLabel htmlFor={field.key} required={field.required}>
|
|
580
591
|
{tl(field.label)}
|
|
581
592
|
</FieldLabel>
|
|
582
|
-
{renderField(withFriendlyOptions(field), header[field.key], (v: any) => updateField(field.key, v), header, fieldRecord, fieldErrors)}
|
|
593
|
+
{renderField(withFriendlyOptions(gatedOptions(field, header)), header[field.key], (v: any) => updateField(field.key, v), header, fieldRecord, fieldErrors)}
|
|
583
594
|
{fieldErrors[field.key] && <p className="mt-1 text-xs text-destructive">{fieldErrors[field.key]}</p>}
|
|
584
595
|
</FieldCell>
|
|
585
596
|
) : null
|
|
@@ -654,7 +665,7 @@ export function DocumentEditor({
|
|
|
654
665
|
{sources.length > 1 && (
|
|
655
666
|
<div
|
|
656
667
|
role="radiogroup"
|
|
657
|
-
aria-label={t('documentEditor.source_kind', { defaultValue: '
|
|
668
|
+
aria-label={t('documentEditor.source_kind', { defaultValue: 'Documento de origen' })}
|
|
658
669
|
className="inline-flex flex-wrap gap-1 rounded-md border p-1"
|
|
659
670
|
>
|
|
660
671
|
{sources.map((s) => (
|
|
@@ -211,17 +211,26 @@ export function DocumentFormDialog({
|
|
|
211
211
|
)
|
|
212
212
|
const lineCfg = type ? editorLinesConfig(type, forms) : undefined
|
|
213
213
|
|
|
214
|
-
// Reset every time the dialog (re)opens.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
214
|
+
// Reset every time the dialog (re)opens — DURING render, not in an effect.
|
|
215
|
+
// The host keeps this dialog mounted while closed (and across views whose
|
|
216
|
+
// forms change, e.g. the same /m/$model route going Facturas → Notas de
|
|
217
|
+
// crédito); an effect reset ran after the first open frame was already
|
|
218
|
+
// painted with the previous type/step, so the wizard (or the previous
|
|
219
|
+
// type's editor) flashed before the right DocumentEditor (QA Pitsline r6).
|
|
220
|
+
const openKey = open ? `${initialType ?? ''}\u0000${single?.key ?? ''}` : null
|
|
221
|
+
const [resetFor, setResetFor] = useState<string | null>(null)
|
|
222
|
+
if (openKey !== resetFor) {
|
|
223
|
+
setResetFor(openKey)
|
|
224
|
+
if (openKey !== null) {
|
|
225
|
+
const start = initialType ?? single?.key ?? null
|
|
226
|
+
setTypeKey(start)
|
|
227
|
+
setStep(start ? 'fields' : 'type')
|
|
228
|
+
setLines([])
|
|
229
|
+
setFieldErrors({})
|
|
230
|
+
setServerLineErrors({})
|
|
231
|
+
setFormError(undefined)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
225
234
|
|
|
226
235
|
// Seed field defaults for the chosen type.
|
|
227
236
|
useEffect(() => {
|
|
@@ -4,8 +4,13 @@
|
|
|
4
4
|
import { z, type ZodTypeAny } from 'zod'
|
|
5
5
|
import type { ActionFieldDef, FieldOptionsConfig, OptionDef, VisibleWhen } from './types'
|
|
6
6
|
import { fieldValidationOf } from './validator'
|
|
7
|
+
import { applyOptionWhen, getDependsOn } from './option-when'
|
|
7
8
|
import { resolveValidatorToken } from './use-org-config-bridge'
|
|
8
9
|
|
|
10
|
+
// Viven en option-when.ts (el validador también los usa); se re-exportan aquí
|
|
11
|
+
// para no romper a quien los importaba de este módulo.
|
|
12
|
+
export { applyOptionWhen, getDependsOn }
|
|
13
|
+
|
|
9
14
|
/**
|
|
10
15
|
* Built-in validators the SDK knows how to apply by symbolic name. Apps
|
|
11
16
|
* that wire `OrgConfigProvider` map `$org.<key>` references to one of
|
|
@@ -389,18 +394,6 @@ export function fieldHasRef(field: ActionFieldDef): boolean {
|
|
|
389
394
|
return getFieldRef(field) !== undefined
|
|
390
395
|
}
|
|
391
396
|
|
|
392
|
-
/**
|
|
393
|
-
* Resolves a field's cascade dependency — the key of another form field whose
|
|
394
|
-
* current value scopes this picker's options (`filter_value`). Tolerates the
|
|
395
|
-
* camelCase `dependsOn` (authored SDK shape) and the snake_case `depends_on`
|
|
396
|
-
* the kernel manifest serves. Returns the trimmed field key, or `undefined`
|
|
397
|
-
* when the field declares no dependency.
|
|
398
|
-
*/
|
|
399
|
-
export function getDependsOn(field: ActionFieldDef): string | undefined {
|
|
400
|
-
const dep = field.dependsOn ?? field.depends_on
|
|
401
|
-
if (typeof dep === 'string' && dep.trim() !== '') return dep.trim()
|
|
402
|
-
return undefined
|
|
403
|
-
}
|
|
404
397
|
|
|
405
398
|
/**
|
|
406
399
|
* Resolves the cascade `filter_value` for a field from the surrounding form
|
|
@@ -424,49 +417,6 @@ export function resolveDependsValue(
|
|
|
424
417
|
return String(raw)
|
|
425
418
|
}
|
|
426
419
|
|
|
427
|
-
/**
|
|
428
|
-
* Filters a STATIC enum's `options[]` by each option's `when` gate against the
|
|
429
|
-
* current form values. Pure — no React, no side effects.
|
|
430
|
-
*
|
|
431
|
-
* Rule per option:
|
|
432
|
-
* - No `when` → always included (retrocompat; existing enums untouched).
|
|
433
|
-
* - With `when`: the gating field is `when.field ?? dependsOn`. If neither is
|
|
434
|
-
* present the option is included (nothing to gate on). Otherwise the form's
|
|
435
|
-
* value for that field is compared AS STRING: included when (no `in`, or
|
|
436
|
-
* value ∈ `in`) AND (no `not_in`, or value ∉ `not_in`). Tolerates the
|
|
437
|
-
* snake_case `not_in` the kernel serves alongside camelCase `notIn`.
|
|
438
|
-
*
|
|
439
|
-
* `formValues` is the flat map of the surrounding form/row values the gating
|
|
440
|
-
* field is read from; `dependsOn` is the containing field's declared dependency
|
|
441
|
-
* used as the default gating field.
|
|
442
|
-
*/
|
|
443
|
-
export function applyOptionWhen(
|
|
444
|
-
options: OptionDef[] | undefined,
|
|
445
|
-
formValues: Record<string, any> | null | undefined,
|
|
446
|
-
dependsOn?: string,
|
|
447
|
-
): OptionDef[] {
|
|
448
|
-
if (!Array.isArray(options)) return []
|
|
449
|
-
return options.filter((opt) => {
|
|
450
|
-
const when = opt?.when
|
|
451
|
-
if (!when) return true
|
|
452
|
-
const gate = (typeof when.field === 'string' && when.field.trim() !== '')
|
|
453
|
-
? when.field.trim()
|
|
454
|
-
: dependsOn
|
|
455
|
-
if (!gate) return true
|
|
456
|
-
const raw = formValues ? formValues[gate] : undefined
|
|
457
|
-
const current = raw == null ? '' : String(raw)
|
|
458
|
-
const inList = when.in
|
|
459
|
-
const notIn = when.notIn ?? when.not_in
|
|
460
|
-
if (Array.isArray(inList) && inList.length > 0) {
|
|
461
|
-
if (!inList.some((v) => String(v) === current)) return false
|
|
462
|
-
}
|
|
463
|
-
if (Array.isArray(notIn) && notIn.length > 0) {
|
|
464
|
-
if (notIn.some((v) => String(v) === current)) return false
|
|
465
|
-
}
|
|
466
|
-
return true
|
|
467
|
-
})
|
|
468
|
-
}
|
|
469
|
-
|
|
470
420
|
/**
|
|
471
421
|
* Reads a field's `visible_when` predicate, tolerating the camelCase alias
|
|
472
422
|
* (`visibleWhen`) an app may author and the snake_case (`visible_when`) the
|
|
@@ -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
|
}
|