@asteby/metacore-runtime-react 37.0.4 → 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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 37.0.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 5f2a732: Action modal + line-items: preserve numeric zero (valueAsNumber), paint cell errors (`lines.0.*`), and toast a Label: message description for every field failure.
8
+
3
9
  ## 37.0.4
4
10
 
5
11
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AA4DA,OAAO,KAAK,EAAE,cAAc,EAAmC,MAAM,SAAS,CAAA;AAE9E,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAExB,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;AAiBhD,MAAM,WAAW,WAAW;IACxB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,SAAS,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1D;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,WAAW,CAM1D;AAsBD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,CAStE;AAGD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAkB3F;AASD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAY1D;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CASjE;AAUD,oEAAoE;AACpE,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,GAAG,OAAO,CAgBnF;AAED,wBAAgB,qBAAqB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,GACZ,EAAE,gBAAgB,sCAoElB"}
1
+ {"version":3,"file":"action-modal-dispatcher.d.ts","sourceRoot":"","sources":["../src/action-modal-dispatcher.tsx"],"names":[],"mappings":"AAmEA,OAAO,KAAK,EAAE,cAAc,EAAmC,MAAM,SAAS,CAAA;AAE9E,OAAO,EACH,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAExB,MAAM,sBAAsB,CAAA;AAE7B,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,CAAA;AAiBhD,MAAM,WAAW,WAAW;IACxB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B,SAAS,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1D;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,WAAW,CAM1D;AAsBD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,cAAc,CAStE;AAGD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAkB3F;AASD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAY1D;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CASjE;AAUD,oEAAoE;AACpE,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,GAAG,OAAO,CAgBnF;AAED,wBAAgB,qBAAqB,CAAC,EAClC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,KAAK,EACL,MAAM,EACN,QAAQ,EACR,SAAS,GACZ,EAAE,gBAAgB,sCAoElB"}
@@ -16,6 +16,7 @@ import { Loader2 } from 'lucide-react';
16
16
  import { toast } from 'sonner';
17
17
  import { toastServerError, toastServerSuccess, extractFieldErrors, localizeFieldErrorMap } from './server-error';
18
18
  import { validateValues, bagHasErrors } from './validator';
19
+ import { clearFieldErrorTree, formatFieldErrorsDescription, labelsForValidationFields, labelForValidationPath, lineItemErrorsFor, } from './field-validation-ui';
19
20
  import { validationCatalog } from './validation-catalog';
20
21
  import { useApi } from './api-context';
21
22
  import { DynamicIcon } from './dynamic-icon';
@@ -432,33 +433,43 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
432
433
  }, [open, action.fields, record]);
433
434
  const updateField = (key, value) => {
434
435
  setFormData((prev) => ({ ...prev, [key]: value }));
435
- setFieldErrors(prev => {
436
- if (!prev[key])
437
- return prev;
438
- const next = { ...prev };
439
- delete next[key];
440
- return next;
441
- });
436
+ setFieldErrors((prev) => clearFieldErrorTree(prev, key));
442
437
  };
443
438
  const lang = i18n.language;
444
439
  const handleActionError = (err) => {
440
+ const labels = labelsForValidationFields(action.fields, t);
445
441
  const localized = localizeActionFieldErrors(err, action.fields, t, lang);
446
442
  if (localized) {
447
- setFieldErrors(localized);
448
- toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }));
443
+ // Enrich labels for dotted line-item paths before toasting.
444
+ const withPathLabels = {};
445
+ for (const [path, msg] of Object.entries(localized)) {
446
+ withPathLabels[path] = msg;
447
+ if (!labels[path])
448
+ labels[path] = labelForValidationPath(path, action.fields, t);
449
+ }
450
+ setFieldErrors(withPathLabels);
451
+ toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }), {
452
+ description: formatFieldErrorsDescription(withPathLabels, action.fields, t),
453
+ });
449
454
  return;
450
455
  }
451
- toastServerError(err, { t, language: lang });
456
+ toastServerError(err, { t, language: lang, labels });
452
457
  };
453
458
  const execute = async () => {
454
459
  if (action.fields) {
455
460
  const bag = validateValues(action.fields, formData);
456
461
  if (bagHasErrors(bag)) {
457
- const labels = {};
458
- for (const f of action.fields)
459
- labels[f.key] = tl(f.label);
460
- setFieldErrors(localizeFieldErrorMap(bag, t, { labels, language: lang }));
461
- toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }));
462
+ const labels = labelsForValidationFields(action.fields, (k, o) => t(k, o));
463
+ // Exact dotted keys (`lines.0.qty`) need path-aware labels.
464
+ for (const path of Object.keys(bag)) {
465
+ if (!labels[path])
466
+ labels[path] = labelForValidationPath(path, action.fields, t);
467
+ }
468
+ const next = localizeFieldErrorMap(bag, t, { labels, language: lang });
469
+ setFieldErrors(next);
470
+ toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }), {
471
+ description: formatFieldErrorsDescription(next, action.fields, t),
472
+ });
462
473
  return;
463
474
  }
464
475
  }
@@ -502,7 +513,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
502
513
  const fullWidth = isLineItemsField(field) ||
503
514
  resolveWidget(field) === 'textarea' ||
504
515
  resolveWidget(field) === 'richtext';
505
- return (_jsxs(FieldCell, { fullWidth: fullWidth, children: [_jsx(FieldLabel, { htmlFor: field.key, required: field.required, children: tl(field.label) }), renderField(field, formData[field.key], (v) => updateField(field.key, v), formData, record), fieldErrors[field.key] && (_jsx("p", { className: "text-destructive text-xs mt-1", children: fieldErrors[field.key] }))] }, field.key));
516
+ return (_jsxs(FieldCell, { fullWidth: fullWidth, children: [_jsx(FieldLabel, { htmlFor: field.key, required: field.required, children: tl(field.label) }), renderField(field, formData[field.key], (v) => updateField(field.key, v), formData, record, fieldErrors), fieldErrors[field.key] && (_jsx("p", { className: "text-destructive text-xs mt-1", children: fieldErrors[field.key] }))] }, field.key));
506
517
  }), embedRelations && relations.length > 0 && (_jsx(FieldCell, { fullWidth: true, children: _jsx(DynamicRelations, { record: record, relations: relations, lineSubtable: true, embedOnly: true }) }))] }) }), _jsxs(DialogFooter, { className: "shrink-0", children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: executing, children: t('common.cancel') }), _jsxs(Button, { onClick: execute, disabled: executing, style: action.color ? { backgroundColor: action.color, color: 'white' } : undefined, children: [executing ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : _jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), tl(action.label)] })] })] }) }));
507
518
  }
508
519
  // buildFieldDefaults seeds formData for a set of action fields, honoring the
