@asteby/metacore-runtime-react 29.2.1 → 29.2.3
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 +12 -0
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +24 -2
- package/dist/dynamic-line-items.d.ts.map +1 -1
- package/dist/dynamic-line-items.js +3 -2
- package/package.json +1 -1
- package/src/dialogs/dynamic-record.tsx +63 -5
- package/src/dynamic-line-items.tsx +82 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 29.2.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 53f6efa: Line-items declarativos (`DynamicLineItems`) responsive en móvil: la tabla de N columnas no cabía en un teléfono (obligaba a scroll horizontal renglón por renglón). En móvil cada renglón se vuelve una **card apilada** con sus columnas como campos etiquetados —mismo widget de celda, editable con el pulgar—, el botón "Agregar renglón" ocupa el ancho completo y los totales se muestran como resumen. De `sm` en adelante se conserva la tabla. Beneficia a todo modal con renglones (crear orden de compra, recepción de mercancía, asientos contables).
|
|
8
|
+
|
|
9
|
+
## 29.2.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
3
15
|
## 29.2.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -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';
|
|
@@ -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();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-line-items.d.ts","sourceRoot":"","sources":["../src/dynamic-line-items.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAgB7C,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,EAAE,GAAG,SAAS,CAAA;IACxB,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACnC;AAkBD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAgB,EAAE,UAAU,EAAE,EAAE,qBAAqB,+
|
|
1
|
+
{"version":3,"file":"dynamic-line-items.d.ts","sourceRoot":"","sources":["../src/dynamic-line-items.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAgB7C,MAAM,WAAW,qBAAqB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,EAAE,GAAG,SAAS,CAAA;IACxB,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC/B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CACnC;AAkBD,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAgB,EAAE,UAAU,EAAE,EAAE,qBAAqB,+BA6O/G"}
|
|
@@ -63,7 +63,8 @@ export function DynamicLineItems({ field, value, onChange, disabled = false, for
|
|
|
63
63
|
}
|
|
64
64
|
updateCell(idx, key, cellValue);
|
|
65
65
|
};
|
|
66
|
-
|
|
66
|
+
const totalCols = itemFields.filter((c) => c.total);
|
|
67
|
+
return (_jsxs("div", { className: "grid gap-2", "data-widget": "line_items", children: [_jsxs("div", { className: "grid gap-2 sm:hidden", children: [rows.length === 0 && (_jsx("div", { className: "text-muted-foreground rounded-md border px-3 py-4 text-center text-sm", children: "Sin renglones" })), rows.map((row, idx) => (_jsxs("div", { className: "rounded-md border p-3", children: [_jsxs("div", { className: "mb-2 flex items-center justify-between", children: [_jsxs("span", { className: "text-muted-foreground text-xs font-medium", children: ["Rengl\u00F3n ", idx + 1] }), !lockRows && (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-7", onClick: () => removeRow(idx), disabled: disabled, "aria-label": "Eliminar rengl\u00F3n", children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }))] }), _jsx("div", { className: "grid gap-2.5", children: itemFields.map((col) => (_jsxs("div", { className: "grid gap-1", children: [_jsxs("span", { className: "text-xs font-medium", children: [col.label, col.required && (_jsx("span", { className: "ml-1 text-red-500", children: "*" }))] }), _jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), disabled: disabled, formValues: formValues, rowValues: row })] }, col.key))) })] }, idx))), hasTotals && rows.length > 0 && (_jsx("div", { className: "bg-muted/30 grid gap-1.5 rounded-md border p-3", children: totalCols.map((col) => (_jsxs("div", { className: "flex items-center justify-between text-sm", children: [_jsx("span", { className: "text-muted-foreground", children: col.label }), _jsx("span", { className: "font-semibold tabular-nums", children: fmtNumber(totals[col.key] ?? 0) })] }, col.key))) }))] }), _jsx("div", { className: "hidden overflow-x-auto rounded-md border sm:block", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/50", children: _jsxs("tr", { children: [itemFields.map((col) => (_jsxs("th", { className: 'px-3 py-2 font-medium ' +
|
|
67
68
|
(isNumericCol(col) ? 'text-right' : 'text-left'), children: [col.label, col.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] }, col.key))), !lockRows && _jsx("th", { className: "w-12 px-3 py-2", "aria-label": "acciones" })] }) }), _jsxs("tbody", { children: [rows.length === 0 && (_jsx("tr", { children: _jsx("td", { colSpan: itemFields.length + (lockRows ? 0 : 1), className: "px-3 py-4 text-center text-muted-foreground", children: "Sin renglones" }) })), rows.map((row, idx) => (_jsxs("tr", { className: "border-t align-top", children: [itemFields.map((col) => (_jsx("td", { className: "px-2 py-1.5", children: _jsx(CellRenderer, { field: col, value: row?.[col.key], onChange: (v) => handleCell(idx, col.key, v), disabled: disabled, formValues: formValues, rowValues: row }) }, col.key))), !lockRows && (_jsx("td", { className: "px-2 py-1.5 text-center", children: _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeRow(idx), disabled: disabled, "aria-label": "Eliminar rengl\u00F3n", children: _jsx(Trash2, { className: "h-4 w-4 text-red-500" }) }) }))] }, idx)))] }), hasTotals && rows.length > 0 && (_jsx("tfoot", { className: "border-t bg-muted/30", children: _jsxs("tr", { children: [itemFields.map((col, ci) => {
|
|
68
69
|
if (ci === 0) {
|
|
69
70
|
return (_jsx("td", { className: "px-3 py-2 text-left font-medium text-muted-foreground", children: "Totales" }, col.key));
|
|
@@ -72,7 +73,7 @@ export function DynamicLineItems({ field, value, onChange, disabled = false, for
|
|
|
72
73
|
(col.total
|
|
73
74
|
? 'text-right font-semibold tabular-nums'
|
|
74
75
|
: ''), children: col.total ? fmtNumber(totals[col.key] ?? 0) : null }, col.key));
|
|
75
|
-
}), !lockRows && _jsx("td", {})] }) }))] }) }), _jsxs("div", { className: "flex items-center justify-between
|
|
76
|
+
}), !lockRows && _jsx("td", {})] }) }))] }) }), _jsxs("div", { className: "flex flex-col items-stretch gap-2 sm:flex-row sm:items-center sm:justify-between", children: [lockRows ? (_jsx("span", {})) : (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addRow, disabled: disabled, className: "w-full sm:w-auto", children: [_jsx(Plus, { className: "mr-1 h-4 w-4" }), "Agregar rengl\u00F3n"] })), balance && _jsx(BalanceBadge, { state: balance })] })] }));
|
|
76
77
|
}
|
|
77
78
|
function BalanceBadge({ state, }) {
|
|
78
79
|
if (state.balanced) {
|
package/package.json
CHANGED
|
@@ -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'
|
|
@@ -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 }) {
|
|
@@ -113,9 +113,81 @@ export function DynamicLineItems({ field, value, onChange, disabled = false, for
|
|
|
113
113
|
updateCell(idx, key, cellValue)
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
const totalCols = itemFields.filter((c) => c.total)
|
|
117
|
+
|
|
116
118
|
return (
|
|
117
119
|
<div className="grid gap-2" data-widget="line_items">
|
|
118
|
-
<
|
|
120
|
+
{/* Móvil (<sm): una tabla de N columnas no cabe en un teléfono —
|
|
121
|
+
obliga a scroll horizontal renglón por renglón. Cada renglón se
|
|
122
|
+
vuelve una CARD apilada con sus columnas como campos etiquetados
|
|
123
|
+
(mismo CellRenderer), legible y editable con el pulgar. La tabla
|
|
124
|
+
se conserva de sm en adelante. */}
|
|
125
|
+
<div className="grid gap-2 sm:hidden">
|
|
126
|
+
{rows.length === 0 && (
|
|
127
|
+
<div className="text-muted-foreground rounded-md border px-3 py-4 text-center text-sm">
|
|
128
|
+
Sin renglones
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
{rows.map((row, idx) => (
|
|
132
|
+
<div key={idx} className="rounded-md border p-3">
|
|
133
|
+
<div className="mb-2 flex items-center justify-between">
|
|
134
|
+
<span className="text-muted-foreground text-xs font-medium">
|
|
135
|
+
Renglón {idx + 1}
|
|
136
|
+
</span>
|
|
137
|
+
{!lockRows && (
|
|
138
|
+
<Button
|
|
139
|
+
type="button"
|
|
140
|
+
variant="ghost"
|
|
141
|
+
size="icon"
|
|
142
|
+
className="size-7"
|
|
143
|
+
onClick={() => removeRow(idx)}
|
|
144
|
+
disabled={disabled}
|
|
145
|
+
aria-label="Eliminar renglón"
|
|
146
|
+
>
|
|
147
|
+
<Trash2 className="h-4 w-4 text-red-500" />
|
|
148
|
+
</Button>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
<div className="grid gap-2.5">
|
|
152
|
+
{itemFields.map((col) => (
|
|
153
|
+
<div key={col.key} className="grid gap-1">
|
|
154
|
+
<span className="text-xs font-medium">
|
|
155
|
+
{col.label}
|
|
156
|
+
{col.required && (
|
|
157
|
+
<span className="ml-1 text-red-500">*</span>
|
|
158
|
+
)}
|
|
159
|
+
</span>
|
|
160
|
+
<CellRenderer
|
|
161
|
+
field={col}
|
|
162
|
+
value={row?.[col.key]}
|
|
163
|
+
onChange={(v: any) => handleCell(idx, col.key, v)}
|
|
164
|
+
disabled={disabled}
|
|
165
|
+
formValues={formValues}
|
|
166
|
+
rowValues={row}
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
))}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
))}
|
|
173
|
+
{hasTotals && rows.length > 0 && (
|
|
174
|
+
<div className="bg-muted/30 grid gap-1.5 rounded-md border p-3">
|
|
175
|
+
{totalCols.map((col) => (
|
|
176
|
+
<div
|
|
177
|
+
key={col.key}
|
|
178
|
+
className="flex items-center justify-between text-sm"
|
|
179
|
+
>
|
|
180
|
+
<span className="text-muted-foreground">{col.label}</span>
|
|
181
|
+
<span className="font-semibold tabular-nums">
|
|
182
|
+
{fmtNumber(totals[col.key] ?? 0)}
|
|
183
|
+
</span>
|
|
184
|
+
</div>
|
|
185
|
+
))}
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div className="hidden overflow-x-auto rounded-md border sm:block">
|
|
119
191
|
<table className="w-full text-sm">
|
|
120
192
|
<thead className="bg-muted/50">
|
|
121
193
|
<tr>
|
|
@@ -210,11 +282,18 @@ export function DynamicLineItems({ field, value, onChange, disabled = false, for
|
|
|
210
282
|
)}
|
|
211
283
|
</table>
|
|
212
284
|
</div>
|
|
213
|
-
<div className="flex items-center justify-between
|
|
285
|
+
<div className="flex flex-col items-stretch gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
214
286
|
{lockRows ? (
|
|
215
287
|
<span />
|
|
216
288
|
) : (
|
|
217
|
-
<Button
|
|
289
|
+
<Button
|
|
290
|
+
type="button"
|
|
291
|
+
variant="outline"
|
|
292
|
+
size="sm"
|
|
293
|
+
onClick={addRow}
|
|
294
|
+
disabled={disabled}
|
|
295
|
+
className="w-full sm:w-auto"
|
|
296
|
+
>
|
|
218
297
|
<Plus className="mr-1 h-4 w-4" />
|
|
219
298
|
Agregar renglón
|
|
220
299
|
</Button>
|