@asteby/metacore-runtime-react 34.0.0 → 34.0.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 +15 -0
- package/dist/dialogs/import.d.ts.map +1 -1
- package/dist/dialogs/import.js +6 -10
- package/dist/display-value.d.ts.map +1 -1
- package/dist/display-value.js +1 -1
- package/dist/file-pick-button.d.ts +21 -0
- package/dist/file-pick-button.d.ts.map +1 -0
- package/dist/file-pick-button.js +39 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/upload-field.d.ts.map +1 -1
- package/dist/upload-field.js +27 -32
- package/package.json +1 -1
- package/src/dialogs/import.tsx +9 -15
- package/src/display-value.tsx +7 -3
- package/src/file-pick-button.tsx +125 -0
- package/src/index.ts +4 -0
- package/src/upload-field.tsx +41 -71
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 34.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7bff072: Locale-aware FilePickButton: hide native file chrome so labels follow app language (es/en). UploadField and ImportDialog use it.
|
|
8
|
+
|
|
9
|
+
## 34.0.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 45b5f91: Fix relation/brand thumbnails (RelationCell) being cropped: switch from
|
|
14
|
+
`object-cover` to `object-contain` on a padded neutral background so
|
|
15
|
+
wide/rectangular logos (brand, supplier, category images) render in full
|
|
16
|
+
instead of clipped to a square.
|
|
17
|
+
|
|
3
18
|
## 34.0.0
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../src/dialogs/import.tsx"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../src/dialogs/import.tsx"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAI7C,UAAU,iBAAiB;IACvB,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,aAAa,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;CAC1B;AAqCD,wBAAgB,YAAY,CAAC,EACzB,IAAI,EACJ,YAAY,EACZ,KAAK,EACL,QAAQ,EACR,UAAU,GACb,EAAE,iBAAiB,+BAyXnB"}
|
package/dist/dialogs/import.js
CHANGED
|
@@ -4,12 +4,14 @@ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-run
|
|
|
4
4
|
// the validation rules all come from the model's ImportSpec served by the
|
|
5
5
|
// kernel, so this dialog carries no per-model knowledge. Axios-like client is
|
|
6
6
|
// provided by <ApiProvider>.
|
|
7
|
-
import { useState, useEffect
|
|
8
|
-
import {
|
|
7
|
+
import { useState, useEffect } from 'react';
|
|
8
|
+
import { useTranslation } from 'react-i18next';
|
|
9
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Button, Label, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@asteby/metacore-ui/primitives';
|
|
9
10
|
import { Progress } from './_primitives';
|
|
10
11
|
import { toast } from 'sonner';
|
|
11
12
|
import { FileDown, Loader2, Check, AlertCircle } from 'lucide-react';
|
|
12
13
|
import { useApi } from '../api-context';
|
|
14
|
+
import { FilePickButton } from '../file-pick-button';
|
|
13
15
|
function errorColumn(e) {
|
|
14
16
|
return e.column ?? e.field ?? '';
|
|
15
17
|
}
|
|
@@ -18,6 +20,7 @@ function errorMessage(e) {
|
|
|
18
20
|
}
|
|
19
21
|
export function ImportDialog({ open, onOpenChange, model, metadata, onImported, }) {
|
|
20
22
|
const api = useApi();
|
|
23
|
+
const { t } = useTranslation();
|
|
21
24
|
const [step, setStep] = useState('upload');
|
|
22
25
|
const [file, setFile] = useState(null);
|
|
23
26
|
const [validating, setValidating] = useState(false);
|
|
@@ -25,7 +28,6 @@ export function ImportDialog({ open, onOpenChange, model, metadata, onImported,
|
|
|
25
28
|
const [validationResult, setValidationResult] = useState(null);
|
|
26
29
|
const [importResult, setImportResult] = useState(null);
|
|
27
30
|
const [progress, setProgress] = useState(0);
|
|
28
|
-
const fileInputRef = useRef(null);
|
|
29
31
|
useEffect(() => {
|
|
30
32
|
if (open) {
|
|
31
33
|
setStep('upload');
|
|
@@ -35,14 +37,8 @@ export function ImportDialog({ open, onOpenChange, model, metadata, onImported,
|
|
|
35
37
|
setValidationResult(null);
|
|
36
38
|
setImportResult(null);
|
|
37
39
|
setProgress(0);
|
|
38
|
-
if (fileInputRef.current)
|
|
39
|
-
fileInputRef.current.value = '';
|
|
40
40
|
}
|
|
41
41
|
}, [open]);
|
|
42
|
-
const handleFileChange = (e) => {
|
|
43
|
-
const selectedFile = e.target.files?.[0] ?? null;
|
|
44
|
-
setFile(selectedFile);
|
|
45
|
-
};
|
|
46
42
|
const handleDownloadTemplate = async () => {
|
|
47
43
|
try {
|
|
48
44
|
const response = await api.get(`/dynamic/${model}/export/template`, {
|
|
@@ -167,6 +163,6 @@ export function ImportDialog({ open, onOpenChange, model, metadata, onImported,
|
|
|
167
163
|
validation: 'Validacion',
|
|
168
164
|
results: 'Resultados',
|
|
169
165
|
};
|
|
170
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-lg max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden", children: [_jsxs(DialogHeader, { className: "p-6 pb-4 border-b shrink-0", children: [_jsxs(DialogTitle, { children: ["Importar ", metadata.title] }), _jsx(DialogDescription, { children: stepTitle[step] })] }), _jsxs("div", { className: "flex-1 overflow-y-auto p-6", children: [step === 'upload' && (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { children: [_jsxs(Button, { variant: "link", className: "px-0 h-auto text-sm", onClick: handleDownloadTemplate, children: [_jsx(FileDown, { className: "h-4 w-4 mr-1" }), "Descargar plantilla"] }), _jsx("p", { className: "text-xs text-muted-foreground mt-1", children: "La plantilla trae las columnas, un ejemplo y las instrucciones. Borra la fila de ejemplo antes de subir." })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "import-file", className: "text-sm font-medium", children:
|
|
166
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-lg max-h-[90vh] flex flex-col p-0 gap-0 overflow-hidden", children: [_jsxs(DialogHeader, { className: "p-6 pb-4 border-b shrink-0", children: [_jsxs(DialogTitle, { children: ["Importar ", metadata.title] }), _jsx(DialogDescription, { children: stepTitle[step] })] }), _jsxs("div", { className: "flex-1 overflow-y-auto p-6", children: [step === 'upload' && (_jsxs("div", { className: "space-y-6", children: [_jsxs("div", { children: [_jsxs(Button, { variant: "link", className: "px-0 h-auto text-sm", onClick: handleDownloadTemplate, children: [_jsx(FileDown, { className: "h-4 w-4 mr-1" }), "Descargar plantilla"] }), _jsx("p", { className: "text-xs text-muted-foreground mt-1", children: "La plantilla trae las columnas, un ejemplo y las instrucciones. Borra la fila de ejemplo antes de subir." })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "import-file", className: "text-sm font-medium", children: t('common.upload.choose', { defaultValue: 'Choose file' }) }), _jsx(FilePickButton, { id: "import-file", accept: ".xlsx,.xls,.csv,.json", hasFile: Boolean(file), fileName: file?.name, onFile: setFile }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Formatos aceptados: Excel, CSV, JSON" })] })] })), step === 'validation' && validationResult && (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center gap-4", children: [_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx(Check, { className: "h-4 w-4 text-green-600" }), _jsxs("span", { children: [_jsx("strong", { children: validationResult.valid }), " valido(s)"] })] }), validationResult.errors.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx(AlertCircle, { className: "h-4 w-4 text-destructive" }), _jsxs("span", { children: [_jsx("strong", { children: validationResult.errors.length }), " error(es)"] })] }))] }), validationResult.skipped > 0 && (_jsxs("p", { className: "text-xs text-muted-foreground", children: ["Se ignoraron ", validationResult.skipped, " fila(s) de ejemplo de la plantilla."] })), validationResult.errors.length > 0 && (_jsx("div", { className: "border rounded-md max-h-60 overflow-auto", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-16", children: "Fila" }), _jsx(TableHead, { children: "Campo" }), _jsx(TableHead, { children: "Error" })] }) }), _jsx(TableBody, { children: validationResult.errors.map((error, idx) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-mono text-xs", children: error.row }), _jsx(TableCell, { className: "text-sm", children: errorColumn(error) }), _jsx(TableCell, { className: "text-sm text-destructive", children: errorMessage(error) })] }, idx))) })] }) })), importing && (_jsxs("div", { className: "space-y-2", children: [_jsx(Progress, { value: progress }), _jsxs("p", { className: "text-xs text-muted-foreground text-center", children: ["Importando... ", progress > 0 ? `${progress}%` : ''] })] }))] })), step === 'results' && importResult && (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center gap-4", children: [importResult.created > 0 && (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx(Check, { className: "h-4 w-4 text-green-600" }), _jsxs("span", { children: [_jsx("strong", { children: importResult.created }), " creado(s)"] })] })), importResult.errors.length > 0 && (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx(AlertCircle, { className: "h-4 w-4 text-destructive" }), _jsxs("span", { children: [_jsx("strong", { children: importResult.errors.length }), " error(es)"] })] }))] }), importResult.created > 0 && importResult.errors.length === 0 && (_jsxs("div", { className: "flex items-center gap-2 rounded-md bg-green-50 dark:bg-green-950/20 p-3 text-sm text-green-700 dark:text-green-400", children: [_jsx(Check, { className: "h-4 w-4 shrink-0" }), "Todos los registros fueron importados correctamente."] })), importResult.errors.length > 0 && (_jsx("div", { className: "border rounded-md max-h-60 overflow-auto", children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-16", children: "Fila" }), _jsx(TableHead, { children: "Campo" }), _jsx(TableHead, { children: "Error" })] }) }), _jsx(TableBody, { children: importResult.errors.map((error, idx) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-mono text-xs", children: error.row }), _jsx(TableCell, { className: "text-sm", children: errorColumn(error) }), _jsx(TableCell, { className: "text-sm text-destructive", children: errorMessage(error) })] }, idx))) })] }) }))] }))] }), _jsxs(DialogFooter, { className: "p-4 border-t shrink-0", children: [step === 'upload' && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", onClick: handleClose, children: "Cancelar" }), _jsxs(Button, { onClick: handleValidate, disabled: !file || validating, children: [validating && (_jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" })), validating ? 'Validando...' : 'Validar'] })] })), step === 'validation' && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", onClick: () => setStep('upload'), disabled: importing, children: "Atras" }), _jsxs(Button, { onClick: handleImport, disabled: importing ||
|
|
171
167
|
(validationResult !== null && validationResult.valid === 0), children: [importing && (_jsx(Loader2, { className: "h-4 w-4 mr-1 animate-spin" })), importing ? 'Importando...' : 'Importar'] })] })), step === 'results' && (_jsx(Button, { onClick: handleClose, children: "Cerrar" }))] })] }) }));
|
|
172
168
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"display-value.d.ts","sourceRoot":"","sources":["../src/display-value.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB;;;GAGG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAmBxC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAepD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC;IACrC,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,
|
|
1
|
+
{"version":3,"file":"display-value.d.ts","sourceRoot":"","sources":["../src/display-value.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,MAAM,OAAO,CAAA;AASzB;;;GAGG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAmBxC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAepD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC;IACrC,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC,IAAI,CAAC,EAAE,MAAM,CAAA;CAChB,CAkBA,CAAA;AAED,MAAM,WAAW,aAAa;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC;IAC/B,MAAM,EAAE,aAAa,CAAA;IACrB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAA;CACpB,CA8BA,CAAA"}
|
package/dist/display-value.js
CHANGED
|
@@ -58,7 +58,7 @@ export function statusColorFor(value) {
|
|
|
58
58
|
* an addon-arbitrary Tailwind class never gets dropped by a consuming app's
|
|
59
59
|
* class scan). Rendered inline alongside a label — never alone.
|
|
60
60
|
*/
|
|
61
|
-
export const RelationThumbnail = ({ src, alt, getImageUrl, size =
|
|
61
|
+
export const RelationThumbnail = ({ src, alt, getImageUrl, size = 20 }) => (_jsxs(Avatar, { className: "shrink-0 rounded-md bg-muted/50 p-[3px] ring-1 ring-border/40", style: { width: size, height: size }, children: [_jsx(AvatarImage, { src: getImageUrl ? getImageUrl(src) : src, alt: alt, className: "object-contain" }), _jsx(AvatarFallback, { className: "rounded-md bg-primary/10 text-[8px] font-bold text-primary", children: getInitials(alt) })] }));
|
|
62
62
|
/**
|
|
63
63
|
* Colored option badge — the canonical "pro" pill for a select/status/badge
|
|
64
64
|
* value. Explicit backend `color` wins; otherwise a stable, cohesive color is
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface FilePickButtonProps {
|
|
2
|
+
/** Called with the picked File, or null when cleared. */
|
|
3
|
+
onFile: (file: File | null) => void;
|
|
4
|
+
accept?: string;
|
|
5
|
+
/** HTML capture attribute (e.g. "environment" for rear camera). */
|
|
6
|
+
capture?: boolean | 'user' | 'environment';
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
loading?: boolean;
|
|
9
|
+
/** When true, shows replace + clear affordances. */
|
|
10
|
+
hasFile?: boolean;
|
|
11
|
+
/** Optional display name next to the button when hasFile. */
|
|
12
|
+
fileName?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
/** Override the empty-state pick label (still falls back to i18n). */
|
|
15
|
+
pickLabel?: string;
|
|
16
|
+
replaceLabel?: string;
|
|
17
|
+
clearLabel?: string;
|
|
18
|
+
id?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function FilePickButton({ onFile, accept, capture, disabled, loading, hasFile, fileName, className, pickLabel, replaceLabel, clearLabel, id: idProp, }: FilePickButtonProps): import("react").JSX.Element;
|
|
21
|
+
//# sourceMappingURL=file-pick-button.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-pick-button.d.ts","sourceRoot":"","sources":["../src/file-pick-button.tsx"],"names":[],"mappings":"AAUA,MAAM,WAAW,mBAAmB;IAChC,yDAAyD;IACzD,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,CAAA;IACnC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,aAAa,CAAA;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,oDAAoD;IACpD,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,EAAE,CAAC,EAAE,MAAM,CAAA;CACd;AAED,wBAAgB,cAAc,CAAC,EAC3B,MAAM,EACN,MAAM,EACN,OAAO,EACP,QAAQ,EACR,OAAO,EACP,OAAO,EACP,QAAQ,EACR,SAAS,EACT,SAAS,EACT,YAAY,EACZ,UAAU,EACV,EAAE,EAAE,MAAM,GACb,EAAE,mBAAmB,+BAiFrB"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// FilePickButton — locale-aware file picker. Native <input type="file"> always
|
|
3
|
+
// paints the browser/OS chrome ("Choose File" / "No file chosen") in the
|
|
4
|
+
// *browser* language, not the app language — so Spanish UIs on an English
|
|
5
|
+
// Chrome show English. Hide the native control and drive it from a Button
|
|
6
|
+
// whose labels come from i18n (common.upload.*).
|
|
7
|
+
import { useCallback, useId, useRef } from 'react';
|
|
8
|
+
import { useTranslation } from 'react-i18next';
|
|
9
|
+
import { Button } from '@asteby/metacore-ui/primitives';
|
|
10
|
+
import { Loader2, Paperclip, X } from 'lucide-react';
|
|
11
|
+
export function FilePickButton({ onFile, accept, capture, disabled, loading, hasFile, fileName, className, pickLabel, replaceLabel, clearLabel, id: idProp, }) {
|
|
12
|
+
const { t } = useTranslation();
|
|
13
|
+
const autoId = useId();
|
|
14
|
+
const id = idProp || autoId;
|
|
15
|
+
const inputRef = useRef(null);
|
|
16
|
+
const busy = Boolean(disabled || loading);
|
|
17
|
+
const pick = useCallback(() => {
|
|
18
|
+
if (busy)
|
|
19
|
+
return;
|
|
20
|
+
inputRef.current?.click();
|
|
21
|
+
}, [busy]);
|
|
22
|
+
const onChange = useCallback((e) => {
|
|
23
|
+
const file = e.target.files?.[0] ?? null;
|
|
24
|
+
if (inputRef.current)
|
|
25
|
+
inputRef.current.value = '';
|
|
26
|
+
if (file)
|
|
27
|
+
onFile(file);
|
|
28
|
+
}, [onFile]);
|
|
29
|
+
const onClear = useCallback(() => {
|
|
30
|
+
if (busy)
|
|
31
|
+
return;
|
|
32
|
+
onFile(null);
|
|
33
|
+
}, [busy, onFile]);
|
|
34
|
+
const emptyLabel = pickLabel || t('common.upload.choose', { defaultValue: 'Choose file' });
|
|
35
|
+
const filledLabel = replaceLabel || t('common.upload.replace', { defaultValue: 'Replace' });
|
|
36
|
+
const removeLabel = clearLabel || t('common.upload.remove', { defaultValue: 'Remove file' });
|
|
37
|
+
const emptyHint = t('common.upload.none', { defaultValue: 'No file chosen' });
|
|
38
|
+
return (_jsxs("div", { className: className ? `grid gap-1.5 ${className}` : 'grid gap-1.5', "data-widget": "file-pick", children: [_jsx("input", { ref: inputRef, id: id, type: "file", accept: accept, ...(capture !== undefined ? { capture } : {}), className: "sr-only", onChange: onChange, tabIndex: -1, "aria-hidden": "true" }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: pick, disabled: busy, children: [loading ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Paperclip, { className: "mr-2 h-4 w-4" })), hasFile ? filledLabel : emptyLabel] }), hasFile ? (_jsxs("div", { className: "flex min-w-0 items-center gap-1 text-sm text-muted-foreground", children: [fileName ? (_jsx("span", { className: "truncate", title: fileName, children: fileName })) : null, _jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-6 w-6 p-0", onClick: onClear, disabled: busy, "aria-label": removeLabel, children: _jsx(X, { className: "h-3.5 w-3.5" }) })] })) : (_jsx("span", { className: "text-xs text-muted-foreground", children: emptyHint }))] })] }));
|
|
39
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export { useStageOverrides, type StageOverridePatch, type UseStageOverridesResul
|
|
|
13
13
|
export { DynamicView, resolveViewRenderer, readViewFromSearch, resolveActiveView, type DynamicViewProps, } from './dynamic-view';
|
|
14
14
|
export { useDynamicFilters, type UseDynamicFiltersOptions, type UseDynamicFiltersResult, } from './use-dynamic-filters';
|
|
15
15
|
export * from './dynamic-form';
|
|
16
|
+
export { FilePickButton, type FilePickButtonProps, } from './file-pick-button';
|
|
16
17
|
export * from './barcode-scanner';
|
|
17
18
|
export * from './form-layout';
|
|
18
19
|
export * from './form-layout-ui';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ export { useStageOverrides, } from './stage-overrides';
|
|
|
18
18
|
export { DynamicView, resolveViewRenderer, readViewFromSearch, resolveActiveView, } from './dynamic-view';
|
|
19
19
|
export { useDynamicFilters, } from './use-dynamic-filters';
|
|
20
20
|
export * from './dynamic-form';
|
|
21
|
+
export { FilePickButton, } from './file-pick-button';
|
|
21
22
|
export * from './barcode-scanner';
|
|
22
23
|
export * from './form-layout';
|
|
23
24
|
export * from './form-layout-ui';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload-field.d.ts","sourceRoot":"","sources":["../src/upload-field.tsx"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C,MAAM,WAAW,gBAAgB;IAC7B,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;CAC7B;AAKD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,GAAG,GAAG,MAAM,CAWzD;AAED,+EAA+E;AAC/E,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAK1D;AAED,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,gBAAgB,+
|
|
1
|
+
{"version":3,"file":"upload-field.d.ts","sourceRoot":"","sources":["../src/upload-field.tsx"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C,MAAM,WAAW,gBAAgB;IAC7B,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;CAC7B;AAKD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,GAAG,GAAG,MAAM,CAWzD;AAED,+EAA+E;AAC/E,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAK1D;AAED,wBAAgB,WAAW,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,gBAAgB,+BA2FvE"}
|
package/dist/upload-field.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// UploadField — the `upload` widget renderer shared by DynamicForm's
|
|
3
3
|
// FieldRenderer and the action-modal-dispatcher's renderField so the two stay
|
|
4
|
-
// in lockstep. Renders a
|
|
5
|
-
// POSTs the picked file to the host upload endpoint as
|
|
6
|
-
// stores the returned file url/path as the field value.
|
|
4
|
+
// in lockstep. Renders a locale-aware FilePickButton that proxies a hidden
|
|
5
|
+
// <input type=file>, POSTs the picked file to the host upload endpoint as
|
|
6
|
+
// multipart/form-data, and stores the returned file url/path as the field value.
|
|
7
7
|
//
|
|
8
8
|
// Endpoint assumption: `POST /uploads` (multipart) returning
|
|
9
9
|
// { success: true, data: { file_url?, url?, path?, file_path? } }
|
|
@@ -11,11 +11,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
// `field.searchEndpoint` (reused as the upload endpoint escape hatch) — kept
|
|
12
12
|
// generic so this carries no host-specific route. Honors field.accept /
|
|
13
13
|
// field.maxSize and forwards field.storagePath as `storage_path`.
|
|
14
|
-
import { useCallback,
|
|
15
|
-
import {
|
|
16
|
-
import { Loader2, Paperclip, X } from 'lucide-react';
|
|
17
|
-
import { useApi } from './api-context';
|
|
14
|
+
import { useCallback, useState } from 'react';
|
|
15
|
+
import { useTranslation } from 'react-i18next';
|
|
18
16
|
import { getUploadConfig } from './dynamic-form-schema';
|
|
17
|
+
import { FilePickButton } from './file-pick-button';
|
|
18
|
+
import { useApi } from './api-context';
|
|
19
19
|
/** Default host upload endpoint. Overridable per-field via `searchEndpoint`. */
|
|
20
20
|
const DEFAULT_UPLOAD_ENDPOINT = '/upload';
|
|
21
21
|
/**
|
|
@@ -46,28 +46,26 @@ export function uploadedDisplayName(value) {
|
|
|
46
46
|
return parts[parts.length - 1] || cleaned;
|
|
47
47
|
}
|
|
48
48
|
export function UploadField({ field, value, onChange }) {
|
|
49
|
+
const { t } = useTranslation();
|
|
49
50
|
const api = useApi();
|
|
50
|
-
const inputRef = useRef(null);
|
|
51
51
|
const [uploading, setUploading] = useState(false);
|
|
52
52
|
const [error, setError] = useState(null);
|
|
53
53
|
const { accept, maxSize, storagePath } = getUploadConfig(field);
|
|
54
54
|
const endpoint = field.searchEndpoint || DEFAULT_UPLOAD_ENDPOINT;
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const handleFile = useCallback(async (e) => {
|
|
61
|
-
const file = e.target.files?.[0];
|
|
62
|
-
// Reset the input so picking the same file again re-fires change.
|
|
63
|
-
if (inputRef.current)
|
|
64
|
-
inputRef.current.value = '';
|
|
65
|
-
if (!file)
|
|
55
|
+
const hasValue = typeof value === 'string' && value !== '';
|
|
56
|
+
const handleFile = useCallback(async (file) => {
|
|
57
|
+
if (!file) {
|
|
58
|
+
setError(null);
|
|
59
|
+
onChange('');
|
|
66
60
|
return;
|
|
61
|
+
}
|
|
67
62
|
setError(null);
|
|
68
63
|
if (maxSize && file.size > maxSize) {
|
|
69
64
|
const mb = (maxSize / (1024 * 1024)).toFixed(1);
|
|
70
|
-
setError(
|
|
65
|
+
setError(t('common.upload.too_large', {
|
|
66
|
+
mb,
|
|
67
|
+
defaultValue: `File too large (max {{mb}} MB).`,
|
|
68
|
+
}));
|
|
71
69
|
return;
|
|
72
70
|
}
|
|
73
71
|
const form = new FormData();
|
|
@@ -87,29 +85,26 @@ export function UploadField({ field, value, onChange }) {
|
|
|
87
85
|
});
|
|
88
86
|
const body = res?.data;
|
|
89
87
|
if (body && body.success === false) {
|
|
90
|
-
setError(body.message ||
|
|
88
|
+
setError(body.message ||
|
|
89
|
+
t('common.upload.failed', { defaultValue: 'Could not upload the file.' }));
|
|
91
90
|
return;
|
|
92
91
|
}
|
|
93
92
|
const stored = extractUploadedValue(body);
|
|
94
93
|
if (!stored) {
|
|
95
|
-
setError('
|
|
94
|
+
setError(t('common.upload.invalid', {
|
|
95
|
+
defaultValue: 'Invalid upload response.',
|
|
96
|
+
}));
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
98
99
|
onChange(stored);
|
|
99
100
|
}
|
|
100
101
|
catch (err) {
|
|
101
|
-
setError(err?.response?.data?.message ||
|
|
102
|
+
setError(err?.response?.data?.message ||
|
|
103
|
+
t('common.upload.failed', { defaultValue: 'Could not upload the file.' }));
|
|
102
104
|
}
|
|
103
105
|
finally {
|
|
104
106
|
setUploading(false);
|
|
105
107
|
}
|
|
106
|
-
}, [api, endpoint, maxSize, storagePath, onChange]);
|
|
107
|
-
|
|
108
|
-
if (uploading)
|
|
109
|
-
return;
|
|
110
|
-
setError(null);
|
|
111
|
-
onChange('');
|
|
112
|
-
}, [uploading, onChange]);
|
|
113
|
-
const hasValue = typeof value === 'string' && value !== '';
|
|
114
|
-
return (_jsxs("div", { className: "grid gap-1.5", "data-widget": "upload", children: [_jsx("input", { ref: inputRef, id: field.key, type: "file", accept: accept, className: "sr-only", onChange: handleFile, tabIndex: -1, "aria-hidden": "true" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: handlePick, disabled: uploading, children: [uploading ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : (_jsx(Paperclip, { className: "mr-2 h-4 w-4" })), hasValue ? 'Reemplazar' : field.placeholder || 'Subir archivo'] }), hasValue && !uploading && (_jsxs("div", { className: "flex min-w-0 items-center gap-1 text-sm text-muted-foreground", children: [_jsx("span", { className: "truncate", title: String(value), children: uploadedDisplayName(value) }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "h-6 w-6 p-0", onClick: handleClear, "aria-label": "Quitar archivo", children: _jsx(X, { className: "h-3.5 w-3.5" }) })] }))] }), error && (_jsx("span", { className: "text-sm text-destructive", role: "alert", children: error }))] }));
|
|
108
|
+
}, [api, endpoint, maxSize, storagePath, onChange, t]);
|
|
109
|
+
return (_jsxs("div", { className: "grid gap-1.5", "data-widget": "upload", children: [_jsx(FilePickButton, { id: field.key, accept: accept, loading: uploading, hasFile: hasValue, fileName: hasValue ? uploadedDisplayName(value) : undefined, onFile: (f) => void handleFile(f), pickLabel: field.placeholder || undefined }), error && (_jsx("span", { className: "text-sm text-destructive", role: "alert", children: error }))] }));
|
|
115
110
|
}
|
package/package.json
CHANGED
package/src/dialogs/import.tsx
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// the validation rules all come from the model's ImportSpec served by the
|
|
4
4
|
// kernel, so this dialog carries no per-model knowledge. Axios-like client is
|
|
5
5
|
// provided by <ApiProvider>.
|
|
6
|
-
import { useState, useEffect
|
|
6
|
+
import { useState, useEffect } from 'react'
|
|
7
|
+
import { useTranslation } from 'react-i18next'
|
|
7
8
|
import {
|
|
8
9
|
Dialog,
|
|
9
10
|
DialogContent,
|
|
@@ -12,7 +13,6 @@ import {
|
|
|
12
13
|
DialogDescription,
|
|
13
14
|
DialogFooter,
|
|
14
15
|
Button,
|
|
15
|
-
Input,
|
|
16
16
|
Label,
|
|
17
17
|
Table,
|
|
18
18
|
TableBody,
|
|
@@ -26,6 +26,7 @@ import { toast } from 'sonner'
|
|
|
26
26
|
import { FileDown, Loader2, Check, AlertCircle } from 'lucide-react'
|
|
27
27
|
import type { TableMetadata } from '../types'
|
|
28
28
|
import { useApi } from '../api-context'
|
|
29
|
+
import { FilePickButton } from '../file-pick-button'
|
|
29
30
|
|
|
30
31
|
interface ImportDialogProps {
|
|
31
32
|
open: boolean
|
|
@@ -78,6 +79,7 @@ export function ImportDialog({
|
|
|
78
79
|
onImported,
|
|
79
80
|
}: ImportDialogProps) {
|
|
80
81
|
const api = useApi()
|
|
82
|
+
const { t } = useTranslation()
|
|
81
83
|
const [step, setStep] = useState<Step>('upload')
|
|
82
84
|
const [file, setFile] = useState<File | null>(null)
|
|
83
85
|
const [validating, setValidating] = useState(false)
|
|
@@ -85,7 +87,6 @@ export function ImportDialog({
|
|
|
85
87
|
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null)
|
|
86
88
|
const [importResult, setImportResult] = useState<ImportResult | null>(null)
|
|
87
89
|
const [progress, setProgress] = useState(0)
|
|
88
|
-
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
89
90
|
|
|
90
91
|
useEffect(() => {
|
|
91
92
|
if (open) {
|
|
@@ -96,15 +97,9 @@ export function ImportDialog({
|
|
|
96
97
|
setValidationResult(null)
|
|
97
98
|
setImportResult(null)
|
|
98
99
|
setProgress(0)
|
|
99
|
-
if (fileInputRef.current) fileInputRef.current.value = ''
|
|
100
100
|
}
|
|
101
101
|
}, [open])
|
|
102
102
|
|
|
103
|
-
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
104
|
-
const selectedFile = e.target.files?.[0] ?? null
|
|
105
|
-
setFile(selectedFile)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
103
|
const handleDownloadTemplate = async () => {
|
|
109
104
|
try {
|
|
110
105
|
const response = await api.get(`/dynamic/${model}/export/template`, {
|
|
@@ -268,15 +263,14 @@ export function ImportDialog({
|
|
|
268
263
|
|
|
269
264
|
<div className="space-y-2">
|
|
270
265
|
<Label htmlFor="import-file" className="text-sm font-medium">
|
|
271
|
-
|
|
266
|
+
{t('common.upload.choose', { defaultValue: 'Choose file' })}
|
|
272
267
|
</Label>
|
|
273
|
-
<
|
|
274
|
-
ref={fileInputRef}
|
|
268
|
+
<FilePickButton
|
|
275
269
|
id="import-file"
|
|
276
|
-
type="file"
|
|
277
270
|
accept=".xlsx,.xls,.csv,.json"
|
|
278
|
-
|
|
279
|
-
|
|
271
|
+
hasFile={Boolean(file)}
|
|
272
|
+
fileName={file?.name}
|
|
273
|
+
onFile={setFile}
|
|
280
274
|
/>
|
|
281
275
|
<p className="text-xs text-muted-foreground">
|
|
282
276
|
Formatos aceptados: Excel, CSV, JSON
|
package/src/display-value.tsx
CHANGED
|
@@ -76,15 +76,19 @@ export const RelationThumbnail: React.FC<{
|
|
|
76
76
|
alt: string
|
|
77
77
|
getImageUrl?: (path: string) => string
|
|
78
78
|
size?: number
|
|
79
|
-
}> = ({ src, alt, getImageUrl, size =
|
|
79
|
+
}> = ({ src, alt, getImageUrl, size = 20 }) => (
|
|
80
80
|
<Avatar
|
|
81
|
-
className="shrink-0 rounded-md ring-1 ring-border/40"
|
|
81
|
+
className="shrink-0 rounded-md bg-muted/50 p-[3px] ring-1 ring-border/40"
|
|
82
82
|
style={{ width: size, height: size }}
|
|
83
83
|
>
|
|
84
|
+
{/* object-contain: brand logos are typically wide/rectangular, so
|
|
85
|
+
object-cover (fills the square, cropping the sides) was cutting
|
|
86
|
+
them off. contain lets the whole mark show, letterboxed by the
|
|
87
|
+
padded neutral background above instead of clipped. */}
|
|
84
88
|
<AvatarImage
|
|
85
89
|
src={getImageUrl ? getImageUrl(src) : src}
|
|
86
90
|
alt={alt}
|
|
87
|
-
className="object-
|
|
91
|
+
className="object-contain"
|
|
88
92
|
/>
|
|
89
93
|
<AvatarFallback className="rounded-md bg-primary/10 text-[8px] font-bold text-primary">
|
|
90
94
|
{getInitials(alt)}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// FilePickButton — locale-aware file picker. Native <input type="file"> always
|
|
2
|
+
// paints the browser/OS chrome ("Choose File" / "No file chosen") in the
|
|
3
|
+
// *browser* language, not the app language — so Spanish UIs on an English
|
|
4
|
+
// Chrome show English. Hide the native control and drive it from a Button
|
|
5
|
+
// whose labels come from i18n (common.upload.*).
|
|
6
|
+
import { useCallback, useId, useRef } from 'react'
|
|
7
|
+
import { useTranslation } from 'react-i18next'
|
|
8
|
+
import { Button } from '@asteby/metacore-ui/primitives'
|
|
9
|
+
import { Loader2, Paperclip, X } from 'lucide-react'
|
|
10
|
+
|
|
11
|
+
export interface FilePickButtonProps {
|
|
12
|
+
/** Called with the picked File, or null when cleared. */
|
|
13
|
+
onFile: (file: File | null) => void
|
|
14
|
+
accept?: string
|
|
15
|
+
/** HTML capture attribute (e.g. "environment" for rear camera). */
|
|
16
|
+
capture?: boolean | 'user' | 'environment'
|
|
17
|
+
disabled?: boolean
|
|
18
|
+
loading?: boolean
|
|
19
|
+
/** When true, shows replace + clear affordances. */
|
|
20
|
+
hasFile?: boolean
|
|
21
|
+
/** Optional display name next to the button when hasFile. */
|
|
22
|
+
fileName?: string
|
|
23
|
+
className?: string
|
|
24
|
+
/** Override the empty-state pick label (still falls back to i18n). */
|
|
25
|
+
pickLabel?: string
|
|
26
|
+
replaceLabel?: string
|
|
27
|
+
clearLabel?: string
|
|
28
|
+
id?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function FilePickButton({
|
|
32
|
+
onFile,
|
|
33
|
+
accept,
|
|
34
|
+
capture,
|
|
35
|
+
disabled,
|
|
36
|
+
loading,
|
|
37
|
+
hasFile,
|
|
38
|
+
fileName,
|
|
39
|
+
className,
|
|
40
|
+
pickLabel,
|
|
41
|
+
replaceLabel,
|
|
42
|
+
clearLabel,
|
|
43
|
+
id: idProp,
|
|
44
|
+
}: FilePickButtonProps) {
|
|
45
|
+
const { t } = useTranslation()
|
|
46
|
+
const autoId = useId()
|
|
47
|
+
const id = idProp || autoId
|
|
48
|
+
const inputRef = useRef<HTMLInputElement | null>(null)
|
|
49
|
+
const busy = Boolean(disabled || loading)
|
|
50
|
+
|
|
51
|
+
const pick = useCallback(() => {
|
|
52
|
+
if (busy) return
|
|
53
|
+
inputRef.current?.click()
|
|
54
|
+
}, [busy])
|
|
55
|
+
|
|
56
|
+
const onChange = useCallback(
|
|
57
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
58
|
+
const file = e.target.files?.[0] ?? null
|
|
59
|
+
if (inputRef.current) inputRef.current.value = ''
|
|
60
|
+
if (file) onFile(file)
|
|
61
|
+
},
|
|
62
|
+
[onFile],
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
const onClear = useCallback(() => {
|
|
66
|
+
if (busy) return
|
|
67
|
+
onFile(null)
|
|
68
|
+
}, [busy, onFile])
|
|
69
|
+
|
|
70
|
+
const emptyLabel =
|
|
71
|
+
pickLabel || t('common.upload.choose', { defaultValue: 'Choose file' })
|
|
72
|
+
const filledLabel =
|
|
73
|
+
replaceLabel || t('common.upload.replace', { defaultValue: 'Replace' })
|
|
74
|
+
const removeLabel =
|
|
75
|
+
clearLabel || t('common.upload.remove', { defaultValue: 'Remove file' })
|
|
76
|
+
const emptyHint = t('common.upload.none', { defaultValue: 'No file chosen' })
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className={className ? `grid gap-1.5 ${className}` : 'grid gap-1.5'} data-widget="file-pick">
|
|
80
|
+
<input
|
|
81
|
+
ref={inputRef}
|
|
82
|
+
id={id}
|
|
83
|
+
type="file"
|
|
84
|
+
accept={accept}
|
|
85
|
+
{...(capture !== undefined ? { capture } : {})}
|
|
86
|
+
className="sr-only"
|
|
87
|
+
onChange={onChange}
|
|
88
|
+
tabIndex={-1}
|
|
89
|
+
aria-hidden="true"
|
|
90
|
+
/>
|
|
91
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
92
|
+
<Button type="button" variant="outline" size="sm" onClick={pick} disabled={busy}>
|
|
93
|
+
{loading ? (
|
|
94
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
95
|
+
) : (
|
|
96
|
+
<Paperclip className="mr-2 h-4 w-4" />
|
|
97
|
+
)}
|
|
98
|
+
{hasFile ? filledLabel : emptyLabel}
|
|
99
|
+
</Button>
|
|
100
|
+
{hasFile ? (
|
|
101
|
+
<div className="flex min-w-0 items-center gap-1 text-sm text-muted-foreground">
|
|
102
|
+
{fileName ? (
|
|
103
|
+
<span className="truncate" title={fileName}>
|
|
104
|
+
{fileName}
|
|
105
|
+
</span>
|
|
106
|
+
) : null}
|
|
107
|
+
<Button
|
|
108
|
+
type="button"
|
|
109
|
+
variant="ghost"
|
|
110
|
+
size="sm"
|
|
111
|
+
className="h-6 w-6 p-0"
|
|
112
|
+
onClick={onClear}
|
|
113
|
+
disabled={busy}
|
|
114
|
+
aria-label={removeLabel}
|
|
115
|
+
>
|
|
116
|
+
<X className="h-3.5 w-3.5" />
|
|
117
|
+
</Button>
|
|
118
|
+
</div>
|
|
119
|
+
) : (
|
|
120
|
+
<span className="text-xs text-muted-foreground">{emptyHint}</span>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -120,6 +120,10 @@ export {
|
|
|
120
120
|
type UseDynamicFiltersResult,
|
|
121
121
|
} from './use-dynamic-filters'
|
|
122
122
|
export * from './dynamic-form'
|
|
123
|
+
export {
|
|
124
|
+
FilePickButton,
|
|
125
|
+
type FilePickButtonProps,
|
|
126
|
+
} from './file-pick-button'
|
|
123
127
|
export * from './barcode-scanner'
|
|
124
128
|
export * from './form-layout'
|
|
125
129
|
export * from './form-layout-ui'
|
package/src/upload-field.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// UploadField — the `upload` widget renderer shared by DynamicForm's
|
|
2
2
|
// FieldRenderer and the action-modal-dispatcher's renderField so the two stay
|
|
3
|
-
// in lockstep. Renders a
|
|
4
|
-
// POSTs the picked file to the host upload endpoint as
|
|
5
|
-
// stores the returned file url/path as the field value.
|
|
3
|
+
// in lockstep. Renders a locale-aware FilePickButton that proxies a hidden
|
|
4
|
+
// <input type=file>, POSTs the picked file to the host upload endpoint as
|
|
5
|
+
// multipart/form-data, and stores the returned file url/path as the field value.
|
|
6
6
|
//
|
|
7
7
|
// Endpoint assumption: `POST /uploads` (multipart) returning
|
|
8
8
|
// { success: true, data: { file_url?, url?, path?, file_path? } }
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
// `field.searchEndpoint` (reused as the upload endpoint escape hatch) — kept
|
|
11
11
|
// generic so this carries no host-specific route. Honors field.accept /
|
|
12
12
|
// field.maxSize and forwards field.storagePath as `storage_path`.
|
|
13
|
-
import { useCallback,
|
|
14
|
-
import {
|
|
15
|
-
import { Loader2, Paperclip, X } from 'lucide-react'
|
|
16
|
-
import { useApi } from './api-context'
|
|
13
|
+
import { useCallback, useState } from 'react'
|
|
14
|
+
import { useTranslation } from 'react-i18next'
|
|
17
15
|
import { getUploadConfig } from './dynamic-form-schema'
|
|
16
|
+
import { FilePickButton } from './file-pick-button'
|
|
17
|
+
import { useApi } from './api-context'
|
|
18
18
|
import type { ActionFieldDef } from './types'
|
|
19
19
|
|
|
20
20
|
export interface UploadFieldProps {
|
|
@@ -52,29 +52,31 @@ export function uploadedDisplayName(value: unknown): string {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export function UploadField({ field, value, onChange }: UploadFieldProps) {
|
|
55
|
+
const { t } = useTranslation()
|
|
55
56
|
const api = useApi()
|
|
56
|
-
const inputRef = useRef<HTMLInputElement | null>(null)
|
|
57
57
|
const [uploading, setUploading] = useState(false)
|
|
58
58
|
const [error, setError] = useState<string | null>(null)
|
|
59
59
|
|
|
60
60
|
const { accept, maxSize, storagePath } = getUploadConfig(field)
|
|
61
61
|
const endpoint = field.searchEndpoint || DEFAULT_UPLOAD_ENDPOINT
|
|
62
|
-
|
|
63
|
-
const handlePick = useCallback(() => {
|
|
64
|
-
if (uploading) return
|
|
65
|
-
inputRef.current?.click()
|
|
66
|
-
}, [uploading])
|
|
62
|
+
const hasValue = typeof value === 'string' && value !== ''
|
|
67
63
|
|
|
68
64
|
const handleFile = useCallback(
|
|
69
|
-
async (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
65
|
+
async (file: File | null) => {
|
|
66
|
+
if (!file) {
|
|
67
|
+
setError(null)
|
|
68
|
+
onChange('')
|
|
69
|
+
return
|
|
70
|
+
}
|
|
74
71
|
setError(null)
|
|
75
72
|
if (maxSize && file.size > maxSize) {
|
|
76
73
|
const mb = (maxSize / (1024 * 1024)).toFixed(1)
|
|
77
|
-
setError(
|
|
74
|
+
setError(
|
|
75
|
+
t('common.upload.too_large', {
|
|
76
|
+
mb,
|
|
77
|
+
defaultValue: `File too large (max {{mb}} MB).`,
|
|
78
|
+
}),
|
|
79
|
+
)
|
|
78
80
|
return
|
|
79
81
|
}
|
|
80
82
|
const form = new FormData()
|
|
@@ -94,77 +96,45 @@ export function UploadField({ field, value, onChange }: UploadFieldProps) {
|
|
|
94
96
|
})
|
|
95
97
|
const body = (res as { data?: any })?.data
|
|
96
98
|
if (body && body.success === false) {
|
|
97
|
-
setError(
|
|
99
|
+
setError(
|
|
100
|
+
body.message ||
|
|
101
|
+
t('common.upload.failed', { defaultValue: 'Could not upload the file.' }),
|
|
102
|
+
)
|
|
98
103
|
return
|
|
99
104
|
}
|
|
100
105
|
const stored = extractUploadedValue(body)
|
|
101
106
|
if (!stored) {
|
|
102
|
-
setError(
|
|
107
|
+
setError(
|
|
108
|
+
t('common.upload.invalid', {
|
|
109
|
+
defaultValue: 'Invalid upload response.',
|
|
110
|
+
}),
|
|
111
|
+
)
|
|
103
112
|
return
|
|
104
113
|
}
|
|
105
114
|
onChange(stored)
|
|
106
115
|
} catch (err: any) {
|
|
107
|
-
setError(
|
|
116
|
+
setError(
|
|
117
|
+
err?.response?.data?.message ||
|
|
118
|
+
t('common.upload.failed', { defaultValue: 'Could not upload the file.' }),
|
|
119
|
+
)
|
|
108
120
|
} finally {
|
|
109
121
|
setUploading(false)
|
|
110
122
|
}
|
|
111
123
|
},
|
|
112
|
-
[api, endpoint, maxSize, storagePath, onChange],
|
|
124
|
+
[api, endpoint, maxSize, storagePath, onChange, t],
|
|
113
125
|
)
|
|
114
126
|
|
|
115
|
-
const handleClear = useCallback(() => {
|
|
116
|
-
if (uploading) return
|
|
117
|
-
setError(null)
|
|
118
|
-
onChange('')
|
|
119
|
-
}, [uploading, onChange])
|
|
120
|
-
|
|
121
|
-
const hasValue = typeof value === 'string' && value !== ''
|
|
122
|
-
|
|
123
127
|
return (
|
|
124
128
|
<div className="grid gap-1.5" data-widget="upload">
|
|
125
|
-
<
|
|
126
|
-
ref={inputRef}
|
|
129
|
+
<FilePickButton
|
|
127
130
|
id={field.key}
|
|
128
|
-
type="file"
|
|
129
131
|
accept={accept}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
loading={uploading}
|
|
133
|
+
hasFile={hasValue}
|
|
134
|
+
fileName={hasValue ? uploadedDisplayName(value) : undefined}
|
|
135
|
+
onFile={(f) => void handleFile(f)}
|
|
136
|
+
pickLabel={field.placeholder || undefined}
|
|
134
137
|
/>
|
|
135
|
-
<div className="flex items-center gap-2">
|
|
136
|
-
<Button
|
|
137
|
-
type="button"
|
|
138
|
-
variant="outline"
|
|
139
|
-
size="sm"
|
|
140
|
-
onClick={handlePick}
|
|
141
|
-
disabled={uploading}
|
|
142
|
-
>
|
|
143
|
-
{uploading ? (
|
|
144
|
-
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
145
|
-
) : (
|
|
146
|
-
<Paperclip className="mr-2 h-4 w-4" />
|
|
147
|
-
)}
|
|
148
|
-
{hasValue ? 'Reemplazar' : field.placeholder || 'Subir archivo'}
|
|
149
|
-
</Button>
|
|
150
|
-
{hasValue && !uploading && (
|
|
151
|
-
<div className="flex min-w-0 items-center gap-1 text-sm text-muted-foreground">
|
|
152
|
-
<span className="truncate" title={String(value)}>
|
|
153
|
-
{uploadedDisplayName(value)}
|
|
154
|
-
</span>
|
|
155
|
-
<Button
|
|
156
|
-
type="button"
|
|
157
|
-
variant="ghost"
|
|
158
|
-
size="sm"
|
|
159
|
-
className="h-6 w-6 p-0"
|
|
160
|
-
onClick={handleClear}
|
|
161
|
-
aria-label="Quitar archivo"
|
|
162
|
-
>
|
|
163
|
-
<X className="h-3.5 w-3.5" />
|
|
164
|
-
</Button>
|
|
165
|
-
</div>
|
|
166
|
-
)}
|
|
167
|
-
</div>
|
|
168
138
|
{error && (
|
|
169
139
|
<span className="text-sm text-destructive" role="alert">
|
|
170
140
|
{error}
|