@@ -648,24 +659,28 @@ function renderField(field, value, onChange,
648
659
  // header picker) resolve a `dependsOn` reference against sibling header
649
660
  // fields. Omitted by callers that have no surrounding form (the field is
650
661
  // then treated as having no resolvable dependency).
651
- formValues, record) {
662
+ formValues, record, fieldErrors) {
652
663
  // Repeatable line-items group → row grid (value is an array of row objects).
653
664
  // The header form values flow in so a cell can depend on a header field.
654
665
  if (isLineItemsField(field)) {
655
- return _jsx(DynamicLineItems, { field: applyPrefillLock(field), value: value, onChange: onChange, formValues: formValues });
666
+ return (_jsx(DynamicLineItems, { field: applyPrefillLock(field), value: value, onChange: onChange, formValues: formValues, errors: lineItemErrorsFor(field.key, fieldErrors) }));
656
667
  }
657
668
  // Resolve the widget the same way DynamicForm does (explicit widget wins,
658
669
  // else inferred from type) so action modals and the standalone form stay in
659
670
  // lockstep — previously this switch keyed off `field.type` and silently
660
671
  // dropped `dynamic_select` to a plain text input.
661
672
  const widget = resolveWidget(field);
673
+ const invalid = !!(fieldErrors && fieldErrors[field.key]);
674
+ const invalidCls = invalid
675
+ ? 'border-destructive ring-1 ring-destructive/30 focus-visible:ring-destructive'
676
+ : '';
662
677
  if (widget === 'dynamic_select') {
663
678
  // A header-level dynamic_select may itself depend on another header
664
679
  // field; resolve its filter_value from the form context.
665
680
  const dependsValue = getDependsOn(field)
666
681
  ? resolveDependsValue(field, formValues)
667
682
  : undefined;
668
- return (_jsx(DynamicSelectField, { field: field, value: value, onChange: onChange, dependsValue: dependsValue, seedOption: seedOptionFromRecord(field, value, record) }));
683
+ return (_jsx(DynamicSelectField, { field: field, value: value, onChange: onChange, dependsValue: dependsValue, seedOption: seedOptionFromRecord(field, value, record), invalid: invalid }));
669
684
  }
670
685
  // File upload → themed picker that POSTs the file to the host upload
671
686
  // endpoint and stores the returned url/path. Kept in sync with DynamicForm.
@@ -674,18 +689,21 @@ formValues, record) {
674
689
  }
