@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 +34 -0
- package/dist/dialogs/create-record-dialog.d.ts +1 -1
- package/dist/dialogs/create-record-dialog.d.ts.map +1 -1
- package/dist/dialogs/create-record-dialog.js +2 -2
- package/dist/dialogs/dynamic-record.d.ts +7 -1
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +53 -13
- package/dist/dialogs/types.d.ts +9 -0
- package/dist/dialogs/types.d.ts.map +1 -1
- package/dist/entity-select.d.ts +14 -1
- package/dist/entity-select.d.ts.map +1 -1
- package/dist/entity-select.js +2 -2
- package/package.json +3 -3
- package/src/__tests__/dynamic-record-nested-form-id.test.tsx +78 -0
- package/src/dialogs/create-record-dialog.tsx +2 -0
- package/src/dialogs/dynamic-record.tsx +78 -13
- package/src/dialogs/types.ts +9 -0
- package/src/entity-select.tsx +18 -0
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,+
|
|
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
|
-
|
|
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
|
-
|
|
497
|
-
|
|
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
|
-
|
|
519
|
-
|
|
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
|
-
|
|
646
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
package/dist/dialogs/types.d.ts
CHANGED
|
@@ -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"}
|
package/dist/entity-select.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/entity-select.js
CHANGED
|
@@ -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.
|
|
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-
|
|
41
|
-
"@asteby/metacore-
|
|
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
|
-
|
|
778
|
-
|
|
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
|
-
|
|
800
|
-
|
|
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
|
-
|
|
946
|
-
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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.
|
package/src/dialogs/types.ts
CHANGED
|
@@ -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
|
}
|
package/src/entity-select.tsx
CHANGED
|
@@ -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>
|