@asteby/metacore-runtime-react 33.0.0 → 33.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 33.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 921a74c: Crear una categoría (u otro registro) desde el "+" de un modal ya no
8
+ valida el formulario de atrás. Cada diálogo tiene su propio `form` id, y
9
+ el toast de validación lista los mensajes del validator.
10
+
11
+ ## 33.1.0
12
+
13
+ ### Minor Changes
14
+
15
+ - 9eab4e0: `EntitySelect` now accepts `createDefaults` and `lockedCreateFields` to seed
16
+ and lock fields in its inline "+" create dialog. Motivating case: the POS
17
+ vehicle picker creating a vehicle for an already-selected customer — the
18
+ customer field should come pre-filled and not be changeable from that
19
+ context, instead of the user having to search/select it again (and risking
20
+ attaching the vehicle to the wrong customer).
21
+
22
+ - `createDefaults?: Record<string, unknown>` — seeds the create dialog's
23
+ form (passed through as `CreateRecordDialog.defaults`).
24
+ - `lockedCreateFields?: string[]` — keys from `createDefaults` that render
25
+ visible-but-disabled on create instead of editable (passed through as the
26
+ new `CreateRecordDialog.lockedFields` / `DynamicRecordDialog.lockedFields`).
27
+
28
+ New `lockedFields` support in `DynamicRecordDialog`/`CreateRecordDialog`:
29
+ unlike a `readonly` field (excluded entirely on create), a locked field stays
30
+ visible so the user sees what value is being submitted, rendered through the
31
+ same `ReadonlyEditField` used for edit-mode readonly fields. Also fixes
32
+ `ReadonlyEditField` for relation/`Ref` fields (readonly-on-edit and now
33
+ locked-on-create): it previously rendered the raw foreign id, and now
34
+ resolves and shows the related record's label like `RelationViewValue`
35
+ already does in view mode.
36
+
3
37
  ## 33.0.0
4
38
 
5
39
  ### Patch Changes
@@ -1,3 +1,3 @@
1
1
  import type { CreateRecordDialogProps } from './types';
2
- export declare function CreateRecordDialog({ modelKey, open, onOpenChange, recordId, endpoint, schema, defaults, onCreate, onUpdate, onSaved, }: CreateRecordDialogProps): import("react").JSX.Element;
2
+ export declare function CreateRecordDialog({ modelKey, open, onOpenChange, recordId, endpoint, schema, defaults, lockedFields, onCreate, onUpdate, onSaved, }: CreateRecordDialogProps): import("react").JSX.Element;
3
3
  //# sourceMappingURL=create-record-dialog.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"create-record-dialog.d.ts","sourceRoot":"","sources":["../../src/dialogs/create-record-dialog.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAA;AAEtD,wBAAgB,kBAAkB,CAAC,EAC/B,QAAQ,EACR,IAAI,EACJ,YAAY,EACZ,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,OAAO,GACV,EAAE,uBAAuB,+BAiBzB"}
