@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.
Files changed (34) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/action-modal-dispatcher.d.ts.map +1 -1
  3. package/dist/action-modal-dispatcher.js +12 -3
  4. package/dist/business/document-editor-model.d.ts +33 -1
  5. package/dist/business/document-editor-model.d.ts.map +1 -1
  6. package/dist/business/document-editor-model.js +105 -3
  7. package/dist/business/document-editor.d.ts.map +1 -1
  8. package/dist/business/document-editor.js +54 -25
  9. package/dist/dynamic-form-schema.d.ts +3 -26
  10. package/dist/dynamic-form-schema.d.ts.map +1 -1
  11. package/dist/dynamic-form-schema.js +4 -56
  12. package/dist/dynamic-row-actions.d.ts +6 -0
  13. package/dist/dynamic-row-actions.d.ts.map +1 -1
  14. package/dist/dynamic-row-actions.js +9 -4
  15. package/dist/option-when.d.ts +27 -0
  16. package/dist/option-when.d.ts.map +1 -0
  17. package/dist/option-when.js +56 -0
  18. package/dist/validator.d.ts.map +1 -1
  19. package/dist/validator.js +17 -7
  20. package/package.json +1 -1
  21. package/src/__tests__/document-editor-model.test.ts +14 -1
  22. package/src/__tests__/document-editor.test.tsx +23 -1
  23. package/src/__tests__/invoice-document-editor.test.tsx +4 -3
  24. package/src/__tests__/option-when-validation.test.tsx +87 -0
  25. package/src/__tests__/r6-editor-option-when.test.tsx +104 -0
  26. package/src/__tests__/r6-editor-source-seeds.test.tsx +137 -0
  27. package/src/__tests__/row-action-link-navigation.test.ts +10 -6
  28. package/src/action-modal-dispatcher.tsx +14 -5
  29. package/src/business/document-editor-model.ts +116 -3
  30. package/src/business/document-editor.tsx +59 -19
  31. package/src/dynamic-form-schema.ts +5 -55
  32. package/src/dynamic-row-actions.tsx +12 -4
  33. package/src/option-when.ts +61 -0
  34. 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(url: string): { to: string; search?: Record<string, string> } {
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
- if (i < 0) return { to: url }
50
- return { to: url.slice(0, i), search: Object.fromEntries(new URLSearchParams(url.slice(i + 1))) }
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) => encodeURIComponent(String(row[field] ?? '')))
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
- function specFromField(field: ActionFieldDef): ValidationSpec {
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) spec.options = field.options.map(o => String(o.value))
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
- walk(itemFieldsOf(field), obj, `${path}.${i}`, bag)
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
  }