675
690
  switch (widget) {
676
691
  case 'textarea':
677
- return _jsx(Textarea, { id: field.key, value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder });
692
+ return _jsx(Textarea, { id: field.key, value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, "aria-invalid": invalid || undefined, className: invalidCls || undefined });
678
693
  case 'select':
679
- return (_jsxs(Select, { value: value || '', onValueChange: onChange, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: field.options?.map((opt) => _jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value)) })] }));
694
+ return (_jsxs(Select, { value: value || '', onValueChange: onChange, children: [_jsx(SelectTrigger, { className: 'w-full' + (invalidCls ? ` ${invalidCls}` : ''), "aria-invalid": invalid || undefined, children: _jsx(SelectValue, { placeholder: field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: field.options?.map((opt) => _jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value)) })] }));
680
695
  case 'switch':
681
696
  return _jsx(Switch, { id: field.key, checked: !!value, onCheckedChange: onChange });
682
697
  case 'number':
683
- return _jsx(Input, { id: field.key, type: "number", value: value ?? '', onChange: (e) => onChange(e.target.valueAsNumber || ''), placeholder: field.placeholder });
698
+ return _jsx(Input, { id: field.key, type: "number", value: value ?? '', onChange: (e) => {
699
+ const n = e.target.valueAsNumber;
700
+ onChange(e.target.value === '' || !Number.isFinite(n) ? '' : n);
701
+ }, placeholder: field.placeholder, "aria-invalid": invalid || undefined, className: invalidCls || undefined });
684
702
  case 'date':
685
703
  // Modern shadcn Calendar in a Popover (portaled, never clipped by the
686
704
  // modal) instead of the native, dated, easily-cut <input type=date>.
687
705
  return _jsx(DynamicDateField, { field: field, value: value, onChange: onChange });
688
706
  default:
689
- return _jsx(Input, { id: field.key, type: field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text', value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder });
707
+ return _jsx(Input, { id: field.key, type: field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text', value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, "aria-invalid": invalid || undefined, className: invalidCls || undefined });
690
708
  }
691
709
  }
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AA6C1C,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAEjF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAoB3C,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAEnE,OAAO,EAAqC,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAK1F,YAAY,EAAE,WAAW,EAAE,CAAA;AAE3B,MAAM,WAAW,WAAW;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,QAAQ;IACrB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,CAAA;IACpH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAA;IACvB,YAAY,CAAC,EAAE,GAAG,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;;OAQG;IACH,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACjC;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,SAAS,EAAE,CAAA;IACxB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,SAAS,EAAE,CAAA;IACzB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,WAAW,CAAA;IAC1B,0CAA0C;IAC1C,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACnB;AA0CD,MAAM,WAAW,wBAAwB;IACrC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC;;;;OAIG;IACH,YAAY,CAAC,EAAE,QAAQ,EAAE,CAAA;IACzB,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;2DAEuD;IACvD,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IAClF;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAA;IACpG;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC9B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAA;IAC1C;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AASD,wBAAgB,WAAW,CAAC,KAAK,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAK1D;AAaD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAG,GAAG,CAgBtE;AAID,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,GAAG,SAAS,EAAE,GAAG,SAAS,CAExE;AAQD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CASrE;AASD,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,GAAG,cAAc,GAAG,IAAI,CAa5F;AA+GD,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAUjE;AAaD,wBAAgB,mBAAmB,CAC/B,MAAM,EAAE,QAAQ,EAAE,GAAG,SAAS,EAC9B,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,EAChC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACjC,QAAQ,EAAE,CAOZ;AAUD,wBAAgB,sBAAsB,CAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,MAAM,EAAE,QAAQ,EAAE,GAAG,SAAS,EAC9B,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GACjC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CASrB;AAiBD,wBAAgB,mBAAmB,CAAC,EAChC,IAAI,EACJ,YAAY,EACZ,sBAAsB,EACtB,YAAY,EACZ,IAAI,EACJ,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,YAAY,EACZ,MAAM,EACN,QAAQ,EACR,MAAM,EACN,cAAc,EACd,aAAa,EACb,WAA8B,EAC9B,QAAQ,EACR,QAAQ,EACR,QAAQ,GACX,EAAE,wBAAwB,+BAojB1B;AAuED,wBAAgB,iBAAiB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,GAAG,CAAA;CAAE,+BAelF;AA+GD,wBAAgB,SAAS,CAAC,EACtB,KAAK,EACL,KAAK,EAAE,QAAQ,EACf,MAAM,EACN,WAAW,EAAE,eAAe,EAC5B,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,YAAY,GACzB,EAAE;IACC,KAAK,EAAE,QAAQ,CAAA;IACf,KAAK,EAAE,GAAG,CAAA;IACV,MAAM,EAAE,GAAG,CAAA;IACX,mFAAmF;IACnF,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB,+BA6SA;AAwLD,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE;IACnE,KAAK,EAAE,QAAQ,CAAA;IACf,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC5B,iFAAiF;IACjF,MAAM,CAAC,EAAE,GAAG,CAAA;IACZ,8EAA8E;IAC9E,OAAO,CAAC,EAAE,OAAO,CAAA;CACpB,+BAiNA"}
1
+ {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
@@ -556,11 +556,14 @@ export function DynamicRecordDialog({ open, onOpenChange, nestedInlineCreateSelf
556
556
  labels[f.key] = f.label;
557
557
  const next = localizeFieldErrorMap(bag, t, { labels });
558
558
  setFieldErrors(next);
559
- const visibleKeys = new Set(visible.map(f => f.key));
560
- const orphans = Object.entries(next).filter(([k]) => !visibleKeys.has(k));
561
- const description = orphans.length
562
- ? orphans.map(([k, msg]) => `${labelForKey(k)}: ${msg}`).join(' · ')
563
- : undefined;
559
+ const description = Object.entries(next)
560
+ .map(([k, msg]) => {
561
+ const label = labels[k] || labelForKey(k);
562
+ return msg.toLowerCase().startsWith(String(label).toLowerCase())
563
+ ? msg
564
+ : `${label}: ${msg}`;
565
+ })
566
+ .join(' · ');
564
567
  toast.error(t('dynamic.validation_failed', { defaultValue: 'Revisa los campos marcados' }), description ? { description } : undefined);
565
568
  return;
566
569
  }
@@ -238,7 +238,10 @@ function FieldRenderer({ field, value, onChange, initialValues, effectiveOptions
238
238
  case 'switch':
239
239
  return _jsx(Switch, { id: field.key, checked: !!value, onCheckedChange: onChange });
240
240
  case 'number':
241
- return _jsx(Input, { id: field.key, type: "number", value: value ?? '', onChange: (e) => onChange(e.target.valueAsNumber || ''), placeholder: field.placeholder });
241
+ return _jsx(Input, { id: field.key, type: "number", value: value ?? '', onChange: (e) => {
242
+ const n = e.target.valueAsNumber;
243
+ onChange(e.target.value === '' || !Number.isFinite(n) ? '' : n);
244
+ }, placeholder: field.placeholder });
242
245
  case 'date':
243
246
  return _jsx(DynamicDateField, { field: field, value: value, onChange: onChange });
244
247
  default:
@@ -263,7 +266,14 @@ function ScannableInput({ field, value, onChange, type, }) {
263
266
  // degrada con un mensaje ("usá un lector físico / escribí a mano") cuando no
264
267
  // hay soporte, así que mostrar el botón es seguro y consistente.
265
268
  const scanEnabled = !!(field.scan ?? field.scannable);
266
- const input = (_jsx(Input, { id: field.key, type: type, value: value ?? '', onChange: (e) => onChange(type === 'number' ? e.target.valueAsNumber || '' : e.target.value), placeholder: field.placeholder }));
269
+ const input = (_jsx(Input, { id: field.key, type: type, value: value ?? '', onChange: (e) => {
270
+ if (type !== 'number') {
271
+ onChange(e.target.value);
272
+ return;
273
+ }
274
+ const n = e.target.valueAsNumber;
275
+ onChange(e.target.value === '' || !Number.isFinite(n) ? '' : n);
276
+ }, placeholder: field.placeholder }));
267
277
  if (!scanEnabled)
268
278
  return input;
269
279
  return (_jsxs("div", { className: "flex w-full min-w-0 items-center gap-1.5", children: [_jsx("div", { className: "min-w-0 flex-1", children: input }), _jsx(Button, { type: "button", variant: "outline", size: "icon", className: "size-9 shrink-0", onClick: () => setScanOpen(true), title: "Escanear con la c\u00E1mara", "aria-label": "Escanear c\u00F3digo de barras con la c\u00E1mara", children: _jsx(ScanLine, { className: "size-4" }) }), _jsx(BarcodeScanner, { open: scanOpen, onClose: () => setScanOpen(false), onDetected: (code) => onChange(code),
@@ -10,6 +10,11 @@ export interface DynamicLineItemsProps {
10
10
  * (e.g. `source_warehouse_id`), not just a sibling cell on the same row.
11
11
  */
12
12
  formValues?: Record<string, any>;
13
+ /**
14
+ * Localized validation messages keyed as `rowIndex.columnKey` (e.g.
15
+ * `0.unit_price`). Painted as destructive borders + under-cell text.
16
+ */
17
+ errors?: Record<string, string>;
13
18
  }
14
- export declare function DynamicLineItems({ field, value, onChange, disabled, formValues }: DynamicLineItemsProps): import("react").JSX.Element;
19
+ export declare function DynamicLineItems({ field, value, onChange, disabled, formValues, errors }: DynamicLineItemsProps): import("react").JSX.Element;
15
20
  //# sourceMappingURL=dynamic-line-items.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-line-items.d.ts","sourceRoot":"","sources":["../src/dynamic-line-items.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAgB7C,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,EAAE,GAAG,SAAS,CAAA;IACxB,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACnC;AAkBD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAgB,EAAE,UAAU,EAAE,EAAE,qBAAqB,+BA6O/G"}
1
+ {"version":3,"file":"dynamic-line-items.d.ts","sourceRoot":"","sources":["../src/dynamic-line-items.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAgB7C,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,EAAE,GAAG,SAAS,CAAA;IACxB,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAChC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAClC;AAkBD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAgB,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,qBAAqB,+BA4PvH"}
@@ -26,9 +26,10 @@ function emptyRow(itemFields) {
26
26
  }
27
27
  return row;
28
28
  }
29
- export function DynamicLineItems({ field, value, onChange, disabled = false, formValues }) {
29
+ export function DynamicLineItems({ field, value, onChange, disabled = false, formValues, errors }) {
30
30
  const itemFields = getItemFields(field);
31
31
  const rows = Array.isArray(value) ? value : [];
32
+ const errMap = errors ?? {};
32
33
  // `lock_rows` fixes the row set: no add-row button, no per-row delete. Rows
33
34
  // stay editable cell-by-cell. Snake_case is what the kernel serves; tolerate
34
35
  // the camelCase alias too. Derived once.
@@ -64,8 +65,14 @@ export function DynamicLineItems({ field, value, onChange, disabled = false, for
64
65
  updateCell(idx, key, cellValue);
65
66
  };
66
67
  const totalCols = itemFields.filter((c) => c.total);
67
- return (_jsxs("div", { className: "grid gap-2", "data-widget": "line_items", children: [_jsxs("div", { className: "grid gap-2 sm:hidden", children: [rows.length === 0 && (_jsx("div", { className: "text-muted-foreground rounded-md border px-3 py-4 text-center text-sm", children: "Sin renglones" })), rows.map((row, idx) => (_jsxs("div", { className: "rounded-md border p-3", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsxs("span", { className: "text-muted-foreground text-xs font-medium", children: ["Rengl\u00F3n ", idx + 1] }), !lockRows && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-7", onClick: () => removeRow(idx), disabled: disabled, "aria-label": "Eliminar rengl\u00F3n", children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }))] }), _jsx("div", { className: "grid gap-2.5", children: itemFields.map((col) => (_jsxs("div", { className: "grid gap-1", children: [_jsxs("span", { className: "text-xs font-medium", children: [col.label, col.required && (_jsx("span", { className: "ml-1 text-red-500", children: "*" }))] }), _jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), disabled: disabled, formValues: formValues, rowValues: row })] }, col.key))) })] }, idx))), hasTotals && rows.length > 0 && (_jsx("div", { className: "bg-muted/30 grid gap-1.5 rounded-md border p-3", children: totalCols.map((col) => (_jsxs("div", { className: "flex items-center justify-between text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: col.label }), _jsx("span", { className: "font-semibold tabular-nums", children: fmtNumber(totals[col.key] ?? 0) })] }, col.key))) }))] }), _jsx("div", { className: "hidden overflow-x-auto rounded-md border sm:block", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50", children: _jsxs("tr", { children: [itemFields.map((col) => (_jsxs("th", { className: 'px-3 py-2 font-medium ' +
68
- (isNumericCol(col) ? 'text-right' : 'text-left'), children: [col.label, col.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }, col.key))), !lockRows && _jsx("th", { className: "w-12 px-3 py-2", "aria-label": "acciones" })] }) }), _jsxs("tbody", { children: [rows.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: itemFields.length + (lockRows ? 0 : 1), className: "px-3 py-4 text-center text-muted-foreground", children: "Sin renglones" }) })), rows.map((row, idx) => (_jsxs("tr", { className: "border-t align-top", children: [itemFields.map((col) => (_jsx("td", { className: "px-2 py-1.5", children: _jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), disabled: disabled, formValues: formValues, rowValues: row }) }, col.key))), !lockRows && (_jsx("td", { className: "px-2 py-1.5 text-center", children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeRow(idx), disabled: disabled, "aria-label": "Eliminar rengl\u00F3n", children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }) }))] }, idx)))] }), hasTotals && rows.length > 0 && (_jsx("tfoot", { className: "border-t bg-muted/30", children: _jsxs("tr", { children: [itemFields.map((col, ci) => {
68
+ return (_jsxs("div", { className: "grid gap-2", "data-widget": "line_items", children: [_jsxs("div", { className: "grid gap-2 sm:hidden", children: [rows.length === 0 && (_jsx("div", { className: "text-muted-foreground rounded-md border px-3 py-4 text-center text-sm", children: "Sin renglones" })), rows.map((row, idx) => (_jsxs("div", { className: "rounded-md border p-3", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsxs("span", { className: "text-muted-foreground text-xs font-medium", children: ["Rengl\u00F3n ", idx + 1] }), !lockRows && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-7", onClick: () => removeRow(idx), disabled: disabled, "aria-label": "Eliminar rengl\u00F3n", children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }))] }), _jsx("div", { className: "grid gap-2.5", children: itemFields.map((col) => {
69
+ const cellErr = errMap[`${idx}.${col.key}`];
70
+ return (_jsxs("div", { className: "grid gap-1", children: [_jsxs("span", { className: "text-xs font-medium", children: [col.label, col.required && (_jsx("span", { className: "ml-1 text-red-500", children: "*" }))] }), _jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), disabled: disabled, formValues: formValues, rowValues: row, invalid: !!cellErr }), cellErr && (_jsx("p", { className: "text-destructive text-xs", children: cellErr }))] }, col.key));
71
+ }) })] }, idx))), hasTotals && rows.length > 0 && (_jsx("div", { className: "bg-muted/30 grid gap-1.5 rounded-md border p-3", children: totalCols.map((col) => (_jsxs("div", { className: "flex items-center justify-between text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: col.label }), _jsx("span", { className: "font-semibold tabular-nums", children: fmtNumber(totals[col.key] ?? 0) })] }, col.key))) }))] }), _jsx("div", { className: "hidden overflow-x-auto rounded-md border sm:block", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50", children: _jsxs("tr", { children: [itemFields.map((col) => (_jsxs("th", { className: 'px-3 py-2 font-medium ' +
72
+ (isNumericCol(col) ? 'text-right' : 'text-left'), children: [col.label, col.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }, col.key))), !lockRows && _jsx("th", { className: "w-12 px-3 py-2", "aria-label": "acciones" })] }) }), _jsxs("tbody", { children: [rows.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: itemFields.length + (lockRows ? 0 : 1), className: "px-3 py-4 text-center text-muted-foreground", children: "Sin renglones" }) })), rows.map((row, idx) => (_jsxs("tr", { className: "border-t align-top", children: [itemFields.map((col) => {
73
+ const cellErr = errMap[`${idx}.${col.key}`];
74
+ return (_jsxs("td", { className: "px-2 py-1.5", children: [_jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), disabled: disabled, formValues: formValues, rowValues: row, invalid: !!cellErr }), cellErr && (_jsx("p", { className: "text-destructive mt-1 text-xs", children: cellErr }))] }, col.key));
75
+ }), !lockRows && (_jsx("td", { className: "px-2 py-1.5 text-center", children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeRow(idx), disabled: disabled, "aria-label": "Eliminar rengl\u00F3n", children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }) }))] }, idx)))] }), hasTotals && rows.length > 0 && (_jsx("tfoot", { className: "border-t bg-muted/30", children: _jsxs("tr", { children: [itemFields.map((col, ci) => {
69
76
  if (ci === 0) {
70
77
  return (_jsx("td", { className: "px-3 py-2 text-left font-medium text-muted-foreground", children: "Totales" }, col.key));
71
78
  }
@@ -86,8 +93,11 @@ function BalanceBadge({ state, }) {
86
93
  // without the per-field Label (the column header is the label) and sized for a
87
94
  // table cell. Nested line-items inside a row are not supported (a row column is
88
95
  // a scalar widget).
89
- function CellRenderer({ field, value, onChange, disabled, formValues, rowValues }) {
96
+ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues, invalid }) {
90
97
  const widget = resolveWidget(field);
98
+ const invalidCls = invalid
99
+ ? 'border-destructive ring-1 ring-destructive/30 focus-visible:ring-destructive'
100
+ : '';
91
101
  // Per-field read-only: a column locked by a PrefillSpec.lock (e.g. the
92
102
  // "ordered" / "already received" progress columns of a receive-goods modal)
93
103
  // renders disabled so it shows context without being editable. Tolerates the
@@ -119,7 +129,7 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues
119
129
  // Async searchable picker per row cell — e.g. the account_id column of a
120
130
  // journal entry's debit/credit lines. Same widget as the flat form.
121
131
  if (widget === 'dynamic_select') {
122
- return _jsx(DynamicSelectField, { field: field, value: value, onChange: onChange, dependsValue: dependsValue, readonly: ro });
132
+ return (_jsx(DynamicSelectField, { field: field, value: value, onChange: onChange, dependsValue: dependsValue, readonly: ro, invalid: invalid }));
123
133
  }
124
134
  if (widget === 'select' && (field.ref || getOptionsConfig(field)?.source)) {
125
135
  return (_jsx(RefCell, { field: field, value: value, onChange: onChange, disabled: disabled, formValues: formValues, rowValues: rowValues }));
@@ -127,24 +137,29 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues
127
137
  switch (widget) {
128
138
  case 'textarea':
129
139
  case 'richtext':
130
- return (_jsx(Textarea, { value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, disabled: off, rows: 2 }));
140
+ return (_jsx(Textarea, { value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, disabled: off, rows: 2, "aria-invalid": invalid || undefined, className: invalidCls || undefined }));
131
141
  case 'color':
132
- return (_jsx(Input, { type: "color", value: value || '#000000', onChange: (e) => onChange(e.target.value), disabled: off }));
142
+ return (_jsx(Input, { type: "color", value: value || '#000000', onChange: (e) => onChange(e.target.value), disabled: off, "aria-invalid": invalid || undefined, className: invalidCls || undefined }));
133
143
  case 'select': {
134
144
  const opts = effectiveOptions ?? field.options;
135
145
  // No option applies under the current sibling value → hide the cell.
136
146
  if (effectiveOptions && effectiveOptions.length === 0)
137
147
  return null;
138
- return (_jsxs(Select, { value: value || '', onValueChange: onChange, disabled: off, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: opts?.map((opt) => (_jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))) })] }));
148
+ return (_jsxs(Select, { value: value || '', onValueChange: onChange, disabled: off, children: [_jsx(SelectTrigger, { className: 'w-full' + (invalidCls ? ` ${invalidCls}` : ''), "aria-invalid": invalid || undefined, children: _jsx(SelectValue, { placeholder: field.placeholder || 'Seleccionar...' }) }), _jsx(SelectContent, { children: opts?.map((opt) => (_jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))) })] }));
139
149
  }
140
150
  case 'switch':
141
151
  return _jsx(Switch, { checked: !!value, onCheckedChange: onChange, disabled: off });
142
152
  case 'number':
143
- return (_jsx(Input, { type: "number", value: value ?? '', onChange: (e) => onChange(e.target.valueAsNumber || ''), placeholder: field.placeholder, disabled: off }));
153
+ return (_jsx(Input, { type: "number", value: value ?? '', onChange: (e) => {
154
+ const n = e.target.valueAsNumber;
155
+ // `0 || ''` would wipe a legitimate zero (unit_price=0)
156
+ // and then required-validation fires with nothing marked.
157
+ onChange(e.target.value === '' || !Number.isFinite(n) ? '' : n);
158
+ }, placeholder: field.placeholder, disabled: off, "aria-invalid": invalid || undefined, className: invalidCls || undefined }));
144
159
  case 'date':
145
- return (_jsx(Input, { type: "date", value: value || '', onChange: (e) => onChange(e.target.value), disabled: off }));
160
+ return (_jsx(Input, { type: "date", value: value || '', onChange: (e) => onChange(e.target.value), disabled: off, "aria-invalid": invalid || undefined, className: invalidCls || undefined }));
146
161
  default:
147
- return (_jsx(Input, { type: field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text', value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, disabled: off }));
162
+ return (_jsx(Input, { type: field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text', value: value || '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, disabled: off, "aria-invalid": invalid || undefined, className: invalidCls || undefined }));
148
163
  }
149
164
  }
150
165
  function RefCell({ field, value, onChange, disabled, formValues, rowValues }) {
@@ -0,0 +1,22 @@
1
+ import type { ActionFieldDef } from './types';
2
+ import type { Translate } from './server-error';
3
+ /**
4
+ * Resolve a validation path (`customer_id`, `lines.0.unit_price`) to a
5
+ * human label using the action/modal field schema. Nested line-item paths
6
+ * become "Renglones → Precio unitario (fila 1)".
7
+ */
8
+ export declare function labelForValidationPath(path: string, fields: readonly ActionFieldDef[] | undefined, t?: Translate): string;
9
+ /** Build `{path: label}` covering scalar fields and `parent.N.child` templates
10
+ * used by localizeFieldErrorMap (exact keys win when present). */
11
+ export declare function labelsForValidationFields(fields: readonly ActionFieldDef[] | undefined, t?: Translate): Record<string, string>;
12
+ /**
13
+ * Toast description: one "Label: message" line per error. Re-resolves labels
14
+ * for dotted line-item paths so operators see e.g.
15
+ * "Precio unitario (fila 1): es obligatorio" instead of a bare headline.
16
+ */
17
+ export declare function formatFieldErrorsDescription(errors: Record<string, string>, fields?: readonly ActionFieldDef[], t?: Translate): string | undefined;
18
+ /** Drop `key` and every `key.*` / `key.N.*` entry after the operator edits that field. */
19
+ export declare function clearFieldErrorTree(prev: Record<string, string>, key: string): Record<string, string>;
20
+ /** Slice `parent.0.child` errors into `{ "0.child": msg }` for DynamicLineItems. */
21
+ export declare function lineItemErrorsFor(parentKey: string, errors: Record<string, string> | undefined): Record<string, string>;
22
+ //# sourceMappingURL=field-validation-ui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-validation-ui.d.ts","sourceRoot":"","sources":["../src/field-validation-ui.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC7C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAiB/C;;;;GAIG;AACH,wBAAgB,sBAAsB,CAClC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,SAAS,cAAc,EAAE,GAAG,SAAS,EAC7C,CAAC,CAAC,EAAE,SAAS,GACd,MAAM,CAwCR;AAED;mEACmE;AACnE,wBAAgB,yBAAyB,CACrC,MAAM,EAAE,SAAS,cAAc,EAAE,GAAG,SAAS,EAC7C,CAAC,CAAC,EAAE,SAAS,GACd,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAYxB;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,MAAM,CAAC,EAAE,SAAS,cAAc,EAAE,EAClC,CAAC,CAAC,EAAE,SAAS,GACd,MAAM,GAAG,SAAS,CAYpB;AAED,0FAA0F;AAC1F,wBAAgB,mBAAmB,CAC/B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,GAAG,EAAE,MAAM,GACZ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQxB;AAED,oFAAoF;AACpF,wBAAgB,iBAAiB,CAC7B,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,GAC3C,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQxB"}
@@ -0,0 +1,123 @@
1
+ function itemFieldsOf(field) {
2
+ const raw = field.itemFields ?? field.item_fields;
3
+ return Array.isArray(raw) ? raw : [];
4
+ }
5
+ function translateLabel(raw, t) {
6
+ if (!raw)
7
+ return '';
8
+ if (!t)
9
+ return raw;
10
+ return t(raw, { defaultValue: raw });
11
+ }
12
+ function humanizeToken(key) {
13
+ return key.replace(/[._-]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
14
+ }
15
+ /**
16
+ * Resolve a validation path (`customer_id`, `lines.0.unit_price`) to a
17
+ * human label using the action/modal field schema. Nested line-item paths
18
+ * become "Renglones → Precio unitario (fila 1)".
19
+ */
20
+ export function labelForValidationPath(path, fields, t) {
21
+ if (!path)
22
+ return '';
23
+ const parts = path.split('.');
24
+ if (!fields?.length)
25
+ return humanizeToken(path);
26
+ let cursor = fields;
27
+ const bits = [];
28
+ let i = 0;
29
+ while (i < parts.length && cursor) {
30
+ const seg = parts[i];
31
+ if (/^\d+$/.test(seg)) {
32
+ const row = Number(seg) + 1;
33
+ const nextKey = parts[i + 1];
34
+ if (nextKey && cursor) {
35
+ const col = cursor.find((f) => f.key === nextKey);
36
+ const colLabel = translateLabel(col?.label, t) || humanizeToken(nextKey);
37
+ bits.push(`${colLabel} (fila ${row})`);
38
+ i += 2;
39
+ cursor = undefined;
40
+ continue;
41
+ }
42
+ bits.push(`fila ${row}`);
43
+ i += 1;
44
+ continue;
45
+ }
46
+ const field = cursor.find((f) => f.key === seg);
47
+ if (!field) {
48
+ bits.push(humanizeToken(parts.slice(i).join('.')));
49
+ break;
50
+ }
51
+ bits.push(translateLabel(field.label, t) || humanizeToken(seg));
52
+ const nested = itemFieldsOf(field);
53
+ cursor = nested.length ? nested : undefined;
54
+ i += 1;
55
+ if (!cursor && i < parts.length) {
56
+ bits.push(humanizeToken(parts.slice(i).join('.')));
57
+ break;
58
+ }
59
+ }
60
+ return bits.join(' → ');
61
+ }
62
+ /** Build `{path: label}` covering scalar fields and `parent.N.child` templates
63
+ * used by localizeFieldErrorMap (exact keys win when present). */
64
+ export function labelsForValidationFields(fields, t) {
65
+ const out = {};
66
+ if (!fields)
67
+ return out;
68
+ for (const f of fields) {
69
+ if (!f.key)
70
+ continue;
71
+ out[f.key] = translateLabel(f.label, t) || humanizeToken(f.key);
72
+ for (const child of itemFieldsOf(f)) {
73
+ if (!child.key)
74
+ continue;
75
+ out[`${f.key}.${child.key}`] = translateLabel(child.label, t) || humanizeToken(child.key);
76
+ }
77
+ }
78
+ return out;
79
+ }
80
+ /**
81
+ * Toast description: one "Label: message" line per error. Re-resolves labels
82
+ * for dotted line-item paths so operators see e.g.
83
+ * "Precio unitario (fila 1): es obligatorio" instead of a bare headline.
84
+ */
85
+ export function formatFieldErrorsDescription(errors, fields, t) {
86
+ const entries = Object.entries(errors);
87
+ if (!entries.length)
88
+ return undefined;
89
+ return entries
90
+ .map(([path, msg]) => {
91
+ const label = labelForValidationPath(path, fields, t);
92
+ // localizeFieldIssue already prefixes "{{label}}: …" — avoid
93
+ // "Label: Label: msg" when the message already starts with the label.
94
+ if (label && msg.toLowerCase().startsWith(label.toLowerCase()))
95
+ return msg;
96
+ return label ? `${label}: ${msg}` : msg;
97
+ })
98
+ .join(' · ');
99
+ }
100
+ /** Drop `key` and every `key.*` / `key.N.*` entry after the operator edits that field. */
101
+ export function clearFieldErrorTree(prev, key) {
102
+ if (!prev[key] && !Object.keys(prev).some((k) => k.startsWith(`${key}.`)))
103
+ return prev;
104
+ const next = {};
105
+ for (const [k, v] of Object.entries(prev)) {
106
+ if (k === key || k.startsWith(`${key}.`))
107
+ continue;
108
+ next[k] = v;
109
+ }
110
+ return next;
111
+ }
112
+ /** Slice `parent.0.child` errors into `{ "0.child": msg }` for DynamicLineItems. */
113
+ export function lineItemErrorsFor(parentKey, errors) {
114
+ if (!errors)
115
+ return {};
116
+ const prefix = `${parentKey}.`;
117
+ const out = {};
118
+ for (const [k, v] of Object.entries(errors)) {
119
+ if (k.startsWith(prefix))
120
+ out[k.slice(prefix.length)] = v;
121
+ }
122
+ return out;
123
+ }
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export * from './options-context';
4
4
  export { extractServerError, extractFieldErrors, localizeFieldIssue, localizeFieldErrorMap, toastServerError, toastServerSuccess, type ExtractedError, type Translate, type FieldIssue, } from './server-error';
5
5
  export { parseRuleString, fieldValidationOf, checkValue, validateValues, bagHasErrors, type ValidationSpec, } from './validator';
6
6
  export { VALIDATION_CATALOGS, validationCatalog, validationMessageKey } from './validation-catalog';
7
+ export { labelForValidationPath, labelsForValidationFields, formatFieldErrorsDescription, clearFieldErrorTree, lineItemErrorsFor, } from './field-validation-ui';
7
8
  export * from './dynamic-table';
8
9
  export { DynamicKanban, type DynamicKanbanProps, deriveStages, groupByStage, isTransitionAllowed, applyOptimisticMove, selectCardColumns, UNASSIGNED_LANE, } from './dynamic-kanban';
9
10
  export { useStageAutomations, StageAutomationsButton, isTagColumn, automationFieldOptions, groupAutomationsByStage, activeAutomationCount, type StageAutomation, type StageAutomationAction, type StageAutomationActionType, type NewStageAutomation, type UseStageAutomationsResult, type StageAutomationsButtonProps, } from './stage-automations';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ export * from './options-context';
9
9
  export { extractServerError, extractFieldErrors, localizeFieldIssue, localizeFieldErrorMap, toastServerError, toastServerSuccess, } from './server-error';
10
10
  export { parseRuleString, fieldValidationOf, checkValue, validateValues, bagHasErrors, } from './validator';
11
11
  export { VALIDATION_CATALOGS, validationCatalog, validationMessageKey } from './validation-catalog';
12
+ export { labelForValidationPath, labelsForValidationFields, formatFieldErrorsDescription, clearFieldErrorTree, lineItemErrorsFor, } from './field-validation-ui';
12
13
  export * from './dynamic-table';
13
14
  export { DynamicKanban, deriveStages, groupByStage, isTransitionAllowed, applyOptimisticMove, selectCardColumns, UNASSIGNED_LANE, } from './dynamic-kanban';
14
15
  export { useStageAutomations, StageAutomationsButton, isTagColumn, automationFieldOptions, groupAutomationsByStage, activeAutomationCount, } from './stage-automations';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "37.0.4",
3
+ "version": "37.0.5",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ clearFieldErrorTree,
4
+ formatFieldErrorsDescription,
5
+ labelForValidationPath,
6
+ lineItemErrorsFor,
7
+ } from '../field-validation-ui'
8
+ import type { ActionFieldDef } from '../types'
9
+
10
+ const fields: ActionFieldDef[] = [
11
+ { key: 'customer_id', label: 'Cliente', type: 'dynamic_select', required: true },
12
+ {
13
+ key: 'lines',
14
+ label: 'Renglones',
15
+ type: 'array',
16
+ required: true,
17
+ itemFields: [
18
+ { key: 'product_id', label: 'Producto', type: 'dynamic_select', required: true },
19
+ { key: 'unit_price', label: 'Precio unitario', type: 'number', required: true },
20
+ ],
21
+ },
22
+ ]
23
+
24
+ describe('field-validation-ui', () => {
25
+ it('labels dotted line-item paths with fila N', () => {
26
+ expect(labelForValidationPath('lines.0.unit_price', fields)).toBe(
27
+ 'Renglones → Precio unitario (fila 1)',
28
+ )
29
+ })
30
+
31
+ it('formats toast description with field labels', () => {
32
+ const desc = formatFieldErrorsDescription(
33
+ { 'lines.0.unit_price': 'es obligatorio' },
34
+ fields,
35
+ )
36
+ expect(desc).toContain('Precio unitario (fila 1)')
37
+ expect(desc).toContain('es obligatorio')
38
+ })
39
+
40
+ it('slices parent errors for DynamicLineItems', () => {
41
+ expect(
42
+ lineItemErrorsFor('lines', {
43
+ 'lines.0.unit_price': 'required',
44
+ customer_id: 'required',
45
+ }),
46
+ ).toEqual({ '0.unit_price': 'required' })
47
+ })
48
+
49
+ it('clears dotted children when parent key edits', () => {
50
+ expect(
51
+ clearFieldErrorTree(
52
+ { 'lines.0.unit_price': 'x', customer_id: 'y' },
53
+ 'lines',
54
+ ),
55
+ ).toEqual({ customer_id: 'y' })
56
+ })
57
+ })
@@ -40,6 +40,13 @@ import { toast } from 'sonner'
40
40
  import { toastServerError, toastServerSuccess, extractFieldErrors, localizeFieldErrorMap } from './server-error'
41
41
  import type { Translate } from './server-error'
42
42
  import { validateValues, bagHasErrors } from './validator'
43
+ import {
44
+ clearFieldErrorTree,
45
+ formatFieldErrorsDescription,
46
+ labelsForValidationFields,
47
+ labelForValidationPath,
48
+ lineItemErrorsFor,
49
+ } from './field-validation-ui'
43
50
  import { validationCatalog } from './validation-catalog'
44
51
  import { useApi } from './api-context'
45
52
  import { DynamicIcon } from './dynamic-icon'
@@ -621,33 +628,43 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
621
628
 
622
629
  const updateField = (key: string, value: any) => {
623
630
  setFormData((prev: Record<string, any>) => ({ ...prev, [key]: value }))
624
- setFieldErrors(prev => {
625
- if (!prev[key]) return prev
626
- const next = { ...prev }
627
- delete next[key]
628
- return next
629
- })
631
+ setFieldErrors((prev) => clearFieldErrorTree(prev, key))
630
632
  }
631
633
 
632
634
  const lang = i18n.language
633
635
  const handleActionError = (err: unknown) => {
636
+ const labels = labelsForValidationFields(action.fields, t)
634
637
  const localized = localizeActionFieldErrors(err, action.fields, t, lang)
635
638
  if (localized) {
636
- setFieldErrors(localized)
637
- toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }))
639
+ // Enrich labels for dotted line-item paths before toasting.
640
+ const withPathLabels: Record<string, string> = {}
641
+ for (const [path, msg] of Object.entries(localized)) {
642
+ withPathLabels[path] = msg
643
+ if (!labels[path]) labels[path] = labelForValidationPath(path, action.fields, t)
644
+ }
645
+ setFieldErrors(withPathLabels)
646
+ toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }), {
647
+ description: formatFieldErrorsDescription(withPathLabels, action.fields, t),
648
+ })
638
649
  return
639
650
  }
640
- toastServerError(err, { t, language: lang })
651
+ toastServerError(err, { t, language: lang, labels })
641
652
  }
642
653
 
643
654
  const execute = async () => {
644
655
  if (action.fields) {
645
656
  const bag = validateValues(action.fields, formData)
646
657
  if (bagHasErrors(bag)) {
647
- const labels: Record<string, string> = {}
648
- for (const f of action.fields) labels[f.key] = tl(f.label)
649
- setFieldErrors(localizeFieldErrorMap(bag, t, { labels, language: lang }))
650
- toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }))
658
+ const labels = labelsForValidationFields(action.fields, (k, o) => t(k, o))
659
+ // Exact dotted keys (`lines.0.qty`) need path-aware labels.
660
+ for (const path of Object.keys(bag)) {
661
+ if (!labels[path]) labels[path] = labelForValidationPath(path, action.fields, t)
662
+ }
663
+ const next = localizeFieldErrorMap(bag, t, { labels, language: lang })
664
+ setFieldErrors(next)
665
+ toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }), {
666
+ description: formatFieldErrorsDescription(next, action.fields, t),
667
+ })
651
668
  return
652
669
  }
653
670
  }