1
+ {"version":3,"file":"create-record-dialog.d.ts","sourceRoot":"","sources":["../../src/dialogs/create-record-dialog.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAA;AAEtD,wBAAgB,kBAAkB,CAAC,EAC/B,QAAQ,EACR,IAAI,EACJ,YAAY,EACZ,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,QAAQ,EACR,QAAQ,EACR,OAAO,GACV,EAAE,uBAAuB,+BAkBzB"}
@@ -14,7 +14,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
14
14
  * with the default `/dynamic/${modelKey}` endpoint convention.
15
15
  */
16
16
  import { DynamicRecordDialog } from './dynamic-record';
17
- export function CreateRecordDialog({ modelKey, open, onOpenChange, recordId, endpoint, schema, defaults, onCreate, onUpdate, onSaved, }) {
17
+ export function CreateRecordDialog({ modelKey, open, onOpenChange, recordId, endpoint, schema, defaults, lockedFields, onCreate, onUpdate, onSaved, }) {
18
18
  const mode = recordId ? 'edit' : 'create';
19
- return (_jsx(DynamicRecordDialog, { open: open, onOpenChange: onOpenChange, mode: mode, model: modelKey, recordId: recordId, endpoint: endpoint, schema: schema, defaults: defaults, onCreate: onCreate, onUpdate: onUpdate, onSaved: onSaved }));
19
+ return (_jsx(DynamicRecordDialog, { open: open, onOpenChange: onOpenChange, mode: mode, model: modelKey, recordId: recordId, endpoint: endpoint, schema: schema, defaults: defaults, lockedFields: lockedFields, onCreate: onCreate, onUpdate: onUpdate, onSaved: onSaved }));
20
20
  }
@@ -122,6 +122,12 @@ export interface DynamicRecordDialogProps {
122
122
  * `mode` is `'edit'` or `'view'` (those fetch from the record endpoint).
123
123
  */
124
124
  defaults?: Record<string, any>;
125
+ /**
126
+ * Field keys that render locked (visible, disabled, seeded from
127
+ * `defaults`) on create instead of editable. Ignored outside create mode.
128
+ * See `CreateRecordDialogProps.lockedFields` for the rationale.
129
+ */
130
+ lockedFields?: string[];
125
131
  /**
126
132
  * Optional pre-fetched metadata. When provided the dialog skips the
127
133
  * `/metadata/modal/:model` request and uses this shape directly.
@@ -185,7 +191,7 @@ export declare function fkSeedOption(field: FieldDef, value: any, record: any):
185
191
  export declare function isMoneyField(field: FieldDef, value: any): boolean;
186
192
  export declare function filterVisibleFields(fields: FieldDef[] | undefined, mode: 'view' | 'edit' | 'create', formValues?: Record<string, any>): FieldDef[];
187
193
  export declare function stripHiddenFieldValues(values: Record<string, any>, fields: FieldDef[] | undefined, mode: 'view' | 'edit' | 'create'): Record<string, any>;
188
- export declare function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl, timeZone, currency, onChange, }: DynamicRecordDialogProps): import("react").JSX.Element;
194
+ export declare function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, lockedFields, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl, timeZone, currency, onChange, }: DynamicRecordDialogProps): import("react").JSX.Element;
189
195
  export declare function ReadonlyEditField({ field, value }: {
190
196
  field: FieldDef;
191
197
  value: any;
@@ -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. */
@@ -284,9 +284,19 @@ export function stripHiddenFieldValues(values, fields, mode) {
284
284
  }
285
285
  return out;
286
286
  }
287
- export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl = identityImageUrl, timeZone, currency, onChange, }) {
287
+ function toastValidationFailed(t, lang, localized) {
288
+ toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }), {
289
+ description: Object.values(localized).filter(Boolean).join('\n'),
290
+ });
291
+ }
292
+ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId, endpoint, onSaved, onCreate, onUpdate, defaults, lockedFields, schema, onDelete, onEdit, onOpenFullPage, initialRecord, getImageUrl = identityImageUrl, timeZone, currency, onChange, }) {
288
293
  const api = useApi();
289
294
  const { t, i18n } = useTranslation();
295
+ // Unique per dialog instance. The footer submit lives OUTSIDE <form>, so
296
+ // it binds via `form={id}`. A hardcoded id made nested create (product +
297
+ // "Crear categoría") submit the PARENT form — toast "Revisa los campos
298
+ // marcados" with no marks on the inner modal.
299
+ const formId = useId();
290
300
  const [modalMeta, setModalMeta] = useState(schema ? schema : null);
291
301
  const [relations, setRelations] = useState([]);
292
302
  const [record, setRecord] = useState(null);
@@ -493,8 +503,9 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
493
503
  const labels = {};
494
504
  for (const f of modalMeta?.fields ?? [])
495
505
  labels[f.key] = labelForKey(f.key);
496
- setFieldErrors(localizeFieldErrorMap(map, t, { labels, language: lang }));
497
- toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }));
506
+ const localized = localizeFieldErrorMap(map, t, { labels, language: lang });
507
+ setFieldErrors(localized);
508
+ toastValidationFailed(t, lang, localized);
498
509
  return;
499
510
  }
500
511
  toastServerError(err, { t, language: lang, fallback: t('dynamic.save_error', { defaultValue: 'No se pudo guardar' }) });
@@ -515,8 +526,9 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
515
526
  const labels = {};
516
527
  for (const f of visible)
517
528
  labels[f.key] = t(f.label, { defaultValue: f.label });
518
- setFieldErrors(localizeFieldErrorMap(bag, t, { labels, language: lang }));
519
- toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }));
529
+ const localized = localizeFieldErrorMap(bag, t, { labels, language: lang });
530
+ setFieldErrors(localized);
531
+ toastValidationFailed(t, lang, localized);
520
532
  return;
521
533
  }
522
534
  }
@@ -622,7 +634,7 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
622
634
  const isFullWidth = field.type === 'textarea' ||
623
635
  field.widget === 'textarea' ||
624
636
  field.widget === 'richtext';
625
- return (_jsx(FieldCell, { fullWidth: isFullWidth, children: _jsx(FieldRow, { field: field, record: record, value: formValues[field.key] ?? '', mode: mode, error: fieldErrors[field.key], onChange: val => {
637
+ return (_jsx(FieldCell, { fullWidth: isFullWidth, children: _jsx(FieldRow, { field: field, record: record, value: formValues[field.key] ?? '', mode: mode, locked: isCreate && !!lockedFields?.includes(field.key), error: fieldErrors[field.key], onChange: val => {
626
638
  setFormValues((prev) => ({ ...prev, [field.key]: val }));
627
639
  setFieldErrors(prev => {
628
640
  if (!prev[field.key])
@@ -642,27 +654,29 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
642
654
  const labels = {};
643
655
  for (const f of step?.fields ?? [])
644
656
  labels[f.key] = t(f.label, { defaultValue: f.label });
645
- setFieldErrors(localizeFieldErrorMap(bag, t, { labels, language: lang }));
646
- toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }));
657
+ const localized = localizeFieldErrorMap(bag, t, { labels, language: lang });
658
+ setFieldErrors(localized);
659
+ toastValidationFailed(t, lang, localized);
647
660
  return;
648
661
  }
649
662
  setFieldErrors({});
650
663
  setStepIndex(Math.min(clampedStep + 1, groups.length - 1));
651
664
  };
652
665
  const goBackStep = () => setStepIndex(Math.max(clampedStep - 1, 0));
653
- return (_jsx(RecordDialogModelContext.Provider, { value: model, children: _jsx(Dialog, { open: open, onOpenChange: onOpenChange, 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] }))] })] })] }) }) }));
666
+ return (_jsx(RecordDialogModelContext.Provider, { value: model, children: _jsx(Dialog, { open: open, onOpenChange: onOpenChange, 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] }))] })] })] }) }) }));
654
667
  }
