@asteby/metacore-runtime-react 37.0.3 → 37.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
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
+
9
+ ## 37.0.4
10
+
11
+ ### Patch Changes
12
+
13
+ - 0396b95: DynamicRecordDialog: unique form ids (nested create), Laravel-style validateValues on submit, destructive borders on invalid fields, toast descriptions for errors on non-rendered fields, and `ensureFields` to inject host scope keys into the modal schema.
14
+
3
15
  ## 37.0.3
4
16
 
5
17
  ### 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
  }
@@ -100,6 +100,12 @@ export interface DynamicRecordDialogProps {
100
100
  * lets it close while the depth lock is held.
101
101
  */
102
102
  nestedInlineCreateSelf?: boolean;
103
+ /**
104
+ * Fields merged into the modal schema after load (by key). Existing keys are
105
+ * shallow-merged; missing keys are prepended. Hosts use this to inject
106
+ * required scope fields (e.g. branch_id) omitted from compiled DefineModal.
107
+ */
108
+ ensureFields?: FieldDef[];
103
109
  mode: 'view' | 'edit' | 'create';
104
110
  model: string;
105
111
  recordId?: string | null;
@@ -198,7 +204,7 @@ export declare function fkSeedOption(field: FieldDef, value: any, record: any):
198
204
  export declare function isMoneyField(field: FieldDef, value: any): boolean;
199
205
  export declare function filterVisibleFields(fields: FieldDef[] | undefined, mode: 'view' | 'edit' | 'create', formValues?: Record<string, any>): FieldDef[];
200
206
  export declare function stripHiddenFieldValues(values: Record<string, any>, fields: FieldDef[] | undefined, mode: 'view' | 'edit' | 'create'): Record<string, any>;
201
- export declare function DynamicRecordDialog({ open, onOpenChange, nestedInlineCreateSelf, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, lockedFields, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl, timeZone, currency, onChange, }: DynamicRecordDialogProps): import("react").JSX.Element;
207
+ export declare function DynamicRecordDialog({ open, onOpenChange, nestedInlineCreateSelf, ensureFields, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, lockedFields, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl, timeZone, currency, onChange, }: DynamicRecordDialogProps): import("react").JSX.Element;
202
208
  export declare function ReadonlyEditField({ field, value }: {
203
209
  field: FieldDef;
204
210
  value: any;
@@ -214,11 +220,13 @@ export declare function ViewValue({ field, value: rawValue, record, getImageUrl:
214
220
  /** Optional override; when omitted falls back to the nearest provider. */
215
221
  currency?: string;
216
222
  }): import("react").JSX.Element;
217
- export declare function EditField({ field, value, onChange, record }: {
223
+ export declare function EditField({ field, value, onChange, record, invalid }: {
218
224
  field: FieldDef;
219
225
  value: any;
220
226
  onChange: (val: any) => void;
221
227
  /** The full record being edited — supplies FK relation siblings + line-items. */
222
228
  record?: any;
229
+ /** When true, paint the control with a destructive border (Laravel-style). */
230
+ invalid?: boolean;
223
231
  }): import("react").JSX.Element;
224
232
  //# sourceMappingURL=dynamic-record.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
@@ -10,7 +10,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
10
  // flows through <ApiProvider> from runtime-react. Host-specific runtime values —
11
11
  // the image-url resolver and the org IANA timezone — are passed as props so the
12
12
  // SDK stays transport- and host-agnostic.
13
- import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
13
+ import { createContext, useCallback, useContext, useEffect, useId, useRef, useState } from 'react';
14
14
  import { useTranslation } from 'react-i18next';
15
15
  /** Model key of the open create/edit dialog — used to hide "+" on self-FK pickers
16
16
  * (e.g. Customer.parent_id) so they don't nest another "Crear Cliente" modal. */
@@ -23,7 +23,7 @@ import { es } from 'date-fns/locale';
23
23
  import { ExternalLink, Loader2, CalendarIcon, ChevronDown, Check, Upload, X as XIcon, ScanLine } from 'lucide-react';
24
24
  import { BarcodeScanner } from '../barcode-scanner';
25
25
  import { useApi } from '../api-context';
26
- import { toastServerError, extractFieldErrors, localizeFieldIssue } from '../server-error';
26
+ import { toastServerError, extractFieldErrors, localizeFieldIssue, localizeFieldErrorMap } from '../server-error';
27
27
  import { DynamicSelectField, OptionLead, OptionThumb } from '../dynamic-select-field';
28
28
  import { DynamicRelations } from '../dynamic-relations';
29
29
  import { useOptionsResolver } from '../use-options-resolver';
@@ -33,6 +33,7 @@ import { FieldSection, WizardProgress } from '../form-layout-ui';
33
33
  import { FieldCell } from '../field-grid';
34
34
  import { isNilUuid, normalizeNilUuid } from '../nil-uuid';
35
35
  import { normalizeRefFieldsForSubmit } from './normalize-submit';
36
+ import { validateValues, bagHasErrors } from '../validator';
36
37
  import { DynamicIcon, isLucideIconName } from '../dynamic-icon';
37
38
  import { IconPickerField } from '../icon-picker-field';
38
39
  import { humanizeToken } from '../dynamic-columns-helpers';
@@ -299,7 +300,24 @@ export function stripHiddenFieldValues(values, fields, mode) {
299
300
  }
300
301
  return out;
301
302
  }
302
- export function DynamicRecordDialog({ open, onOpenChange, nestedInlineCreateSelf, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, lockedFields, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl = identityImageUrl, timeZone, currency, onChange, }) {
303
+ function applyEnsureFields(meta, ensureFields) {
304
+ if (!meta)
305
+ return meta ?? null;
306
+ if (!ensureFields?.length)
307
+ return meta;
308
+ const fields = Array.isArray(meta.fields) ? [...meta.fields] : [];
309
+ for (const ensure of ensureFields) {
310
+ const idx = fields.findIndex((f) => f?.key === ensure.key);
311
+ if (idx >= 0) {
312
+ fields[idx] = { ...fields[idx], ...ensure };
313
+ }
314
+ else {
315
+ fields.unshift(ensure);
316
+ }
317
+ }
318
+ return { ...meta, fields };
319
+ }
320
+ export function DynamicRecordDialog({ open, onOpenChange, nestedInlineCreateSelf, ensureFields, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, lockedFields, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl = identityImageUrl, timeZone, currency, onChange, }) {
303
321
  const api = useApi();
304
322
  const { t } = useTranslation();
305
323
  const [modalMeta, setModalMeta] = useState(schema ? schema : null);
@@ -310,6 +328,9 @@ export function DynamicRecordDialog({ open, onOpenChange, nestedInlineCreateSelf
310
328
  // inline under each input; populated from a 422 `errors` map or the client
311
329
  // required-field check, cleared per-field on change and wholesale on reopen.
312
330
  const [fieldErrors, setFieldErrors] = useState({});
331
+ // Unique form id per dialog instance — nested create must not share
332
+ // id={formId} or the child footer submits the parent.
333
+ const formId = useId();
313
334
  const [loading, setLoading] = useState(false);
314
335
  const [saving, setSaving] = useState(false);
315
336
  const [deleting, setDeleting] = useState(false);
@@ -367,6 +388,7 @@ export function DynamicRecordDialog({ open, onOpenChange, nestedInlineCreateSelf
367
388
  return;
368
389
  meta = metaRes.data?.data ?? metaRes.data;
369
390
  }
391
+ meta = applyEnsureFields(meta, ensureFields);
370
392
  setModalMeta(meta);
371
393
  if (isCreate) {
372
394
  const initial = {};
@@ -415,7 +437,7 @@ export function DynamicRecordDialog({ open, onOpenChange, nestedInlineCreateSelf
415
437
  // initialRecord intentionally omitted: the row identity is captured per open
416
438
  // via recordId; re-seeding mid-open would clobber edits.
417
439
  // eslint-disable-next-line react-hooks/exhaustive-deps
418
- }, [open, recordId, model, endpoint, isCreate, schema]);
440
+ }, [open, recordId, model, endpoint, isCreate, schema, ensureFields]);
419
441
  // Reset when closed
420
442
  useEffect(() => {
421
443
  if (!open) {
@@ -509,7 +531,12 @@ export function DynamicRecordDialog({ open, onOpenChange, nestedInlineCreateSelf
509
531
  next[key] = localizeFieldIssue(issues[0], labelForKey(key), t);
510
532
  }
511
533
  setFieldErrors(next);
512
- toast.error(t('dynamic.validation_failed', { defaultValue: 'Revisa los campos marcados' }));
534
+ const visibleKeys = new Set(filterVisibleFields(modalMeta?.fields ?? [], mode, formValues).map(f => f.key));
535
+ const orphans = Object.entries(next).filter(([k]) => !visibleKeys.has(k));
536
+ const description = orphans.length
537
+ ? orphans.map(([k, msg]) => `${labelForKey(k)}: ${msg}`).join(' · ')
538
+ : undefined;
539
+ toast.error(t('dynamic.validation_failed', { defaultValue: 'Revisa los campos marcados' }), description ? { description } : undefined);
513
540
  return;
514
541
  }
515
542
  toastServerError(err, { t, fallback: t('dynamic.save_error', { defaultValue: 'No se pudo guardar' }) });
@@ -519,20 +546,25 @@ export function DynamicRecordDialog({ open, onOpenChange, nestedInlineCreateSelf
519
546
  if (!modalMeta)
520
547
  return;
521
548
  if (isEditable) {
522
- // Collect ALL missing required fields (not just the first) and mark
523
- // each inline instead of a single toast. Only CURRENTLY-VISIBLE
524
- // fields are gated: a field hidden by its `visible_when` predicate
525
- // must not block submit even when it is declared required (matching
526
- // the render, which drops it via the same filter).
527
- const missing = {};
528
- for (const field of filterVisibleFields(modalMeta.fields, mode, formValues)) {
529
- if (field.required && !formValues[field.key] && formValues[field.key] !== 0 && formValues[field.key] !== false) {
530
- missing[field.key] = localizeFieldIssue({ code: 'required' }, field.label, t);
531
- }
532
- }
533
- if (Object.keys(missing).length) {
534
- setFieldErrors(missing);
535
- toast.error(t('dynamic.validation_failed', { defaultValue: 'Revisa los campos marcados' }));
549
+ // Laravel-style: collect every issue from the shared validator
550
+ // (required + rule strings / min/max / email…) on visible fields only.
551
+ const visible = filterVisibleFields(modalMeta.fields, mode, formValues);
552
+ const bag = validateValues(visible, formValues);
553
+ if (bagHasErrors(bag)) {
554
+ const labels = {};
555
+ for (const f of visible)
556
+ labels[f.key] = f.label;
557
+ const next = localizeFieldErrorMap(bag, t, { labels });
558
+ setFieldErrors(next);
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(' · ');
567
+ toast.error(t('dynamic.validation_failed', { defaultValue: 'Revisa los campos marcados' }), description ? { description } : undefined);
536
568
  return;
537
569
  }
538
570
  }
@@ -653,14 +685,13 @@ export function DynamicRecordDialog({ open, onOpenChange, nestedInlineCreateSelf
653
685
  // then advance. Mirrors handleSubmit's required check but scoped to the step.
654
686
  const goNextStep = () => {
655
687
  const step = groups[clampedStep];
656
- const missing = {};
657
- for (const field of step?.fields ?? []) {
658
- if (field.required && !formValues[field.key] && formValues[field.key] !== 0 && formValues[field.key] !== false) {
659
- missing[field.key] = localizeFieldIssue({ code: 'required' }, field.label, t);
660
- }
661
- }
662
- if (Object.keys(missing).length) {
663
- setFieldErrors(missing);
688
+ const stepFields = step?.fields ?? [];
689
+ const bag = validateValues(stepFields, formValues);
690
+ if (bagHasErrors(bag)) {
691
+ const labels = {};
692
+ for (const f of stepFields)
693
+ labels[f.key] = f.label;
694
+ setFieldErrors(localizeFieldErrorMap(bag, t, { labels }));
664
695
  toast.error(t('dynamic.validation_failed', { defaultValue: 'Revisa los campos marcados' }));
665
696
  return;
666
697
  }
@@ -668,7 +699,7 @@ export function DynamicRecordDialog({ open, onOpenChange, nestedInlineCreateSelf
668
699
  setStepIndex(Math.min(clampedStep + 1, groups.length - 1));
669
700
  };
670
701
  const goBackStep = () => setStepIndex(Math.max(clampedStep - 1, 0));
671
- return (_jsx(RecordDialogModelContext.Provider, { value: model, children: _jsx(Dialog, { open: open, onOpenChange: onOpenChange, nestedInlineCreateSelf: nestedInlineCreateSelf, children: _jsxs(DialogContent, { className: "sm:max-w-2xl max-h-[90dvh] flex flex-col p-0 gap-0 overflow-hidden", style: { maxHeight: '90dvh' }, children: [_jsxs(DialogHeader, { className: "p-6 pb-4 border-b shrink-0", children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: config.description })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-6", children: loading ? (_jsx(LoadingSkeleton, {})) : modalMeta ? (_jsx(ModelContext.Provider, { value: model, children: _jsx(ImageUrlContext.Provider, { value: getImageUrl, children: _jsx(TimeZoneContext.Provider, { value: timeZone, children: _jsxs(CurrencyContext.Provider, { value: currency, children: [_jsxs("form", { id: "dynamic-record-form", onSubmit: handleSubmit, className: "grid gap-y-4", children: [isSteps && (_jsx(WizardProgress, { groups: groups, stepIndex: clampedStep })), (isSteps ? [groups[clampedStep]] : groups).map(group => (_jsx(FieldSection, { group: group, children: _jsx("div", { className: "grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2", children: renderFields(group.fields) }) }, group.key))), record?.external_url && (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2", children: _jsx("div", { className: "sm:col-span-2 min-w-0", children: _jsxs("a", { href: record.external_url, target: "_blank", rel: "noreferrer", className: "inline-flex items-center gap-1.5 text-sm text-primary hover:underline mt-1", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5" }), "Ver en ", record.external_provider ?? 'proveedor externo'] }) }) }))] }), !isCreate && record && relations.length > 0 && (_jsx("div", { className: "mt-6", children: _jsx(DynamicRelations, { record: record, relations: relations, lineSubtable: true, embedOnly: true, canCreate: mode === 'edit', canEdit: mode === 'edit', canDelete: mode === 'edit', onChange: handleChildChange }) }))] }) }) }) })) : null }), _jsxs(DialogFooter, { className: "p-4 border-t shrink-0 sm:justify-between", children: [isView && onOpenFullPage ? (_jsxs(Button, { variant: "ghost", size: "sm", className: "text-muted-foreground", onClick: () => { onOpenChange(false); onOpenFullPage(); }, children: [_jsx(ExternalLink, { className: "mr-1.5 h-3.5 w-3.5" }), "Ver p\u00E1gina completa"] })) : _jsx("span", {}), _jsxs("div", { className: "flex items-center gap-2", children: [isSteps && clampedStep > 0 ? (_jsx(Button, { variant: "outline", onClick: goBackStep, disabled: saving || deleting, children: "Anterior" })) : (_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: saving || deleting, children: config.cancelLabel })), isView && onDelete && (_jsxs(Button, { variant: "destructive", onClick: handleDelete, disabled: deleting || loading, children: [deleting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), deleting ? 'Eliminando...' : 'Eliminar'] })), isView && onEdit && (_jsx(Button, { onClick: onEdit, disabled: deleting || loading, children: "Editar" })), isEditable && isSteps && !isLastStep && (_jsx(Button, { type: "button", onClick: goNextStep, disabled: saving || loading, children: "Siguiente" })), isEditable && (!isSteps || isLastStep) && (_jsxs(Button, { type: "submit", form: "dynamic-record-form", disabled: saving || loading, children: [saving && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), saving ? config.submittingLabel : config.submitLabel] }))] })] })] }) }) }));
702
+ return (_jsx(RecordDialogModelContext.Provider, { value: model, children: _jsx(Dialog, { open: open, onOpenChange: onOpenChange, nestedInlineCreateSelf: nestedInlineCreateSelf, children: _jsxs(DialogContent, { className: "sm:max-w-2xl max-h-[90dvh] flex flex-col p-0 gap-0 overflow-hidden", style: { maxHeight: '90dvh' }, children: [_jsxs(DialogHeader, { className: "p-6 pb-4 border-b shrink-0", children: [_jsx(DialogTitle, { children: title }), _jsx(DialogDescription, { children: config.description })] }), _jsx("div", { className: "flex-1 overflow-y-auto p-6", children: loading ? (_jsx(LoadingSkeleton, {})) : modalMeta ? (_jsx(ModelContext.Provider, { value: model, children: _jsx(ImageUrlContext.Provider, { value: getImageUrl, children: _jsx(TimeZoneContext.Provider, { value: timeZone, children: _jsxs(CurrencyContext.Provider, { value: currency, children: [_jsxs("form", { id: formId, onSubmit: handleSubmit, className: "grid gap-y-4", children: [isSteps && (_jsx(WizardProgress, { groups: groups, stepIndex: clampedStep })), (isSteps ? [groups[clampedStep]] : groups).map(group => (_jsx(FieldSection, { group: group, children: _jsx("div", { className: "grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2", children: renderFields(group.fields) }) }, group.key))), record?.external_url && (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2", children: _jsx("div", { className: "sm:col-span-2 min-w-0", children: _jsxs("a", { href: record.external_url, target: "_blank", rel: "noreferrer", className: "inline-flex items-center gap-1.5 text-sm text-primary hover:underline mt-1", children: [_jsx(ExternalLink, { className: "h-3.5 w-3.5" }), "Ver en ", record.external_provider ?? 'proveedor externo'] }) }) }))] }), !isCreate && record && relations.length > 0 && (_jsx("div", { className: "mt-6", children: _jsx(DynamicRelations, { record: record, relations: relations, lineSubtable: true, embedOnly: true, canCreate: mode === 'edit', canEdit: mode === 'edit', canDelete: mode === 'edit', onChange: handleChildChange }) }))] }) }) }) })) : null }), _jsxs(DialogFooter, { className: "p-4 border-t shrink-0 sm:justify-between", children: [isView && onOpenFullPage ? (_jsxs(Button, { variant: "ghost", size: "sm", className: "text-muted-foreground", onClick: () => { onOpenChange(false); onOpenFullPage(); }, children: [_jsx(ExternalLink, { className: "mr-1.5 h-3.5 w-3.5" }), "Ver p\u00E1gina completa"] })) : _jsx("span", {}), _jsxs("div", { className: "flex items-center gap-2", children: [isSteps && clampedStep > 0 ? (_jsx(Button, { variant: "outline", onClick: goBackStep, disabled: saving || deleting, children: "Anterior" })) : (_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: saving || deleting, children: config.cancelLabel })), isView && onDelete && (_jsxs(Button, { variant: "destructive", onClick: handleDelete, disabled: deleting || loading, children: [deleting && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), deleting ? 'Eliminando...' : 'Eliminar'] })), isView && onEdit && (_jsx(Button, { onClick: onEdit, disabled: deleting || loading, children: "Editar" })), isEditable && isSteps && !isLastStep && (_jsx(Button, { type: "button", onClick: goNextStep, disabled: saving || loading, children: "Siguiente" })), isEditable && (!isSteps || isLastStep) && (_jsxs(Button, { type: "submit", form: formId, disabled: saving || loading, children: [saving && _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }), saving ? config.submittingLabel : config.submitLabel] }))] })] })] }) }) }));
672
703
  }
673
704
  function LoadingSkeleton() {
674
705
  return (_jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4", children: Array.from({ length: 6 }).map((_, i) => (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Skeleton, { className: "h-3.5 w-24" }), _jsx(Skeleton, { className: "h-9 w-full" })] }, i))) }));
@@ -682,7 +713,7 @@ function FieldRow({ field, record, value, mode, onChange, error, locked }) {
682
713
  // the rich read-only renderer. `locked` forces the same disabled rendering on
683
714
  // CREATE for a caller-specified field (see `lockedFields`).
684
715
  const isEditReadonly = (mode === 'edit' && !!field.readonly) || !!locked;
685
- return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs(Label, { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: [field.label, field.required && mode !== 'view' && !isEditReadonly && (_jsx("span", { className: "text-destructive ml-0.5", children: "*" }))] }), mode === 'view' ? (_jsx(ViewValue, { field: field, value: value, record: record })) : isEditReadonly ? (_jsx(ReadonlyEditField, { field: field, value: value })) : (_jsx(EditField, { field: field, value: value, onChange: onChange, record: record })), error && mode !== 'view' && (_jsx("p", { className: "text-destructive text-xs mt-1", children: error }))] }));
716
+ return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsxs(Label, { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide", children: [field.label, field.required && mode !== 'view' && !isEditReadonly && (_jsx("span", { className: "text-destructive ml-0.5", children: "*" }))] }), mode === 'view' ? (_jsx(ViewValue, { field: field, value: value, record: record })) : isEditReadonly ? (_jsx(ReadonlyEditField, { field: field, value: value })) : (_jsx(EditField, { field: field, value: value, onChange: onChange, record: record, invalid: !!error })), error && mode !== 'view' && (_jsx("p", { className: "text-destructive text-xs mt-1", children: error }))] }));
686
717
  }
687
718
  // ReadonlyEditField — the edit-mode rendering of a `readonly` (system-generated)
688
719
  // field: a disabled, muted input that shows the current value without allowing
@@ -1047,7 +1078,10 @@ function JsonObjectViewValue({ value }) {
1047
1078
  return (_jsxs("div", { className: "min-w-0", children: [_jsx("dt", { className: "text-[11px] font-medium uppercase tracking-wide text-muted-foreground", children: label }), _jsx("dd", { className: "mt-0.5 break-words text-foreground", children: isNest ? (_jsx("pre", { className: "max-h-48 overflow-auto rounded-md bg-muted/40 p-2 text-[11px] leading-relaxed whitespace-pre-wrap", children: JSON.stringify(raw, null, 2) })) : raw === null || raw === '' ? (_jsx("span", { className: "text-muted-foreground", children: "\u2014" })) : (String(raw)) })] }, key));
1048
1079
  }) }));
1049
1080
  }
1050
- export function EditField({ field, value, onChange, record }) {
1081
+ export function EditField({ field, value, onChange, record, invalid }) {
1082
+ const invalidCls = invalid
1083
+ ? 'border-destructive ring-1 ring-destructive/30 focus-visible:ring-destructive aria-invalid:border-destructive'
1084
+ : undefined;
1051
1085
  const { t, i18n } = useTranslation();
1052
1086
  const editFieldImageUrl = useContext(ImageUrlContext);
1053
1087
  const dialogModel = useContext(RecordDialogModelContext);
@@ -1062,7 +1096,7 @@ export function EditField({ field, value, onChange, record }) {
1062
1096
  return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [_jsx(Switch, { checked: !!value, onCheckedChange: onChange }), _jsx("span", { className: "text-sm text-muted-foreground", children: value ? 'Sí' : 'No' })] }));
1063
1097
  }
1064
1098
  if (field.type === 'textarea') {
1065
- return (_jsx(Textarea, { value: value ?? '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, rows: 4 }));
1099
+ return (_jsx(Textarea, { value: value ?? '', onChange: (e) => onChange(e.target.value), placeholder: field.placeholder, rows: 4, "aria-invalid": invalid || undefined, className: invalidCls }));
1066
1100
  }
1067
1101
  // Icon picker: the kernel serves `type: 'icon'` (or an explicit
1068
1102
  // `widget: 'icon'`) when the manifest column declares `widget: "icon"`.
@@ -1103,7 +1137,7 @@ export function EditField({ field, value, onChange, record }) {
1103
1137
  // backend-injected FK sibling, key without `_id`) so an existing
1104
1138
  // selection shows the label, not the raw uuid — without waiting
1105
1139
  // for the popover to open and fetch a page.
1106
- seedOption: fkSeedOption(field, value, record), hideCreate: hideSelfCreate }));
1140
+ seedOption: fkSeedOption(field, value, record), hideCreate: hideSelfCreate, invalid: invalid }));
1107
1141
  }
1108
1142
  if (field.type === 'search' && field.searchEndpoint) {
1109
1143
  return _jsx(SearchField, { field: field, value: value, onChange: onChange });
@@ -1112,7 +1146,7 @@ export function EditField({ field, value, onChange, record }) {
1112
1146
  return _jsx(SearchField, { field: { ...field, type: 'search' }, value: value, onChange: onChange });
1113
1147
  }
1114
1148
  if (field.type === 'select' && field.options?.length) {
1115
- return (_jsxs(Select, { value: String(value ?? ''), onValueChange: onChange, children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, { placeholder: "Seleccionar..." }) }), _jsx(SelectContent, { children: field.options.map(opt => (_jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))) })] }));
1149
+ return (_jsxs(Select, { value: String(value ?? ''), onValueChange: onChange, children: [_jsx(SelectTrigger, { className: cn("w-full", invalidCls), "aria-invalid": invalid || undefined, children: _jsx(SelectValue, { placeholder: "Seleccionar..." }) }), _jsx(SelectContent, { children: field.options.map(opt => (_jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))) })] }));
1116
1150
  }
1117
1151
  if (field.type === 'color') {
1118
1152
  return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { type: "color", value: value || '#6366f1', onChange: (e) => onChange(e.target.value), className: "h-9 w-14 cursor-pointer rounded-md border p-1" }), _jsx(Input, { value: value || '', onChange: (e) => onChange(e.target.value), placeholder: "#6366f1", className: "flex-1 h-9" })] }));
@@ -1133,7 +1167,7 @@ export function EditField({ field, value, onChange, record }) {
1133
1167
  : field.type === 'email'
1134
1168
  ? 'email'
1135
1169
  : 'text';
1136
- return _jsx(ScannableRecordInput, { field: field, value: value, onChange: onChange, inputType: inputType });
1170
+ return _jsx(ScannableRecordInput, { field: field, value: value, onChange: onChange, inputType: inputType, invalid: invalid, className: invalidCls });
1137
1171
  }
1138
1172
  /**
1139
1173
  * Input de texto/número del modal de crear/editar registro con opción de ESCANEO
@@ -1146,14 +1180,14 @@ export function EditField({ field, value, onChange, record }) {
1146
1180
  * eso el escaneo se cablea también acá (si no, el auto-CRUD de crear producto
1147
1181
  * nunca mostraba el botón).
1148
1182
  */
1149
- function ScannableRecordInput({ field, value, onChange, inputType, }) {
1183
+ function ScannableRecordInput({ field, value, onChange, inputType, invalid, className, }) {
1150
1184
  const [scanOpen, setScanOpen] = useState(false);
1151
1185
  // El botón de escaneo aparece siempre que el campo declara `scan` (como el
1152
1186
  // POS), sin esconderse por `isCameraScanSupported()`. El BarcodeScanner ya
1153
1187
  // muestra un mensaje amable cuando el navegador no tiene cámara.
1154
1188
  const scanEnabled = !!(field.scan ??
1155
1189
  field.scannable);
1156
- const input = (_jsx(Input, { type: inputType, value: value ?? '', onChange: (e) => onChange(inputType === 'number' ? (e.target.value === '' ? '' : Number(e.target.value)) : e.target.value), placeholder: field.placeholder }));
1190
+ const input = (_jsx(Input, { type: inputType, value: value ?? '', onChange: (e) => onChange(inputType === 'number' ? (e.target.value === '' ? '' : Number(e.target.value)) : e.target.value), placeholder: field.placeholder, "aria-invalid": invalid || undefined, className: className }));
1157
1191
  if (!scanEnabled)
1158
1192
  return input;
1159
1193
  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),
@@ -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"}