@@ -727,7 +744,14 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
727
744
  <FieldLabel htmlFor={field.key} required={field.required}>
728
745
  {tl(field.label)}
729
746
  </FieldLabel>
730
- {renderField(field, formData[field.key], (v: any) => updateField(field.key, v), formData, record)}
747
+ {renderField(
748
+ field,
749
+ formData[field.key],
750
+ (v: any) => updateField(field.key, v),
751
+ formData,
752
+ record,
753
+ fieldErrors,
754
+ )}
731
755
  {fieldErrors[field.key] && (
732
756
  <p className="text-destructive text-xs mt-1">{fieldErrors[field.key]}</p>
733
757
  )}
@@ -1011,17 +1035,30 @@ function renderField(
1011
1035
  // then treated as having no resolvable dependency).
1012
1036
  formValues?: Record<string, any>,
1013
1037
  record?: Record<string, any>,
1038
+ fieldErrors?: Record<string, string>,
1014
1039
  ) {
1015
1040
  // Repeatable line-items group → row grid (value is an array of row objects).
1016
1041
  // The header form values flow in so a cell can depend on a header field.
1017
1042
  if (isLineItemsField(field)) {
1018
- return <DynamicLineItems field={applyPrefillLock(field)} value={value} onChange={onChange} formValues={formValues} />
1043
+ return (
1044
+ <DynamicLineItems
1045
+ field={applyPrefillLock(field)}
1046
+ value={value}
1047
+ onChange={onChange}
1048
+ formValues={formValues}
1049
+ errors={lineItemErrorsFor(field.key, fieldErrors)}
1050
+ />
1051
+ )
1019
1052
  }
1020
1053
  // Resolve the widget the same way DynamicForm does (explicit widget wins,
1021
1054
  // else inferred from type) so action modals and the standalone form stay in
1022
1055
  // lockstep — previously this switch keyed off `field.type` and silently
1023
1056
  // dropped `dynamic_select` to a plain text input.
1024
1057
  const widget = resolveWidget(field)
1058
+ const invalid = !!(fieldErrors && fieldErrors[field.key])
1059
+ const invalidCls = invalid
1060
+ ? 'border-destructive ring-1 ring-destructive/30 focus-visible:ring-destructive'
1061
+ : ''
1025
1062
  if (widget === 'dynamic_select') {
1026
1063
  // A header-level dynamic_select may itself depend on another header
1027
1064
  // field; resolve its filter_value from the form context.
@@ -1035,6 +1072,7 @@ function renderField(
1035
1072
  onChange={onChange}
1036
1073
  dependsValue={dependsValue}
1037
1074
  seedOption={seedOptionFromRecord(field, value, record)}
1075
+ invalid={invalid}
1038
1076
  />
1039
1077
  )
1040
1078
  }
@@ -1045,11 +1083,11 @@ function renderField(
1045
1083
  }
1046
1084
  switch (widget) {
1047
1085
  case 'textarea':
1048
- return <Textarea id={field.key} value={value || ''} onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange(e.target.value)} placeholder={field.placeholder} />
1086
+ return <Textarea id={field.key} value={value || ''} onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange(e.target.value)} placeholder={field.placeholder} aria-invalid={invalid || undefined} className={invalidCls || undefined} />
1049
1087
  case 'select':