655
668
  function LoadingSkeleton() {
656
669
  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))) }));
657
670
  }
658
- function FieldRow({ field, record, value, mode, onChange, error }) {
671
+ function FieldRow({ field, record, value, mode, onChange, error, locked }) {
659
672
  // A `readonly` field is server/system-generated (e.g. the GitHub addon's
660
673
  // `number`/`github_url`, filled by the API after the outbound create). On
661
674
  // CREATE it is excluded from the form entirely (see `visibleFields`); on EDIT
662
675
  // it stays visible but is NOT editable — rendered as a disabled, muted input
663
676
  // so the user sees its value without being able to change it. View mode keeps
664
- // the rich read-only renderer.
665
- const isEditReadonly = mode === 'edit' && !!field.readonly;
677
+ // the rich read-only renderer. `locked` forces the same disabled rendering on
678
+ // CREATE for a caller-specified field (see `lockedFields`).
679
+ const isEditReadonly = (mode === 'edit' && !!field.readonly) || !!locked;
666
680
  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 }))] }));
667
681
  }
668
682
  // ReadonlyEditField — the edit-mode rendering of a `readonly` (system-generated)
@@ -674,9 +688,35 @@ export function ReadonlyEditField({ field, value }) {
674
688
  if (field.type === 'boolean' || typeof value === 'boolean') {
675
689
  return (_jsxs("div", { className: "flex items-center gap-2 py-1", children: [_jsx(Switch, { checked: !!value, disabled: true }), _jsx("span", { className: "text-sm text-muted-foreground", children: value ? 'Sí' : 'No' })] }));
676
690
  }
691
+ const fieldRef = getFieldRef(field);
692
+ if (fieldRef || field.searchEndpoint) {
693
+ return _jsx(ReadonlyRelationField, { field: field, value: value, fieldRef: fieldRef });
694
+ }
677
695
  const display = formatDisplayValue(value, field);
678
696
  return _jsx(Input, { value: display === '—' ? '' : display, disabled: true, readOnly: true, className: "text-muted-foreground" });
679
697
  }
698
+ // ReadonlyRelationField — a locked/readonly FK field (customer_id, category_id…)
699
+ // resolves the record's label instead of showing the raw id, mirroring
700
+ // RelationViewValue's lookup but rendered as a disabled input to match the rest
701
+ // of ReadonlyEditField. Without this, a locked relation field (e.g. `lockedFields`
702
+ // seeding a POS-selected customer into a vehicle create modal) would show a bare
703
+ // UUID — the exact readability bug this dialog otherwise avoids elsewhere.
704
+ function ReadonlyRelationField({ field, value, fieldRef, }) {
705
+ const rawVal = value && typeof value === 'object' ? (value.value ?? value.id) : value;
706
+ const needResolve = fieldRef != null || !!field.searchEndpoint;
707
+ const { options } = useOptionsResolver({
708
+ modelKey: '',
709
+ fieldKey: 'id',
710
+ ref: fieldRef,
711
+ endpoint: fieldRef ? undefined : field.searchEndpoint,
712
+ query: '',
713
+ limit: 50,
714
+ enabled: needResolve && rawVal != null && rawVal !== '',
715
+ });
716
+ const resolved = options.find(o => String(o.id) === String(rawVal));
717
+ const display = resolved?.label ?? (rawVal != null && rawVal !== '' ? String(rawVal) : '');
718
+ return _jsx(Input, { value: display, disabled: true, readOnly: true, className: "text-muted-foreground" });
719
+ }
680
720
  // RelationViewValue — read-only FK lead. Resolves the relation's label + image
681
721
  // from (1) the sibling object the table served, then (2) the canonical options
682
722
  // endpoint, and renders an OptionLead (thumbnail / icon / color dot) + label.
