@asteby/metacore-runtime-react 29.2.0 → 29.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 29.2.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- b07391e: El escaneo por cámara (`scan` opt-in) ahora también funciona en el modal de **crear/editar registro** (auto-CRUD), no solo en los modales de acción. Su renderer de campos es SEPARADO del de dynamic-form, así que el botón de escaneo en el input de texto (ej. el CÓDIGO/SKU al crear producto o variante) se cableó también ahí — antes nunca aparecía en ese modal.
|
|
8
|
+
|
|
9
|
+
## 29.2.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 3358745: Modales de crear/editar y de acciones (incluida crear orden de compra con line-items) usan `90dvh` en vez de `90vh`: en móvil el `vh` contaba el alto detrás de la barra del navegador y empujaba el footer con **Guardar/Crear** fuera de lo visible. Con `dvh` el botón queda siempre alcanzable.
|
|
14
|
+
|
|
3
15
|
## 29.2.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -453,7 +453,7 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
453
453
|
: hasLineItems
|
|
454
454
|
? '820px'
|
|
455
455
|
: undefined;
|
|
456
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: 'flex max-h-[
|
|
456
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: 'flex max-h-[90dvh] flex-col overflow-hidden ' + (widthPx ? '' : 'sm:max-w-xl'), style: { maxHeight: '90dvh', ...(widthPx ? { maxWidth: widthPx, width: '95vw' } : {}) }, children: [_jsxs(DialogHeader, { className: "shrink-0", children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), tl(action.label)] }), action.confirmMessage && _jsx(DialogDescription, { children: action.confirmMessage })] }), _jsx("div", { className: "-mx-1 min-h-0 flex-1 overflow-y-auto px-1 py-4", children: _jsxs(FieldGrid, { children: [action.fields?.map((field) => {
|
|
457
457
|
const fullWidth = isLineItemsField(field) ||
|
|
458
458
|
resolveWidget(field) === 'textarea' ||
|
|
459
459
|
resolveWidget(field) === 'richtext';
|
|
@@ -570,7 +570,7 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
|
|
|
570
570
|
};
|
|
571
571
|
if (steps.length === 0)
|
|
572
572
|
return null;
|
|
573
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: 'flex max-h-[
|
|
573
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: 'flex max-h-[90dvh] flex-col overflow-hidden ' + (widthPx ? '' : 'sm:max-w-xl'), style: { maxHeight: '90dvh', ...(widthPx ? { maxWidth: widthPx, width: '95vw' } : {}) }, children: [_jsxs(DialogHeader, { className: "shrink-0", children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(DynamicIcon, { name: action.icon, className: "h-5 w-5" }), tl(action.label)] }), _jsxs("div", { className: "pt-2", children: [_jsx("div", { className: "flex items-center gap-1.5", role: "list", "aria-label": "progress", children: steps.map((s, i) => (_jsx("div", { role: "listitem", "aria-current": i === stepIndex ? 'step' : undefined, className: "h-1.5 flex-1 rounded-full", style: {
|
|
574
574
|
backgroundColor: i <= stepIndex ? (action.color || 'hsl(var(--primary))') : 'hsl(var(--muted))',
|
|
575
575
|
} }, i))) }), _jsxs(DialogDescription, { className: "pt-2", children: [t('common.step', { defaultValue: 'Paso' }), " ", stepIndex + 1, "/", steps.length, step?.title ? ` · ${tl(step.title)}` : ''] }), step?.description && (_jsx("p", { className: "pt-1 text-sm text-muted-foreground", children: tl(step.description) }))] })] }), _jsx("div", { className: "-mx-1 min-h-0 flex-1 overflow-y-auto px-1 py-4", children: _jsx(FieldGrid, { children: stepFields.map((field) => {
|
|
576
576
|
const fullWidth = isLineItemsField(field) ||
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"dynamic-record.d.ts","sourceRoot":"","sources":["../../src/dialogs/dynamic-record.tsx"],"names":[],"mappings":"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"}
|
|
@@ -17,7 +17,8 @@ import { cn } from '@asteby/metacore-ui/lib';
|
|
|
17
17
|
import { toast } from 'sonner';
|
|
18
18
|
import { format, parseISO } from 'date-fns';
|
|
19
19
|
import { es } from 'date-fns/locale';
|
|
20
|
-
import { ExternalLink, Loader2, CalendarIcon, ChevronDown, Check, Upload, X as XIcon } from 'lucide-react';
|
|
20
|
+
import { ExternalLink, Loader2, CalendarIcon, ChevronDown, Check, Upload, X as XIcon, ScanLine } from 'lucide-react';
|
|
21
|
+
import { BarcodeScanner, isCameraScanSupported } from '../barcode-scanner';
|
|
21
22
|
import { useApi } from '../api-context';
|
|
22
23
|
import { toastServerError, extractFieldErrors, localizeFieldIssue } from '../server-error';
|
|
23
24
|
import { DynamicSelectField, OptionLead, OptionThumb } from '../dynamic-select-field';
|
|
@@ -638,7 +639,7 @@ export function DynamicRecordDialog({ open, onOpenChange, mode, model, recordId,
|
|
|
638
639
|
setStepIndex(Math.min(clampedStep + 1, groups.length - 1));
|
|
639
640
|
};
|
|
640
641
|
const goBackStep = () => setStepIndex(Math.max(clampedStep - 1, 0));
|
|
641
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-2xl max-h-[
|
|
642
|
+
return (_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] }))] })] })] }) }));
|
|
642
643
|
}
|
|
643
644
|
function LoadingSkeleton() {
|
|
644
645
|
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))) }));
|
|
@@ -1010,7 +1011,28 @@ export function EditField({ field, value, onChange, record }) {
|
|
|
1010
1011
|
: field.type === 'email'
|
|
1011
1012
|
? 'email'
|
|
1012
1013
|
: 'text';
|
|
1013
|
-
return
|
|
1014
|
+
return _jsx(ScannableRecordInput, { field: field, value: value, onChange: onChange, inputType: inputType });
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Input de texto/número del modal de crear/editar registro con opción de ESCANEO
|
|
1018
|
+
* por cámara. Cuando el campo declara `scan` (alias `scannable`) y el navegador
|
|
1019
|
+
* lo soporta, agrega un botón de cámara: el código escaneado LLENA el input (ej.
|
|
1020
|
+
* el SKU/CÓDIGO al crear producto o variante) y el escáner queda abierto para
|
|
1021
|
+
* corregir/reintentar. Sin el flag o sin soporte, es un Input normal.
|
|
1022
|
+
*
|
|
1023
|
+
* NOTA: este modal renderer es SEPARADO del FieldRenderer de dynamic-form, por
|
|
1024
|
+
* eso el escaneo se cablea también acá (si no, el auto-CRUD de crear producto
|
|
1025
|
+
* nunca mostraba el botón).
|
|
1026
|
+
*/
|
|
1027
|
+
function ScannableRecordInput({ field, value, onChange, inputType, }) {
|
|
1028
|
+
const [scanOpen, setScanOpen] = useState(false);
|
|
1029
|
+
const scanEnabled = !!(field.scan ??
|
|
1030
|
+
field.scannable) &&
|
|
1031
|
+
isCameraScanSupported();
|
|
1032
|
+
const input = (_jsx(Input, { type: inputType, value: value ?? '', onChange: (e) => onChange(inputType === 'number' ? (e.target.value === '' ? '' : Number(e.target.value)) : e.target.value), placeholder: field.placeholder }));
|
|
1033
|
+
if (!scanEnabled)
|
|
1034
|
+
return input;
|
|
1035
|
+
return (_jsxs("div", { className: "flex w-full min-w-0 items-center gap-1.5", children: [_jsx("div", { className: "min-w-0 flex-1", children: input }), _jsx(Button, { type: "button", variant: "outline", size: "icon", className: "size-9 shrink-0", onClick: () => setScanOpen(true), title: "Escanear con la c\u00E1mara", "aria-label": "Escanear c\u00F3digo de barras con la c\u00E1mara", children: _jsx(ScanLine, { className: "size-4" }) }), _jsx(BarcodeScanner, { open: scanOpen, onClose: () => setScanOpen(false), onDetected: (code) => onChange(code), continuous: true, position: "fixed", title: `Escanear ${field.label ?? ''}`.trim() })] }));
|
|
1014
1036
|
}
|
|
1015
1037
|
function ImageUploadField({ field: _field, value, onChange }) {
|
|
1016
1038
|
const { t } = useTranslation();
|
package/package.json
CHANGED
|
@@ -645,13 +645,13 @@ function GenericActionModal({ open, onOpenChange, action, model, record, endpoin
|
|
|
645
645
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
646
646
|
{/* Sticky header + footer, scrollable body: the form can grow tall
|
|
647
647
|
(many line-items rows) past the viewport, so cap the dialog at
|
|
648
|
-
|
|
648
|
+
90dvh and let ONLY the field area scroll — the title and the
|
|
649
649
|
Cancel/Submit actions stay pinned and always reachable. maxHeight
|
|
650
|
-
is inline (guaranteed) since an arbitrary max-h-[
|
|
650
|
+
is inline (guaranteed) since an arbitrary max-h-[90dvh] class may be
|
|
651
651
|
dropped by a consuming app's Tailwind scan. */}
|
|
652
652
|
<DialogContent
|
|
653
|
-
className={'flex max-h-[
|
|
654
|
-
style={{ maxHeight: '
|
|
653
|
+
className={'flex max-h-[90dvh] flex-col overflow-hidden ' + (widthPx ? '' : 'sm:max-w-xl')}
|
|
654
|
+
style={{ maxHeight: '90dvh', ...(widthPx ? { maxWidth: widthPx, width: '95vw' } : {}) }}
|
|
655
655
|
>
|
|
656
656
|
<DialogHeader className="shrink-0">
|
|
657
657
|
<DialogTitle className="flex items-center gap-2">
|
|
@@ -837,8 +837,8 @@ function WizardActionModal({ open, onOpenChange, action, model, record, endpoint
|
|
|
837
837
|
return (
|
|
838
838
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
839
839
|
<DialogContent
|
|
840
|
-
className={'flex max-h-[
|
|
841
|
-
style={{ maxHeight: '
|
|
840
|
+
className={'flex max-h-[90dvh] flex-col overflow-hidden ' + (widthPx ? '' : 'sm:max-w-xl')}
|
|
841
|
+
style={{ maxHeight: '90dvh', ...(widthPx ? { maxWidth: widthPx, width: '95vw' } : {}) }}
|
|
842
842
|
>
|
|
843
843
|
<DialogHeader className="shrink-0">
|
|
844
844
|
<DialogTitle className="flex items-center gap-2">
|
|
@@ -46,7 +46,8 @@ import { cn } from '@asteby/metacore-ui/lib'
|
|
|
46
46
|
import { toast } from 'sonner'
|
|
47
47
|
import { format, parseISO } from 'date-fns'
|
|
48
48
|
import { es } from 'date-fns/locale'
|
|
49
|
-
import { ExternalLink, Loader2, CalendarIcon, ChevronDown, Check, Upload, X as XIcon } from 'lucide-react'
|
|
49
|
+
import { ExternalLink, Loader2, CalendarIcon, ChevronDown, Check, Upload, X as XIcon, ScanLine } from 'lucide-react'
|
|
50
|
+
import { BarcodeScanner, isCameraScanSupported } from '../barcode-scanner'
|
|
50
51
|
import { useApi } from '../api-context'
|
|
51
52
|
import { toastServerError, extractFieldErrors, localizeFieldIssue } from '../server-error'
|
|
52
53
|
import { DynamicSelectField, OptionLead, OptionThumb } from '../dynamic-select-field'
|
|
@@ -941,7 +942,7 @@ export function DynamicRecordDialog({
|
|
|
941
942
|
|
|
942
943
|
return (
|
|
943
944
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
944
|
-
<DialogContent className="sm:max-w-2xl max-h-[
|
|
945
|
+
<DialogContent className="sm:max-w-2xl max-h-[90dvh] flex flex-col p-0 gap-0 overflow-hidden" style={{ maxHeight: '90dvh' }}>
|
|
945
946
|
<DialogHeader className="p-6 pb-4 border-b shrink-0">
|
|
946
947
|
<DialogTitle>{title}</DialogTitle>
|
|
947
948
|
<DialogDescription>{config.description}</DialogDescription>
|
|
@@ -1821,16 +1822,73 @@ export function EditField({ field, value, onChange, record }: {
|
|
|
1821
1822
|
? 'email'
|
|
1822
1823
|
: 'text'
|
|
1823
1824
|
|
|
1824
|
-
return
|
|
1825
|
+
return <ScannableRecordInput field={field} value={value} onChange={onChange} inputType={inputType} />
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
/**
|
|
1829
|
+
* Input de texto/número del modal de crear/editar registro con opción de ESCANEO
|
|
1830
|
+
* por cámara. Cuando el campo declara `scan` (alias `scannable`) y el navegador
|
|
1831
|
+
* lo soporta, agrega un botón de cámara: el código escaneado LLENA el input (ej.
|
|
1832
|
+
* el SKU/CÓDIGO al crear producto o variante) y el escáner queda abierto para
|
|
1833
|
+
* corregir/reintentar. Sin el flag o sin soporte, es un Input normal.
|
|
1834
|
+
*
|
|
1835
|
+
* NOTA: este modal renderer es SEPARADO del FieldRenderer de dynamic-form, por
|
|
1836
|
+
* eso el escaneo se cablea también acá (si no, el auto-CRUD de crear producto
|
|
1837
|
+
* nunca mostraba el botón).
|
|
1838
|
+
*/
|
|
1839
|
+
function ScannableRecordInput({
|
|
1840
|
+
field,
|
|
1841
|
+
value,
|
|
1842
|
+
onChange,
|
|
1843
|
+
inputType,
|
|
1844
|
+
}: {
|
|
1845
|
+
field: FieldDef
|
|
1846
|
+
value: any
|
|
1847
|
+
onChange: (val: any) => void
|
|
1848
|
+
inputType: string
|
|
1849
|
+
}) {
|
|
1850
|
+
const [scanOpen, setScanOpen] = useState(false)
|
|
1851
|
+
const scanEnabled =
|
|
1852
|
+
!!((field as { scan?: boolean; scannable?: boolean }).scan ??
|
|
1853
|
+
(field as { scan?: boolean; scannable?: boolean }).scannable) &&
|
|
1854
|
+
isCameraScanSupported()
|
|
1855
|
+
const input = (
|
|
1825
1856
|
<Input
|
|
1826
1857
|
type={inputType}
|
|
1827
1858
|
value={value ?? ''}
|
|
1828
|
-
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
1829
|
-
|
|
1830
|
-
|
|
1859
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
1860
|
+
onChange(
|
|
1861
|
+
inputType === 'number' ? (e.target.value === '' ? '' : Number(e.target.value)) : e.target.value,
|
|
1862
|
+
)
|
|
1863
|
+
}
|
|
1831
1864
|
placeholder={field.placeholder}
|
|
1832
1865
|
/>
|
|
1833
1866
|
)
|
|
1867
|
+
if (!scanEnabled) return input
|
|
1868
|
+
return (
|
|
1869
|
+
<div className="flex w-full min-w-0 items-center gap-1.5">
|
|
1870
|
+
<div className="min-w-0 flex-1">{input}</div>
|
|
1871
|
+
<Button
|
|
1872
|
+
type="button"
|
|
1873
|
+
variant="outline"
|
|
1874
|
+
size="icon"
|
|
1875
|
+
className="size-9 shrink-0"
|
|
1876
|
+
onClick={() => setScanOpen(true)}
|
|
1877
|
+
title="Escanear con la cámara"
|
|
1878
|
+
aria-label="Escanear código de barras con la cámara"
|
|
1879
|
+
>
|
|
1880
|
+
<ScanLine className="size-4" />
|
|
1881
|
+
</Button>
|
|
1882
|
+
<BarcodeScanner
|
|
1883
|
+
open={scanOpen}
|
|
1884
|
+
onClose={() => setScanOpen(false)}
|
|
1885
|
+
onDetected={(code) => onChange(code)}
|
|
1886
|
+
continuous
|
|
1887
|
+
position="fixed"
|
|
1888
|
+
title={`Escanear ${field.label ?? ''}`.trim()}
|
|
1889
|
+
/>
|
|
1890
|
+
</div>
|
|
1891
|
+
)
|
|
1834
1892
|
}
|
|
1835
1893
|
|
|
1836
1894
|
function ImageUploadField({ field: _field, value, onChange }: { field: FieldDef; value: any; onChange: (val: any) => void }) {
|