1050
1088
  return (
1051
1089
  <Select value={value || ''} onValueChange={onChange}>
1052
- <SelectTrigger className="w-full"><SelectValue placeholder={field.placeholder || 'Seleccionar...'} /></SelectTrigger>
1090
+ <SelectTrigger className={'w-full' + (invalidCls ? ` ${invalidCls}` : '')} aria-invalid={invalid || undefined}><SelectValue placeholder={field.placeholder || 'Seleccionar...'} /></SelectTrigger>
1053
1091
  <SelectContent>
1054
1092
  {field.options?.map((opt) => <SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>)}
1055
1093
  </SelectContent>
@@ -1058,12 +1096,15 @@ function renderField(
1058
1096
  case 'switch':
1059
1097
  return <Switch id={field.key} checked={!!value} onCheckedChange={onChange} />
1060
1098
  case 'number':
1061
- return <Input id={field.key} type="number" value={value ?? ''} onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.valueAsNumber || '')} placeholder={field.placeholder} />
1099
+ return <Input id={field.key} type="number" value={value ?? ''} onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
1100
+ const n = e.target.valueAsNumber
1101
+ onChange(e.target.value === '' || !Number.isFinite(n) ? '' : n)
1102
+ }} placeholder={field.placeholder} aria-invalid={invalid || undefined} className={invalidCls || undefined} />
1062
1103
  case 'date':