@@ -95,6 +95,15 @@ export interface CreateRecordDialogProps extends RecordDialogProps {
95
95
  onUpdate?: (recordId: string, data: Record<string, unknown>) => Promise<CreateResult>;
96
96
  /** Default values seeded into the form on create. */
97
97
  defaults?: Record<string, unknown>;
98
+ /**
99
+ * Field keys that must render locked (visible, disabled, seeded from
100
+ * `defaults`) on create instead of editable — e.g. a parent id the caller
101
+ * already knows and does not want the user to change. Unlike a `readonly`
102
+ * field (which is excluded entirely on create, see `filterVisibleFields`),
103
+ * a locked field stays visible so the user sees what value is being sent.
104
+ * Ignored outside create mode.
105
+ */
106
+ lockedFields?: string[];
98
107
  /** Notification when a create or update succeeds. */
99
108
  onSaved?: () => void;
100
109
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/dialogs/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH;;;;GAIG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAA;AAE7B;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,KAAK,CAAC;QACX,GAAG,EAAE,MAAM,CAAA;QACX,KAAK,EAAE,MAAM,CAAA;QACb,IAAI,EAAE,MAAM,CAAA;QACZ,QAAQ,CAAC,EAAE,OAAO,CAAA;QAClB,YAAY,CAAC,EAAE,OAAO,CAAA;QACtB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;QAClB,MAAM,CAAC,EAAE,OAAO,CAAA;QAEhB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAA;KAC3B,CAAC,CAAA;CACL;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAA;AAEhD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAC9B,uEAAuE;IACvE,QAAQ,EAAE,QAAQ,CAAA;IAClB,6BAA6B;IAC7B,IAAI,EAAE,OAAO,CAAA;IACb,6BAA6B;IAC7B,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;CACvB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,uBAAwB,SAAQ,iBAAiB;IAC9D,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,YAAY,CAAC,CAAA;IACnE;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,YAAY,CAAC,CAAA;IACrF,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,iBAAiB;IAC5D,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAA;IAChB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACjC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/dialogs/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH;;;;GAIG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAA;AAE7B;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,KAAK,CAAC;QACX,GAAG,EAAE,MAAM,CAAA;QACX,KAAK,EAAE,MAAM,CAAA;QACb,IAAI,EAAE,MAAM,CAAA;QACZ,QAAQ,CAAC,EAAE,OAAO,CAAA;QAClB,YAAY,CAAC,EAAE,OAAO,CAAA;QACtB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;QAClB,MAAM,CAAC,EAAE,OAAO,CAAA;QAEhB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAA;KAC3B,CAAC,CAAA;CACL;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAA;AAEhD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAC9B,uEAAuE;IACvE,QAAQ,EAAE,QAAQ,CAAA;IAClB,6BAA6B;IAC7B,IAAI,EAAE,OAAO,CAAA;IACb,6BAA6B;IAC7B,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;CACvB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,uBAAwB,SAAQ,iBAAiB;IAC9D,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,YAAY,CAAC,CAAA;IACnE;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,YAAY,CAAC,CAAA;IACrF,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAClC;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,qBAAsB,SAAQ,iBAAiB;IAC5D,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAA;IAChB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,IAAI,CAAA;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACjC"}
@@ -38,6 +38,19 @@ export interface EntitySelectProps {
38
38
  labelField?: string;
39
39
  /** Disable the whole control. */
40
40
  disabled?: boolean;
41
+ /**
42
+ * Values seeded into the create dialog's form (e.g. a parent id the caller
43
+ * already knows — a POS sale's selected customer, when creating a vehicle
44
+ * from the vehicle picker). Passed through to `CreateRecordDialog.defaults`.
45
+ */
46
+ createDefaults?: Record<string, unknown>;
47
+ /**
48
+ * Field keys from `createDefaults` that must also render locked (visible,
49
+ * disabled) on create instead of editable — e.g. don't let the user swap
50
+ * the customer while creating their vehicle from this picker. Passed
51
+ * through to `CreateRecordDialog.lockedFields`.
52
+ */
53
+ lockedCreateFields?: string[];
41
54
  }
42
- export declare function EntitySelect({ model, value, label, onSelect, fetcher, icon: Icon, placeholder, searchPlaceholder, emptyText, preload, canCreate, canEdit, endpoint, labelField, disabled, }: EntitySelectProps): import("react").JSX.Element;
55
+ export declare function EntitySelect({ model, value, label, onSelect, fetcher, icon: Icon, placeholder, searchPlaceholder, emptyText, preload, canCreate, canEdit, endpoint, labelField, disabled, createDefaults, lockedCreateFields, }: EntitySelectProps): import("react").JSX.Element;
43
56
  //# sourceMappingURL=entity-select.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"entity-select.d.ts","sourceRoot":"","sources":["../src/entity-select.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,cAAc,CAAA;AAiBvE,0EAA0E;AAC1E,MAAM,WAAW,kBAAkB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,iBAAiB;IAC9B,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,kEAAkE;IAClE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,sDAAsD;IACtD,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC3D,sFAAsF;IACtF,OAAO,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAA;IAE1E,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAA;CACrB;AAOD,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,KAAK,EACL,KAAK,EACL,QAAQ,EACR,OAAO,EACP,IAAI,EAAE,IAAI,EACV,WAA4B,EAC5B,iBAA6B,EAC7B,SAA4B,EAC5B,OAAe,EACf,SAAS,EACT,OAAO,EACP,QAAQ,EACR,UAAmB,EACnB,QAAgB,GACnB,EAAE,iBAAiB,+BAuNnB"}
1
+ {"version":3,"file":"entity-select.d.ts","sourceRoot":"","sources":["../src/entity-select.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAA2B,KAAK,UAAU,EAAE,MAAM,cAAc,CAAA;AAiBvE,0EAA0E;AAC1E,MAAM,WAAW,kBAAkB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,iBAAiB;IAC9B,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAA;IACb,uCAAuC;IACvC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,kEAAkE;IAClE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,sDAAsD;IACtD,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IAC3D,sFAAsF;IACtF,OAAO,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAA;IAE1E,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAElB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;CAChC;AAOD,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,KAAK,EACL,KAAK,EACL,QAAQ,EACR,OAAO,EACP,IAAI,EAAE,IAAI,EACV,WAA4B,EAC5B,iBAA6B,EAC7B,SAA4B,EAC5B,OAAe,EACf,SAAS,EACT,OAAO,EACP,QAAQ,EACR,UAAmB,EACnB,QAAgB,EAChB,cAAc,EACd,kBAAkB,GACrB,EAAE,iBAAiB,+BAyNnB"}
@@ -25,7 +25,7 @@ import { useApi } from './api-context';
25
25
  function capabilityNamespace(model) {
26
26
  return model.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
27
27
  }
28
- export function EntitySelect({ model, value, label, onSelect, fetcher, icon: Icon, placeholder = 'Seleccionar…', searchPlaceholder = 'Buscar…', emptyText = 'Sin resultados', preload = false, canCreate, canEdit, endpoint, labelField = 'name', disabled = false, }) {
28
+ export function EntitySelect({ model, value, label, onSelect, fetcher, icon: Icon, placeholder = 'Seleccionar…', searchPlaceholder = 'Buscar…', emptyText = 'Sin resultados', preload = false, canCreate, canEdit, endpoint, labelField = 'name', disabled = false, createDefaults, lockedCreateFields, }) {
29
29
  const can = useCan();
30
30
  const api = useApi();
31
31
  const ns = capabilityNamespace(model);
@@ -110,7 +110,7 @@ export function EntitySelect({ model, value, label, onSelect, fetcher, icon: Ico
110
110
  searchTerm.length >= minChars &&
111
111
  results.length === 0 && _jsx(CommandEmpty, { children: emptyText }), !isLoading && results.length > 0 && (_jsx(CommandGroup, { className: "max-h-64 overflow-auto", children: results.map((row) => (_jsxs(CommandItem, { value: row.value, onSelect: () => pick(row), className: "flex flex-col items-start gap-0.5", children: [_jsx("span", { className: "text-sm font-medium", children: row.label }), row.description && (_jsx("span", { className: "text-muted-foreground text-xs", children: row.description }))] }, row.value))) })), !isLoading && !preload && searchTerm.length < minChars && (_jsxs("div", { className: "text-muted-foreground flex flex-col items-center gap-1 py-6", children: [_jsx(Search, { className: "size-5" }), _jsx("span", { className: "text-xs", children: "Escribe al menos 2 caracteres" })] }))] })] }) })] }), value
