@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 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;AAG7C,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,+BAgYnB"}
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"}
@@ -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, useRef } from 'react';
8
- import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Button, Input, Label, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@asteby/metacore-ui/primitives';
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: "Archivo" }), _jsx(Input, { ref: fileInputRef, id: "import-file", type: "file", accept: ".xlsx,.xls,.csv,.json", onChange: handleFileChange, className: "cursor-pointer" }), _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 ||
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,CAcA,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"}
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"}
@@ -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 = 18 }) => (_jsxs(Avatar, { className: "shrink-0 rounded-md ring-1 ring-border/40", style: { width: size, height: size }, children: [_jsx(AvatarImage, { src: getImageUrl ? getImageUrl(src) : src, alt: alt, className: "object-cover" }), _jsx(AvatarFallback, { className: "rounded-md bg-primary/10 text-[8px] font-bold text-primary", children: getInitials(alt) })] }));
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';
@@ -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,+BAyHvE"}
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"}
@@ -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 themed Button that proxies a hidden <input type=file>,
5
- // POSTs the picked file to the host upload endpoint as multipart/form-data, and
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, useRef, useState } from 'react';
15
- import { Button } from '@asteby/metacore-ui/primitives';
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 handlePick = useCallback(() => {
56
- if (uploading)
57
- return;
58
- inputRef.current?.click();
59
- }, [uploading]);
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(`Archivo muy grande (máx. ${mb} MB).`);
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 || 'No se pudo subir el archivo.');
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('Respuesta de subida inválida.');
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 || 'No se pudo subir el archivo.');
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
- const handleClear = useCallback(() => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "34.0.0",
3
+ "version": "34.0.2",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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, useRef } from 'react'
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
- Archivo
266
+ {t('common.upload.choose', { defaultValue: 'Choose file' })}
272
267
  </Label>
273
- <Input
274
- ref={fileInputRef}
268
+ <FilePickButton
275
269
  id="import-file"
276
- type="file"
277
270
  accept=".xlsx,.xls,.csv,.json"
278
- onChange={handleFileChange}
279
- className="cursor-pointer"
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
@@ -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 = 18 }) => (
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-cover"
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'
@@ -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 themed Button that proxies a hidden <input type=file>,
4
- // POSTs the picked file to the host upload endpoint as multipart/form-data, and
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, useRef, useState } from 'react'
14
- import { Button } from '@asteby/metacore-ui/primitives'
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 (e: React.ChangeEvent<HTMLInputElement>) => {
70
- const file = e.target.files?.[0]
71
- // Reset the input so picking the same file again re-fires change.
72
- if (inputRef.current) inputRef.current.value = ''
73
- if (!file) return
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(`Archivo muy grande (máx. ${mb} MB).`)
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(body.message || 'No se pudo subir el archivo.')
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('Respuesta de subida inválida.')
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(err?.response?.data?.message || 'No se pudo subir el archivo.')
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
- <input
126
- ref={inputRef}
129
+ <FilePickButton
127
130
  id={field.key}
128
- type="file"
129
131
  accept={accept}
130
- className="sr-only"
131
- onChange={handleFile}
132
- tabIndex={-1}
133
- aria-hidden="true"
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}