1063
1104
  // Modern shadcn Calendar in a Popover (portaled, never clipped by the
1064
1105
  // modal) instead of the native, dated, easily-cut <input type=date>.
1065
1106
  return <DynamicDateField field={field} value={value} onChange={onChange} />
1066
1107
  default:
1067
- return <Input id={field.key} type={field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text'} value={value || ''} onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value)} placeholder={field.placeholder} />
1108
+ return <Input id={field.key} type={field.type === 'email' ? 'email' : field.type === 'url' ? 'url' : 'text'} value={value || ''} onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value)} placeholder={field.placeholder} aria-invalid={invalid || undefined} className={invalidCls || undefined} />
1068
1109
  }
1069
1110
  }
@@ -864,11 +864,14 @@ export function DynamicRecordDialog({
864
864
  for (const f of visible) labels[f.key] = f.label
865
865
  const next = localizeFieldErrorMap(bag, t, { labels })
866
866
  setFieldErrors(next)
867
- const visibleKeys = new Set(visible.map(f => f.key))
868
- const orphans = Object.entries(next).filter(([k]) => !visibleKeys.has(k))
869
- const description = orphans.length
870
- ? orphans.map(([k, msg]) => `${labelForKey(k)}: ${msg}`).join(' · ')
871
- : undefined
867
+ const description = Object.entries(next)
868
+ .map(([k, msg]) => {
869
+ const label = labels[k] || labelForKey(k)
870
+ return msg.toLowerCase().startsWith(String(label).toLowerCase())
871
+ ? msg
872
+ : `${label}: ${msg}`
873
+ })
874
+ .join(' · ')
872
875
  toast.error(
873
876
  t('dynamic.validation_failed', { defaultValue: 'Revisa los campos marcados' }),
874
877
  description ? { description } : undefined,
@@ -420,7 +420,10 @@ function FieldRenderer({
420
420
  case 'switch':
421
421
  return <Switch id={field.key} checked={!!value} onCheckedChange={onChange} />
422
422
  case 'number':
423
- return <Input id={field.key} type="number" value={value ?? ''} onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.valueAsNumber || '')} placeholder={field.placeholder} />
423
+ return <Input id={field.key} type="number" value={value ?? ''} onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
424
+ const n = e.target.valueAsNumber
425
+ onChange(e.target.value === '' || !Number.isFinite(n) ? '' : n)
426
+ }} placeholder={field.placeholder} />
424
427
  case 'date':
425
428
  return <DynamicDateField field={field} value={value} onChange={onChange} />
426
429
  default:
@@ -463,9 +466,14 @@ function ScannableInput({
463
466
  id={field.key}
464
467
  type={type}
465
468
  value={value ?? ''}
466
- onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
467
- onChange(type === 'number' ? e.target.valueAsNumber || '' : e.target.value)
468
- }
469
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
470
+ if (type !== 'number') {
471
+ onChange(e.target.value)
472
+ return
473
+ }
474
+ const n = e.target.valueAsNumber
475
+ onChange(e.target.value === '' || !Number.isFinite(n) ? '' : n)
476
+ }}
469
477
  placeholder={field.placeholder}