112
112
  ? mayEdit && (_jsx(Button, { type: "button", variant: "outline", size: "icon", disabled: disabled, onClick: openEdit, "aria-label": "Editar", title: "Editar", className: "shrink-0", children: _jsx(Pencil, { className: "size-4" }) }))
113
- : mayCreate && (_jsx(Button, { type: "button", variant: "outline", size: "icon", disabled: disabled, onClick: openCreate, "aria-label": "Crear", title: "Crear", className: "shrink-0", children: _jsx(Plus, { className: "size-4" }) })), dialogOpen && (_jsx(CreateRecordDialog, { modelKey: model, open: dialogOpen, onOpenChange: setDialogOpen, recordId: dialogRecordId, endpoint: base, onCreate: async (data) => {
113
+ : mayCreate && (_jsx(Button, { type: "button", variant: "outline", size: "icon", disabled: disabled, onClick: openCreate, "aria-label": "Crear", title: "Crear", className: "shrink-0", children: _jsx(Plus, { className: "size-4" }) })), dialogOpen && (_jsx(CreateRecordDialog, { modelKey: model, open: dialogOpen, onOpenChange: setDialogOpen, recordId: dialogRecordId, endpoint: base, defaults: dialogRecordId ? undefined : createDefaults, lockedFields: dialogRecordId ? undefined : lockedCreateFields, onCreate: async (data) => {
114
114
  const res = await api.post(base, data);
115
115
  const rec = (res.data?.data ?? res.data);
116
116
  selectSaved(rec);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "33.0.0",
3
+ "version": "33.1.1",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,8 +37,8 @@
37
37
  "react-i18next": ">=13",
38
38
  "sonner": ">=1.7",
39
39
  "zustand": ">=5",
40
- "@asteby/metacore-sdk": "^3.6.0",
41
- "@asteby/metacore-ui": "^2.15.0"
40
+ "@asteby/metacore-ui": "^2.15.0",
41
+ "@asteby/metacore-sdk": "^3.6.0"
42
42
  },
43
43
  "peerDependenciesMeta": {
44
44
  "@tanstack/react-router": {
@@ -0,0 +1,78 @@
1
+ // @vitest-environment happy-dom
2
+ //
3
+ // Nested create (product modal + "Crear categoría" from the "+" picker) used
4
+ // to share id="dynamic-record-form". The inner footer's submit lives outside
5
+ // <form> and binds via form={id}, so HTML sent the click to the PARENT form
6
+ // — toast "Revisa los campos marcados" with no marks on the category fields.
7
+ import { afterEach, describe, expect, it, vi } from 'vitest'
8
+ import { cleanup, render } from '@testing-library/react'
9
+ import { I18nextProvider } from 'react-i18next'
10
+ import i18next from 'i18next'
11
+
12
+ import { DynamicRecordDialog } from '../dialogs/dynamic-record'
13
+ import { ApiProvider } from '../api-context'
14
+
15
+ afterEach(cleanup)
16
+
17
+ function makeI18n() {
18
+ const inst = i18next.createInstance()
19
+ void inst.init({
20
+ lng: 'es',
21
+ fallbackLng: 'es',
22
+ react: { useSuspense: false },
23
+ resources: { es: { translation: {} } },
24
+ })
25
+ return inst
26
+ }
27
+
28
+ const api = {
29
+ get: vi.fn(async () => ({ data: { data: {} } })),
30
+ post: async () => ({ data: { success: true } }),
31
+ put: async () => ({ data: { success: true } }),
32
+ delete: async () => ({ data: { success: true } }),
33
+ } as never
34
+
35
+ function schema(title: string) {
36
+ return {
37
+ title,
38
+ fields: [{ key: 'name', label: 'Nombre', type: 'text', required: true }],
39
+ }
40
+ }
41
+
42
+ describe('DynamicRecordDialog nested create — unique form ids', () => {
43
+ it('gives each open dialog its own form id so Crear does not submit the parent', () => {
44
+ render(
45
+ <I18nextProvider i18n={makeI18n()}>
46
+ <ApiProvider client={api}>
47
+ <DynamicRecordDialog
48
+ open
49
+ onOpenChange={() => {}}
50
+ mode="create"
51
+ model="Product"
52
+ schema={schema('Crear Producto') as never}
53
+ />
54
+ <DynamicRecordDialog
55
+ open
56
+ onOpenChange={() => {}}
57
+ mode="create"
58
+ model="Category"
59
+ schema={schema('Crear Categoría') as never}
60
+ />
61
+ </ApiProvider>
62
+ </I18nextProvider>,
63
+ )
64
+
65
+ const forms = [...document.querySelectorAll('form')]
66
+ expect(forms).toHaveLength(2)
67
+ const ids = forms.map(f => f.id)
68
+ expect(ids[0]).toBeTruthy()
69
+ expect(ids[1]).toBeTruthy()
70
+ expect(ids[0]).not.toBe(ids[1])
71
+ expect(ids).not.toContain('dynamic-record-form')
72
+
73
+ const submits = [...document.querySelectorAll('button[type="submit"]')]
74
+ expect(submits).toHaveLength(2)
75
+ const bound = submits.map(b => b.getAttribute('form'))
76
+ expect(new Set(bound)).toEqual(new Set(ids))
77
+ })
78
+ })
@@ -23,6 +23,7 @@ export function CreateRecordDialog({
23
23
  endpoint,
24
24
  schema,
25
25
  defaults,
26
+ lockedFields,
26
27
  onCreate,
27
28
  onUpdate,
28
29
  onSaved,
@@ -38,6 +39,7 @@ export function CreateRecordDialog({
38
39
  endpoint={endpoint}
39
40
  schema={schema}
40
41
  defaults={defaults}
42
+ lockedFields={lockedFields}
41
43
  onCreate={onCreate}
42
44
  onUpdate={onUpdate}
43
45
  onSaved={onSaved}
@@ -9,7 +9,7 @@
9
9
  // flows through <ApiProvider> from runtime-react. Host-specific runtime values —
10
10
  // the image-url resolver and the org IANA timezone — are passed as props so the
11
11
  // SDK stays transport- and host-agnostic.
12
- import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'
12
+ import { createContext, useCallback, useContext, useEffect, useId, useRef, useState } from 'react'
13
13
  import { useTranslation } from 'react-i18next'
14
14
  import type { ModelSchema } from './types'
15
15
 
@@ -53,7 +53,7 @@ import { es } from 'date-fns/locale'
53
53
  import { ExternalLink, Loader2, CalendarIcon, ChevronDown, Check, Upload, X as XIcon, ScanLine } from 'lucide-react'
54
54
  import { BarcodeScanner } from '../barcode-scanner'
55
55
  import { useApi } from '../api-context'
56
- import { toastServerError, extractFieldErrors, localizeFieldErrorMap } from '../server-error'
56
+ import { toastServerError, extractFieldErrors, localizeFieldErrorMap, type Translate } from '../server-error'
57
57
  import { validateValues, bagHasErrors } from '../validator'
58
58
  import { validationCatalog } from '../validation-catalog'
59
59
  import { DynamicSelectField, OptionLead, OptionThumb } from '../dynamic-select-field'
@@ -243,6 +243,12 @@ export interface DynamicRecordDialogProps {
243
243
  * `mode` is `'edit'` or `'view'` (those fetch from the record endpoint).
244
244
  */
245
245
  defaults?: Record<string, any>
246
+ /**
247
+ * Field keys that render locked (visible, disabled, seeded from
248
+ * `defaults`) on create instead of editable. Ignored outside create mode.
249
+ * See `CreateRecordDialogProps.lockedFields` for the rationale.
250
+ */
251
+ lockedFields?: string[]
246
252
  /**
247
253
  * Optional pre-fetched metadata. When provided the dialog skips the
248
254
  * `/metadata/modal/:model` request and uses this shape directly.
@@ -537,6 +543,12 @@ export function stripHiddenFieldValues(
537
543
  return out
538
544
  }
539
545
 
546
+ function toastValidationFailed(t: Translate, lang: string, localized: Record<string, string>) {
547
+ toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }), {
548
+ description: Object.values(localized).filter(Boolean).join('\n'),
549
+ })
550
+ }
551
+
540
552
  export function DynamicRecordDialog({
541
553
  open,
542
554
  onOpenChange,
@@ -548,6 +560,7 @@ export function DynamicRecordDialog({
548
560
  onCreate,
549
561
  onUpdate,
550
562
  defaults,
563
+ lockedFields,
551
564
  schema,
552
565
  onDelete,
553
566
  onEdit,
@@ -560,6 +573,11 @@ export function DynamicRecordDialog({
560
573
  }: DynamicRecordDialogProps) {
561
574
  const api = useApi()
562
575
  const { t, i18n } = useTranslation()
576
+ // Unique per dialog instance. The footer submit lives OUTSIDE <form>, so
577
+ // it binds via `form={id}`. A hardcoded id made nested create (product +
578
+ // "Crear categoría") submit the PARENT form — toast "Revisa los campos
579
+ // marcados" with no marks on the inner modal.
580
+ const formId = useId()
563
581
  const [modalMeta, setModalMeta] = useState<ModalMetadata | null>(
564
582
  schema ? (schema as ModalMetadata) : null,
565
583
  )
@@ -774,8 +792,9 @@ export function DynamicRecordDialog({
774
792
  if (map) {
775
793
  const labels: Record<string, string> = {}
776
794
  for (const f of modalMeta?.fields ?? []) labels[f.key] = labelForKey(f.key)
777
- setFieldErrors(localizeFieldErrorMap(map, t, { labels, language: lang }))
778
- toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }))
795
+ const localized = localizeFieldErrorMap(map, t, { labels, language: lang })
796
+ setFieldErrors(localized)
797
+ toastValidationFailed(t, lang, localized)
779
798
  return
780
799
  }
781
800
  toastServerError(err, { t, language: lang, fallback: t('dynamic.save_error', { defaultValue: 'No se pudo guardar' }) })
@@ -796,8 +815,9 @@ export function DynamicRecordDialog({
796
815
  if (bagHasErrors(bag)) {
797
816
  const labels: Record<string, string> = {}
798
817
  for (const f of visible) labels[f.key] = t(f.label, { defaultValue: f.label })
799
- setFieldErrors(localizeFieldErrorMap(bag, t, { labels, language: lang }))
800
- toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }))
818
+ const localized = localizeFieldErrorMap(bag, t, { labels, language: lang })
819
+ setFieldErrors(localized)
820
+ toastValidationFailed(t, lang, localized)
801
821
  return
802
822
  }
803
823
  }
@@ -919,6 +939,7 @@ export function DynamicRecordDialog({
919
939
  record={record}
920
940
  value={formValues[field.key] ?? ''}
921
941
  mode={mode}
942
+ locked={isCreate && !!lockedFields?.includes(field.key)}
922
943
  error={fieldErrors[field.key]}
923
944
  onChange={val => {
924
945
  setFormValues((prev: Record<string, any>) => ({ ...prev, [field.key]: val }))
@@ -942,8 +963,9 @@ export function DynamicRecordDialog({
942
963
  if (bagHasErrors(bag)) {
943
964
  const labels: Record<string, string> = {}
944
965
  for (const f of step?.fields ?? []) labels[f.key] = t(f.label, { defaultValue: f.label })
945
- setFieldErrors(localizeFieldErrorMap(bag, t, { labels, language: lang }))
946
- toast.error(t('validation.failed', { defaultValue: validationCatalog(lang).failed }))
966
+ const localized = localizeFieldErrorMap(bag, t, { labels, language: lang })
967
+ setFieldErrors(localized)
968
+ toastValidationFailed(t, lang, localized)
947
969
  return
948
970
  }
949
971
  setFieldErrors({})
@@ -976,7 +998,7 @@ export function DynamicRecordDialog({
976
998
  cell `min-w-0` so a long select/input value can't
977
999
  blow the two columns past the dialog width. */}
978
1000
  <form
979
- id="dynamic-record-form"
1001
+ id={formId}
980
1002
  onSubmit={handleSubmit}
981
1003
  className="grid gap-y-4"
982
1004
  >
@@ -1086,7 +1108,7 @@ export function DynamicRecordDialog({
1086
1108
  {isEditable && (!isSteps || isLastStep) && (
1087
1109
  <Button
1088
1110
  type="submit"
1089
- form="dynamic-record-form"
1111
+ form={formId}
1090
1112
  disabled={saving || loading}
1091
1113
  >
1092
1114
  {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
@@ -1122,16 +1144,24 @@ interface FieldRowProps {
1122
1144
  onChange: (val: any) => void
1123
1145
  /** Localized validation error for this field, shown in red under the input. */
1124
1146
  error?: string
1147
+ /**
1148
+ * Caller-forced lock (via `lockedFields`), independent of `field.readonly`.
1149
+ * Renders the same disabled/muted input as an edit-mode readonly field, but
1150
+ * applies on CREATE — where a plain `readonly` field would be excluded
1151
+ * instead. The seeded `value` (from `defaults`) still submits.
1152
+ */
1153
+ locked?: boolean
1125
1154
  }
1126
1155
 
1127
- function FieldRow({ field, record, value, mode, onChange, error }: FieldRowProps) {
1156
+ function FieldRow({ field, record, value, mode, onChange, error, locked }: FieldRowProps) {
1128
1157
  // A `readonly` field is server/system-generated (e.g. the GitHub addon's
1129
1158
  // `number`/`github_url`, filled by the API after the outbound create). On
1130
1159
  // CREATE it is excluded from the form entirely (see `visibleFields`); on EDIT
1131
1160
  // it stays visible but is NOT editable — rendered as a disabled, muted input
1132
1161
  // so the user sees its value without being able to change it. View mode keeps
1133
- // the rich read-only renderer.
1134
- const isEditReadonly = mode === 'edit' && !!field.readonly
1162
+ // the rich read-only renderer. `locked` forces the same disabled rendering on
1163
+ // CREATE for a caller-specified field (see `lockedFields`).
1164
+ const isEditReadonly = (mode === 'edit' && !!field.readonly) || !!locked
1135
1165
 
1136
1166
  return (
1137
1167
  <div className="flex flex-col gap-1.5">
@@ -1171,10 +1201,45 @@ export function ReadonlyEditField({ field, value }: { field: FieldDef; value: an
1171
1201
  </div>
1172
1202
  )
1173
1203
  }
1204
+ const fieldRef = getFieldRef(field as ActionFieldDef)
1205
+ if (fieldRef || field.searchEndpoint) {
1206
+ return <ReadonlyRelationField field={field} value={value} fieldRef={fieldRef} />
1207
+ }
1174
1208
  const display = formatDisplayValue(value, field)
1175
1209
  return <Input value={display === '—' ? '' : display} disabled readOnly className="text-muted-foreground" />
1176
1210
  }
1177
1211
 
1212
+ // ReadonlyRelationField — a locked/readonly FK field (customer_id, category_id…)
1213
+ // resolves the record's label instead of showing the raw id, mirroring
1214
+ // RelationViewValue's lookup but rendered as a disabled input to match the rest
1215
+ // of ReadonlyEditField. Without this, a locked relation field (e.g. `lockedFields`
1216
+ // seeding a POS-selected customer into a vehicle create modal) would show a bare
1217
+ // UUID — the exact readability bug this dialog otherwise avoids elsewhere.
1218
+ function ReadonlyRelationField({
1219
+ field,
1220
+ value,
1221
+ fieldRef,
1222
+ }: {
1223
+ field: FieldDef
1224
+ value: any
1225
+ fieldRef?: string
1226
+ }) {
1227
+ const rawVal = value && typeof value === 'object' ? (value.value ?? value.id) : value
1228
+ const needResolve = fieldRef != null || !!field.searchEndpoint
1229
+ const { options } = useOptionsResolver({
1230
+ modelKey: '',
1231
+ fieldKey: 'id',
1232
+ ref: fieldRef,
1233
+ endpoint: fieldRef ? undefined : field.searchEndpoint,
1234
+ query: '',
1235
+ limit: 50,
1236
+ enabled: needResolve && rawVal != null && rawVal !== '',
1237
+ })
1238
+ const resolved = options.find(o => String(o.id) === String(rawVal))
1239
+ const display = resolved?.label ?? (rawVal != null && rawVal !== '' ? String(rawVal) : '')
1240
+ return <Input value={display} disabled readOnly className="text-muted-foreground" />
1241
+ }
1242
+
1178
1243
  // RelationViewValue — read-only FK lead. Resolves the relation's label + image
1179
1244
  // from (1) the sibling object the table served, then (2) the canonical options
1180
1245
  // endpoint, and renders an OptionLead (thumbnail / icon / color dot) + label.
@@ -99,6 +99,15 @@ export interface CreateRecordDialogProps extends RecordDialogProps {
99
99
  onUpdate?: (recordId: string, data: Record<string, unknown>) => Promise<CreateResult>
100
100
  /** Default values seeded into the form on create. */
101
101
  defaults?: Record<string, unknown>
102
+ /**
103
+ * Field keys that must render locked (visible, disabled, seeded from
104
+ * `defaults`) on create instead of editable — e.g. a parent id the caller
105
+ * already knows and does not want the user to change. Unlike a `readonly`
106
+ * field (which is excluded entirely on create, see `filterVisibleFields`),
107
+ * a locked field stays visible so the user sees what value is being sent.
108
+ * Ignored outside create mode.
109
+ */
110
+ lockedFields?: string[]
102
111
  /** Notification when a create or update succeeds. */
103
112
  onSaved?: () => void
104
113
  }
@@ -75,6 +75,20 @@ export interface EntitySelectProps {
75
75
  labelField?: string
76
76
  /** Disable the whole control. */
77
77
  disabled?: boolean
78
+
79
+ /**
80
+ * Values seeded into the create dialog's form (e.g. a parent id the caller
81
+ * already knows — a POS sale's selected customer, when creating a vehicle
82
+ * from the vehicle picker). Passed through to `CreateRecordDialog.defaults`.
83
+ */
84
+ createDefaults?: Record<string, unknown>
85
+ /**
86
+ * Field keys from `createDefaults` that must also render locked (visible,
87
+ * disabled) on create instead of editable — e.g. don't let the user swap
88
+ * the customer while creating their vehicle from this picker. Passed
89
+ * through to `CreateRecordDialog.lockedFields`.
90
+ */
91
+ lockedCreateFields?: string[]
78
92
  }
79
93
 
80
94
  /** Lowercase model → permission capability namespace (Supplier → supplier). */
@@ -98,6 +112,8 @@ export function EntitySelect({
98
112
  endpoint,
99
113
  labelField = 'name',
100
114
  disabled = false,
115
+ createDefaults,
116
+ lockedCreateFields,
101
117
  }: EntitySelectProps) {
102
118
  const can = useCan()
103
119
  const api = useApi()
@@ -297,6 +313,8 @@ export function EntitySelect({
297
313
  onOpenChange={setDialogOpen}
298
314
  recordId={dialogRecordId}
299
315
  endpoint={base}
316
+ defaults={dialogRecordId ? undefined : createDefaults}
317
+ lockedFields={dialogRecordId ? undefined : lockedCreateFields}
300
318
  onCreate={async (data) => {
301
319
  const res = await api.post(base, data)
302
320
  const rec = (res.data?.data ?? res.data) as Record<string, unknown>