470
478
  />
471
479
  )
@@ -47,6 +47,11 @@ export interface DynamicLineItemsProps {
47
47
  * (e.g. `source_warehouse_id`), not just a sibling cell on the same row.
48
48
  */
49
49
  formValues?: Record<string, any>
50
+ /**
51
+ * Localized validation messages keyed as `rowIndex.columnKey` (e.g.
52
+ * `0.unit_price`). Painted as destructive borders + under-cell text.
53
+ */
54
+ errors?: Record<string, string>
50
55
  }
51
56
 
52
57
  const fmtNumber = (n: number): string =>
@@ -65,9 +70,10 @@ function emptyRow(itemFields: ActionFieldDef[]): Record<string, any> {
65
70
  return row
66
71
  }
67
72
 
68
- export function DynamicLineItems({ field, value, onChange, disabled = false, formValues }: DynamicLineItemsProps) {
73
+ export function DynamicLineItems({ field, value, onChange, disabled = false, formValues, errors }: DynamicLineItemsProps) {
69
74
  const itemFields = getItemFields(field)
70
75
  const rows: any[] = Array.isArray(value) ? value : []
76
+ const errMap = errors ?? {}
71
77
 
72
78
  // `lock_rows` fixes the row set: no add-row button, no per-row delete. Rows
73
79
  // stay editable cell-by-cell. Snake_case is what the kernel serves; tolerate
@@ -149,7 +155,9 @@ export function DynamicLineItems({ field, value, onChange, disabled = false, for
149
155
  )}
150
156
  </div>
151
157
  <div className="grid gap-2.5">
152
- {itemFields.map((col) => (
158
+ {itemFields.map((col) => {
159
+ const cellErr = errMap[`${idx}.${col.key}`]
160
+ return (
153
161
  <div key={col.key} className="grid gap-1">
154
162
  <span className="text-xs font-medium">
155
163
  {col.label}
@@ -164,9 +172,14 @@ export function DynamicLineItems({ field, value, onChange, disabled = false, for
164
172
  disabled={disabled}
165
173
  formValues={formValues}
166
174
  rowValues={row}
175
+ invalid={!!cellErr}
167
176
  />
177
+ {cellErr && (
178
+ <p className="text-destructive text-xs">{cellErr}</p>
179
+ )}
168
180
  </div>
169
- ))}
181
+ )
182
+ })}
170
183
  </div>
171
184
  </div>
172
185
  ))}
@@ -219,7 +232,9 @@ export function DynamicLineItems({ field, value, onChange, disabled = false, for
219
232
  )}
220
233
  {rows.map((row, idx) => (
221
234
  <tr key={idx} className="border-t align-top">
222
- {itemFields.map((col) => (
235
+ {itemFields.map((col) => {
236
+ const cellErr = errMap[`${idx}.${col.key}`]
237
+ return (
223
238
  <td key={col.key} className="px-2 py-1.5">
224
239
  <CellRenderer
225
240
  field={col}
@@ -228,9 +243,14 @@ export function DynamicLineItems({ field, value, onChange, disabled = false, for
228
243
  disabled={disabled}
229
244
  formValues={formValues}
230
245
  rowValues={row}
246
+ invalid={!!cellErr}
231
247
  />
248
+ {cellErr && (
249
+ <p className="text-destructive mt-1 text-xs">{cellErr}</p>
250
+ )}
232
251
  </td>
233
- ))}
252
+ )
253
+ })}
234
254
  {!lockRows && (
235
255
  <td className="px-2 py-1.5 text-center">
236
256
  <Button
@@ -342,14 +362,19 @@ interface CellRendererProps {
342
362
  formValues?: Record<string, any>
343
363
  /** This row's values — for resolving a cell's `dependsOn` to a sibling cell. */
344
364
  rowValues?: Record<string, any>
365
+ /** Paint the control as invalid (destructive border). */
366
+ invalid?: boolean
345
367
  }
346
368
 
347
369
  // Per-cell widget. Mirrors the flat FieldRenderer in dynamic-form.tsx but
348
370
  // without the per-field Label (the column header is the label) and sized for a
349
371
  // table cell. Nested line-items inside a row are not supported (a row column is
350
372
  // a scalar widget).
351
- function CellRenderer({ field, value, onChange, disabled, formValues, rowValues }: CellRendererProps) {
373
+ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues, invalid }: CellRendererProps) {
352
374
  const widget = resolveWidget(field)
375
+ const invalidCls = invalid
376
+ ? 'border-destructive ring-1 ring-destructive/30 focus-visible:ring-destructive'
377
+ : ''
353
378
  // Per-field read-only: a column locked by a PrefillSpec.lock (e.g. the
354
379
  // "ordered" / "already received" progress columns of a receive-goods modal)
355
380
  // renders disabled so it shows context without being editable. Tolerates the
@@ -384,7 +409,16 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues
384
409
  // Async searchable picker per row cell — e.g. the account_id column of a
385
410
  // journal entry's debit/credit lines. Same widget as the flat form.
386
411
  if (widget === 'dynamic_select') {
387
- return <DynamicSelectField field={field} value={value} onChange={onChange} dependsValue={dependsValue} readonly={ro} />
412
+ return (
413
+ <DynamicSelectField
414
+ field={field}
415
+ value={value}
416
+ onChange={onChange}
417
+ dependsValue={dependsValue}
418
+ readonly={ro}
419
+ invalid={invalid}
420
+ />
421
+ )
388
422
  }
389
423
  if (widget === 'select' && (field.ref || getOptionsConfig(field)?.source)) {
390
424
  return (
@@ -408,6 +442,8 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues
408
442
  placeholder={field.placeholder}
409
443
  disabled={off}
410
444
  rows={2}
445
+ aria-invalid={invalid || undefined}
446
+ className={invalidCls || undefined}
411
447
  />
412
448
  )
413
449
  case 'color':
@@ -417,6 +453,8 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues
417
453
  value={value || '#000000'}
418
454
  onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}
419
455
  disabled={off}
456
+ aria-invalid={invalid || undefined}
457
+ className={invalidCls || undefined}
420
458
  />
421
459
  )
422
460
  case 'select': {
@@ -425,7 +463,7 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues
425
463
  if (effectiveOptions && effectiveOptions.length === 0) return null
426
464
  return (
427
465
  <Select value={value || ''} onValueChange={onChange} disabled={off}>
428
- <SelectTrigger className="w-full">
466
+ <SelectTrigger className={'w-full' + (invalidCls ? ` ${invalidCls}` : '')} aria-invalid={invalid || undefined}>
429
467
  <SelectValue placeholder={field.placeholder || 'Seleccionar...'} />
430
468
  </SelectTrigger>
431
469
  <SelectContent>
@@ -445,9 +483,16 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues
445
483
  <Input
446
484
  type="number"
447
485
  value={value ?? ''}
448
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.valueAsNumber || '')}
486
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
487
+ const n = e.target.valueAsNumber
488
+ // `0 || ''` would wipe a legitimate zero (unit_price=0)
489
+ // and then required-validation fires with nothing marked.
490
+ onChange(e.target.value === '' || !Number.isFinite(n) ? '' : n)
491
+ }}
449
492
  placeholder={field.placeholder}
450
493
  disabled={off}
494
+ aria-invalid={invalid || undefined}
495
+ className={invalidCls || undefined}
451
496
  />
452
497
  )
453
498
  case 'date':
@@ -457,6 +502,8 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues
457
502
  value={value || ''}
458
503
  onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}
459
504
  disabled={off}
505
+ aria-invalid={invalid || undefined}
506
+ className={invalidCls || undefined}
460
507
  />
461
508
  )
462
509
  default:
@@ -467,6 +514,8 @@ function CellRenderer({ field, value, onChange, disabled, formValues, rowValues
467
514
  onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}
468
515
  placeholder={field.placeholder}
469
516
  disabled={off}
517
+ aria-invalid={invalid || undefined}
518
+ className={invalidCls || undefined}
470
519
  />
471
520
  )
472
521
  }
@@ -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,