@zauru-sdk/components 2.0.197 → 2.0.199
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 +16 -0
- package/dist/esm/Form/FileUpload/index.js +93 -37
- package/package.json +7 -5
- package/src/Form/FileUpload/index.tsx +143 -48
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [2.0.199](https://github.com/intuitiva/zauru-typescript-sdk/compare/v2.0.198...v2.0.199) (2025-03-19)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @zauru-sdk/components
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## [2.0.198](https://github.com/intuitiva/zauru-typescript-sdk/compare/v2.0.197...v2.0.198) (2025-03-17)
|
|
15
|
+
|
|
16
|
+
**Note:** Version bump only for package @zauru-sdk/components
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
## [2.0.197](https://github.com/intuitiva/zauru-typescript-sdk/compare/v2.0.196...v2.0.197) (2025-03-14)
|
|
7
23
|
|
|
8
24
|
**Note:** Version bump only for package @zauru-sdk/components
|
|
@@ -1,74 +1,130 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { DownloadIconSVG, IdeaIconSVG } from "@zauru-sdk/icons";
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useState, useEffect, useRef } from "react";
|
|
4
4
|
import { useFormContext } from "react-hook-form";
|
|
5
5
|
export const FileUploadField = (props) => {
|
|
6
6
|
const { id, name, title, helpText, hint, onChange, readOnly = false, fileTypes = [], showAvailableTypes = false, className, defaultValue, download = false, required = false, } = props;
|
|
7
|
-
const { register: tempRegister, formState: { errors }, } = useFormContext() || { formState: {} };
|
|
7
|
+
const { register: tempRegister, setValue, formState: { errors }, } = useFormContext() || { formState: {} };
|
|
8
8
|
const error = errors ? errors[name] : undefined;
|
|
9
9
|
const register = tempRegister ? tempRegister(name, { required }) : undefined;
|
|
10
10
|
const [showTooltip, setShowTooltip] = useState(false);
|
|
11
|
-
|
|
11
|
+
const [previewSrc, setPreviewSrc] = useState(null);
|
|
12
|
+
const [fileDeleted, setFileDeleted] = useState(false);
|
|
13
|
+
const [uploading, setUploading] = useState(false);
|
|
14
|
+
const [uploadProgress, setUploadProgress] = useState(0);
|
|
15
|
+
const fileInputRef = useRef(null);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
return () => {
|
|
18
|
+
if (previewSrc) {
|
|
19
|
+
URL.revokeObjectURL(previewSrc);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}, [previewSrc]);
|
|
12
23
|
let hintMessage = hint;
|
|
13
24
|
if (showAvailableTypes && fileTypes.length > 0) {
|
|
14
25
|
hintMessage = `${hint || ""} Archivos permitidos: ${fileTypes.join(", ")}`;
|
|
15
26
|
}
|
|
16
|
-
// Clases de estilo basadas en si hay error (color rojo) o no (gris),
|
|
17
|
-
// pero ahora ignoramos el "disabled" y nos centramos en "readOnly".
|
|
18
27
|
const color = error ? "red" : "gray";
|
|
19
28
|
const isReadOnly = readOnly;
|
|
20
|
-
// En modo readOnly, puedes poner un fondo distinto, o dejarlo en blanco
|
|
21
29
|
const bgColor = isReadOnly ? "bg-gray-100" : `bg-${color}-50`;
|
|
22
30
|
const textColor = isReadOnly ? "text-gray-700" : `text-${color}-900`;
|
|
23
31
|
const borderColor = error ? "border-red-500" : `border-${color}-500`;
|
|
24
32
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
33
|
+
* Función que se dispara cuando el usuario selecciona un archivo.
|
|
34
|
+
* Además de actualizar la vista previa, inicia la subida directa a AWS.
|
|
27
35
|
*/
|
|
28
36
|
const handleInputChange = (event) => {
|
|
29
37
|
onChange && onChange(event);
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
setFileDeleted(false);
|
|
39
|
+
if (event.target.files && event.target.files.length > 0) {
|
|
40
|
+
const file = event.target.files[0];
|
|
41
|
+
// Actualizamos la vista previa para imágenes
|
|
42
|
+
if (file && file.type.startsWith("image/")) {
|
|
43
|
+
const objectUrl = URL.createObjectURL(file);
|
|
44
|
+
setPreviewSrc(objectUrl);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
setPreviewSrc(null);
|
|
48
|
+
}
|
|
49
|
+
// Importamos dinámicamente DirectUpload solo en el cliente
|
|
50
|
+
import("@rails/activestorage")
|
|
51
|
+
.then(({ DirectUpload }) => {
|
|
52
|
+
const uploadUrl = "https://zauru.herokuapp.com/rails/active_storage/direct_uploads";
|
|
53
|
+
// Inicializamos el progreso y activamos el estado de subida
|
|
54
|
+
setUploading(true);
|
|
55
|
+
setUploadProgress(0);
|
|
56
|
+
const directUpload = new DirectUpload(file, uploadUrl, {
|
|
57
|
+
directUploadWillStoreFileWithXHR: (xhr) => {
|
|
58
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
59
|
+
if (event.lengthComputable) {
|
|
60
|
+
const progress = Math.round((event.loaded / event.total) * 100);
|
|
61
|
+
setUploadProgress(progress);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
directUpload.create((error, blob) => {
|
|
67
|
+
setUploading(false);
|
|
68
|
+
if (error) {
|
|
69
|
+
console.error("Error al subir el archivo:", error);
|
|
70
|
+
// Manejo de error según tus necesidades
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// blob.signed_id es el identificador que debes enviar a tu API
|
|
74
|
+
console.log("Archivo subido exitosamente. Blob:", blob);
|
|
75
|
+
setValue(name, blob.signed_id);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
})
|
|
79
|
+
.catch((err) => console.error("Error al cargar DirectUpload:", err));
|
|
80
|
+
}
|
|
32
81
|
};
|
|
33
82
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* - Si `download` es false, mostramos la imagen en miniatura.
|
|
37
|
-
* El click abre la URL en nueva ventana.
|
|
83
|
+
* Función para eliminar el archivo. Además de limpiar la vista previa,
|
|
84
|
+
* limpia el input y el valor del campo en el formulario.
|
|
38
85
|
*/
|
|
39
|
-
|
|
86
|
+
const deleteFile = () => {
|
|
87
|
+
setPreviewSrc(null);
|
|
88
|
+
setFileDeleted(true);
|
|
89
|
+
if (fileInputRef.current) {
|
|
90
|
+
fileInputRef.current.value = "";
|
|
91
|
+
}
|
|
92
|
+
setValue(name, "");
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Renderiza la vista previa del archivo.
|
|
96
|
+
* - Si `download` es true, muestra un botón para descargar.
|
|
97
|
+
* - Si no, muestra la imagen en miniatura.
|
|
98
|
+
*/
|
|
99
|
+
function renderPreview(src) {
|
|
40
100
|
if (download) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
window.open(defaultValue, "_blank");
|
|
45
|
-
}
|
|
101
|
+
return (_jsxs("div", { role: "button", tabIndex: 0, onClick: () => window.open(src, "_blank"), onKeyDown: (event) => {
|
|
102
|
+
if (event.key === "Enter")
|
|
103
|
+
window.open(src, "_blank");
|
|
46
104
|
}, className: "inline-flex items-center cursor-pointer", children: [_jsx(DownloadIconSVG, {}), _jsx("span", { className: "ml-1 text-blue-600 underline", children: "Descargar archivo" })] }));
|
|
47
105
|
}
|
|
48
106
|
else {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
}, className: "inline-block cursor-pointer", children: _jsx("img", { src: defaultValue, alt: name, className: "h-48 w-48 inline mr-1 pb-1", style: {
|
|
55
|
-
objectFit: "contain",
|
|
56
|
-
backgroundColor: "transparent",
|
|
57
|
-
} }) }));
|
|
107
|
+
return (_jsx("div", { role: "button", tabIndex: 0, onClick: () => window.open(src, "_blank"), onKeyDown: (event) => {
|
|
108
|
+
if (event.key === "Enter")
|
|
109
|
+
window.open(src, "_blank");
|
|
110
|
+
}, className: "inline-block cursor-pointer", children: _jsx("img", { src: src, alt: name, className: "h-48 w-48 inline mr-1 pb-1", style: { objectFit: "contain", backgroundColor: "transparent" } }) }));
|
|
58
111
|
}
|
|
59
112
|
}
|
|
60
113
|
/**
|
|
61
|
-
* 1)
|
|
62
|
-
* - Si defaultValue es string
|
|
63
|
-
* - Si no hay defaultValue (o es File)
|
|
114
|
+
* 1) Modo readOnly:
|
|
115
|
+
* - Si defaultValue es string, se muestra el preview (descarga/imagen).
|
|
116
|
+
* - Si no hay defaultValue (o es File), se muestra "Sin archivo".
|
|
64
117
|
*/
|
|
65
118
|
if (readOnly) {
|
|
66
|
-
return (_jsxs("div", { className: `col-span-6 sm:col-span-3 ${className}`, children: [title && (_jsx("label", { htmlFor: name, className:
|
|
119
|
+
return (_jsxs("div", { className: `col-span-6 sm:col-span-3 ${className}`, children: [title && (_jsx("label", { htmlFor: name, className: "block mb-1 text-sm font-medium text-gray-700", children: title })), typeof defaultValue === "string" && defaultValue ? (renderPreview(defaultValue)) : (_jsx("div", { className: "text-sm italic text-gray-400", children: "No hay archivo disponible" }))] }));
|
|
67
120
|
}
|
|
68
121
|
/**
|
|
69
|
-
* 2) readOnly = false:
|
|
70
|
-
* - Si
|
|
71
|
-
*
|
|
122
|
+
* 2) Modo editable (readOnly = false):
|
|
123
|
+
* - Si se ha seleccionado una imagen o existe defaultValue y no se ha eliminado,
|
|
124
|
+
* se muestra la vista previa generada junto con un botón para eliminar el archivo.
|
|
125
|
+
* - Si se elimina el archivo o no hay ninguno, se muestra el input para cargar uno.
|
|
72
126
|
*/
|
|
73
|
-
return (_jsxs("div", { className: `col-span-6 sm:col-span-3 ${className}`, children: [title && (_jsxs("label", { htmlFor: name, className: `block mb-1 text-sm font-medium text-${color}-700`, children: [title, required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] })),
|
|
127
|
+
return (_jsxs("div", { className: `col-span-6 sm:col-span-3 ${className}`, children: [title && (_jsxs("label", { htmlFor: name, className: `block mb-1 text-sm font-medium text-${color}-700`, children: [title, required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] })), !fileDeleted &&
|
|
128
|
+
(previewSrc ? (_jsxs("div", { className: "mb-2 flex items-center", children: [renderPreview(previewSrc), _jsx("button", { type: "button", onClick: deleteFile, className: "ml-2 text-red-600 underline text-sm", children: "Eliminar archivo" })] })) : (typeof defaultValue === "string" &&
|
|
129
|
+
defaultValue && (_jsxs("div", { className: "mb-2 flex items-center", children: [renderPreview(defaultValue), _jsx("button", { type: "button", onClick: deleteFile, className: "ml-2 text-red-600 underline text-sm", children: "Eliminar archivo" })] })))), _jsxs("div", { className: "flex relative items-center", children: [_jsx("input", { type: "file", id: id ?? name, accept: fileTypes.map((ft) => `.${ft}`).join(", "), className: `block w-full rounded-md ${bgColor} ${borderColor} ${textColor} shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm`, ...(register ?? {}), ref: fileInputRef, name: name, onChange: handleInputChange }), helpText && (_jsx("div", { className: "flex items-center relative ml-3", children: _jsxs("div", { className: "relative cursor-pointer", onMouseEnter: () => setShowTooltip(true), onMouseLeave: () => setShowTooltip(false), children: [_jsx(IdeaIconSVG, {}), showTooltip && (_jsx("div", { className: "absolute -left-48 top-0 mt-8 p-2 bg-white border rounded shadow text-black z-50", children: helpText }))] }) }))] }), uploading && (_jsxs("div", { className: "mt-2", children: [_jsx("progress", { value: uploadProgress, max: "100", className: "w-full" }), _jsxs("p", { className: "text-sm text-blue-600 mt-1", children: ["Subiendo archivo: ", uploadProgress, "%"] })] })), error && (_jsxs("p", { className: "mt-2 text-sm text-red-600", children: [_jsx("span", { className: "font-medium", children: "\u00A1Oops!" }), " ", error.message?.toString()] })), !error && hintMessage && (_jsx("p", { className: `mt-2 italic text-sm text-${color}-500`, children: hintMessage }))] }));
|
|
74
130
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zauru-sdk/components",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.199",
|
|
4
4
|
"description": "Componentes reutilizables en las WebApps de Zauru.",
|
|
5
5
|
"main": "./dist/esm/index.js",
|
|
6
6
|
"module": "./dist/esm/index.js",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@tailwindcss/forms": "^0.5.7",
|
|
24
24
|
"@types/jsonwebtoken": "^9.0.2",
|
|
25
|
+
"@types/rails__activestorage": "^7.1.1",
|
|
25
26
|
"@types/react": "^18.2.20",
|
|
26
27
|
"@types/react-dom": "^18.2.7",
|
|
27
28
|
"@types/styled-components": "^5.1.34",
|
|
@@ -31,13 +32,14 @@
|
|
|
31
32
|
},
|
|
32
33
|
"dependencies": {
|
|
33
34
|
"@hookform/resolvers": "^3.9.0",
|
|
35
|
+
"@rails/activestorage": "^8.0.200",
|
|
34
36
|
"@reduxjs/toolkit": "^2.2.1",
|
|
35
37
|
"@remix-run/react": "^2.8.1",
|
|
36
|
-
"@zauru-sdk/common": "^2.0.
|
|
37
|
-
"@zauru-sdk/hooks": "^2.0.
|
|
38
|
+
"@zauru-sdk/common": "^2.0.198",
|
|
39
|
+
"@zauru-sdk/hooks": "^2.0.198",
|
|
38
40
|
"@zauru-sdk/icons": "^2.0.188",
|
|
39
41
|
"@zauru-sdk/types": "^2.0.197",
|
|
40
|
-
"@zauru-sdk/utils": "^2.0.
|
|
42
|
+
"@zauru-sdk/utils": "^2.0.199",
|
|
41
43
|
"framer-motion": "^11.7.0",
|
|
42
44
|
"jsonwebtoken": "^9.0.2",
|
|
43
45
|
"react": "^18.2.0",
|
|
@@ -49,5 +51,5 @@
|
|
|
49
51
|
"styled-components": "^5.3.5",
|
|
50
52
|
"zod": "^3.23.8"
|
|
51
53
|
},
|
|
52
|
-
"gitHead": "
|
|
54
|
+
"gitHead": "ac9f963d081c81851a7890a0634fac9bbc8ff7d6"
|
|
53
55
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DownloadIconSVG, IdeaIconSVG } from "@zauru-sdk/icons";
|
|
2
|
-
import React, { useState } from "react";
|
|
2
|
+
import React, { useState, useEffect, useRef } from "react";
|
|
3
3
|
import { useFormContext } from "react-hook-form";
|
|
4
4
|
|
|
5
5
|
type Props = {
|
|
@@ -10,7 +10,7 @@ type Props = {
|
|
|
10
10
|
helpText?: string;
|
|
11
11
|
hint?: string;
|
|
12
12
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
13
|
-
readOnly?: boolean;
|
|
13
|
+
readOnly?: boolean;
|
|
14
14
|
fileTypes?: string[];
|
|
15
15
|
showAvailableTypes?: boolean;
|
|
16
16
|
className?: string;
|
|
@@ -38,6 +38,7 @@ export const FileUploadField = (props: Props) => {
|
|
|
38
38
|
|
|
39
39
|
const {
|
|
40
40
|
register: tempRegister,
|
|
41
|
+
setValue,
|
|
41
42
|
formState: { errors },
|
|
42
43
|
} = useFormContext() || { formState: {} };
|
|
43
44
|
|
|
@@ -45,50 +46,116 @@ export const FileUploadField = (props: Props) => {
|
|
|
45
46
|
const register = tempRegister ? tempRegister(name, { required }) : undefined;
|
|
46
47
|
|
|
47
48
|
const [showTooltip, setShowTooltip] = useState<boolean>(false);
|
|
49
|
+
const [previewSrc, setPreviewSrc] = useState<string | null>(null);
|
|
50
|
+
const [fileDeleted, setFileDeleted] = useState<boolean>(false);
|
|
51
|
+
const [uploading, setUploading] = useState<boolean>(false);
|
|
52
|
+
const [uploadProgress, setUploadProgress] = useState<number>(0);
|
|
53
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
return () => {
|
|
57
|
+
if (previewSrc) {
|
|
58
|
+
URL.revokeObjectURL(previewSrc);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}, [previewSrc]);
|
|
48
62
|
|
|
49
|
-
// Para mostrar en el hint los tipos de archivo permitidos (opcional)
|
|
50
63
|
let hintMessage = hint;
|
|
51
64
|
if (showAvailableTypes && fileTypes.length > 0) {
|
|
52
65
|
hintMessage = `${hint || ""} Archivos permitidos: ${fileTypes.join(", ")}`;
|
|
53
66
|
}
|
|
54
67
|
|
|
55
|
-
// Clases de estilo basadas en si hay error (color rojo) o no (gris),
|
|
56
|
-
// pero ahora ignoramos el "disabled" y nos centramos en "readOnly".
|
|
57
68
|
const color = error ? "red" : "gray";
|
|
58
69
|
const isReadOnly = readOnly;
|
|
59
|
-
// En modo readOnly, puedes poner un fondo distinto, o dejarlo en blanco
|
|
60
70
|
const bgColor = isReadOnly ? "bg-gray-100" : `bg-${color}-50`;
|
|
61
71
|
const textColor = isReadOnly ? "text-gray-700" : `text-${color}-900`;
|
|
62
72
|
const borderColor = error ? "border-red-500" : `border-${color}-500`;
|
|
63
73
|
|
|
64
74
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
75
|
+
* Función que se dispara cuando el usuario selecciona un archivo.
|
|
76
|
+
* Además de actualizar la vista previa, inicia la subida directa a AWS.
|
|
67
77
|
*/
|
|
68
78
|
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
69
79
|
onChange && onChange(event);
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
setFileDeleted(false);
|
|
81
|
+
|
|
82
|
+
if (event.target.files && event.target.files.length > 0) {
|
|
83
|
+
const file = event.target.files[0];
|
|
84
|
+
|
|
85
|
+
// Actualizamos la vista previa para imágenes
|
|
86
|
+
if (file && file.type.startsWith("image/")) {
|
|
87
|
+
const objectUrl = URL.createObjectURL(file);
|
|
88
|
+
setPreviewSrc(objectUrl);
|
|
89
|
+
} else {
|
|
90
|
+
setPreviewSrc(null);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Importamos dinámicamente DirectUpload solo en el cliente
|
|
94
|
+
import("@rails/activestorage")
|
|
95
|
+
.then(({ DirectUpload }) => {
|
|
96
|
+
const uploadUrl =
|
|
97
|
+
"https://zauru.herokuapp.com/rails/active_storage/direct_uploads";
|
|
98
|
+
|
|
99
|
+
// Inicializamos el progreso y activamos el estado de subida
|
|
100
|
+
setUploading(true);
|
|
101
|
+
setUploadProgress(0);
|
|
102
|
+
|
|
103
|
+
const directUpload = new DirectUpload(file, uploadUrl, {
|
|
104
|
+
directUploadWillStoreFileWithXHR: (xhr: XMLHttpRequest) => {
|
|
105
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
106
|
+
if (event.lengthComputable) {
|
|
107
|
+
const progress = Math.round(
|
|
108
|
+
(event.loaded / event.total) * 100
|
|
109
|
+
);
|
|
110
|
+
setUploadProgress(progress);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
directUpload.create((error, blob) => {
|
|
117
|
+
setUploading(false);
|
|
118
|
+
if (error) {
|
|
119
|
+
console.error("Error al subir el archivo:", error);
|
|
120
|
+
// Manejo de error según tus necesidades
|
|
121
|
+
} else {
|
|
122
|
+
// blob.signed_id es el identificador que debes enviar a tu API
|
|
123
|
+
console.log("Archivo subido exitosamente. Blob:", blob);
|
|
124
|
+
setValue(name, blob.signed_id);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
})
|
|
128
|
+
.catch((err) => console.error("Error al cargar DirectUpload:", err));
|
|
129
|
+
}
|
|
72
130
|
};
|
|
73
131
|
|
|
74
132
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* - Si `download` es false, mostramos la imagen en miniatura.
|
|
78
|
-
* El click abre la URL en nueva ventana.
|
|
133
|
+
* Función para eliminar el archivo. Además de limpiar la vista previa,
|
|
134
|
+
* limpia el input y el valor del campo en el formulario.
|
|
79
135
|
*/
|
|
80
|
-
|
|
136
|
+
const deleteFile = () => {
|
|
137
|
+
setPreviewSrc(null);
|
|
138
|
+
setFileDeleted(true);
|
|
139
|
+
if (fileInputRef.current) {
|
|
140
|
+
fileInputRef.current.value = "";
|
|
141
|
+
}
|
|
142
|
+
setValue(name, "");
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Renderiza la vista previa del archivo.
|
|
147
|
+
* - Si `download` es true, muestra un botón para descargar.
|
|
148
|
+
* - Si no, muestra la imagen en miniatura.
|
|
149
|
+
*/
|
|
150
|
+
function renderPreview(src: string) {
|
|
81
151
|
if (download) {
|
|
82
|
-
// Botón de descarga
|
|
83
152
|
return (
|
|
84
153
|
<div
|
|
85
154
|
role="button"
|
|
86
155
|
tabIndex={0}
|
|
87
|
-
onClick={() => window.open(
|
|
156
|
+
onClick={() => window.open(src, "_blank")}
|
|
88
157
|
onKeyDown={(event) => {
|
|
89
|
-
if (event.key === "Enter")
|
|
90
|
-
window.open(defaultValue, "_blank");
|
|
91
|
-
}
|
|
158
|
+
if (event.key === "Enter") window.open(src, "_blank");
|
|
92
159
|
}}
|
|
93
160
|
className="inline-flex items-center cursor-pointer"
|
|
94
161
|
>
|
|
@@ -99,27 +166,21 @@ export const FileUploadField = (props: Props) => {
|
|
|
99
166
|
</div>
|
|
100
167
|
);
|
|
101
168
|
} else {
|
|
102
|
-
// Vista previa como imagen
|
|
103
169
|
return (
|
|
104
170
|
<div
|
|
105
171
|
role="button"
|
|
106
172
|
tabIndex={0}
|
|
107
|
-
onClick={() => window.open(
|
|
173
|
+
onClick={() => window.open(src, "_blank")}
|
|
108
174
|
onKeyDown={(event) => {
|
|
109
|
-
if (event.key === "Enter")
|
|
110
|
-
window.open(defaultValue, "_blank");
|
|
111
|
-
}
|
|
175
|
+
if (event.key === "Enter") window.open(src, "_blank");
|
|
112
176
|
}}
|
|
113
177
|
className="inline-block cursor-pointer"
|
|
114
178
|
>
|
|
115
179
|
<img
|
|
116
|
-
src={
|
|
180
|
+
src={src}
|
|
117
181
|
alt={name}
|
|
118
182
|
className="h-48 w-48 inline mr-1 pb-1"
|
|
119
|
-
style={{
|
|
120
|
-
objectFit: "contain",
|
|
121
|
-
backgroundColor: "transparent",
|
|
122
|
-
}}
|
|
183
|
+
style={{ objectFit: "contain", backgroundColor: "transparent" }}
|
|
123
184
|
/>
|
|
124
185
|
</div>
|
|
125
186
|
);
|
|
@@ -127,9 +188,9 @@ export const FileUploadField = (props: Props) => {
|
|
|
127
188
|
}
|
|
128
189
|
|
|
129
190
|
/**
|
|
130
|
-
* 1)
|
|
131
|
-
* - Si defaultValue es string
|
|
132
|
-
* - Si no hay defaultValue (o es File)
|
|
191
|
+
* 1) Modo readOnly:
|
|
192
|
+
* - Si defaultValue es string, se muestra el preview (descarga/imagen).
|
|
193
|
+
* - Si no hay defaultValue (o es File), se muestra "Sin archivo".
|
|
133
194
|
*/
|
|
134
195
|
if (readOnly) {
|
|
135
196
|
return (
|
|
@@ -137,12 +198,11 @@ export const FileUploadField = (props: Props) => {
|
|
|
137
198
|
{title && (
|
|
138
199
|
<label
|
|
139
200
|
htmlFor={name}
|
|
140
|
-
className=
|
|
201
|
+
className="block mb-1 text-sm font-medium text-gray-700"
|
|
141
202
|
>
|
|
142
203
|
{title}
|
|
143
204
|
</label>
|
|
144
205
|
)}
|
|
145
|
-
|
|
146
206
|
{typeof defaultValue === "string" && defaultValue ? (
|
|
147
207
|
renderPreview(defaultValue)
|
|
148
208
|
) : (
|
|
@@ -155,9 +215,10 @@ export const FileUploadField = (props: Props) => {
|
|
|
155
215
|
}
|
|
156
216
|
|
|
157
217
|
/**
|
|
158
|
-
* 2) readOnly = false:
|
|
159
|
-
* - Si
|
|
160
|
-
*
|
|
218
|
+
* 2) Modo editable (readOnly = false):
|
|
219
|
+
* - Si se ha seleccionado una imagen o existe defaultValue y no se ha eliminado,
|
|
220
|
+
* se muestra la vista previa generada junto con un botón para eliminar el archivo.
|
|
221
|
+
* - Si se elimina el archivo o no hay ninguno, se muestra el input para cargar uno.
|
|
161
222
|
*/
|
|
162
223
|
return (
|
|
163
224
|
<div className={`col-span-6 sm:col-span-3 ${className}`}>
|
|
@@ -171,12 +232,34 @@ export const FileUploadField = (props: Props) => {
|
|
|
171
232
|
</label>
|
|
172
233
|
)}
|
|
173
234
|
|
|
174
|
-
{
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
235
|
+
{!fileDeleted &&
|
|
236
|
+
(previewSrc ? (
|
|
237
|
+
<div className="mb-2 flex items-center">
|
|
238
|
+
{renderPreview(previewSrc)}
|
|
239
|
+
<button
|
|
240
|
+
type="button"
|
|
241
|
+
onClick={deleteFile}
|
|
242
|
+
className="ml-2 text-red-600 underline text-sm"
|
|
243
|
+
>
|
|
244
|
+
Eliminar archivo
|
|
245
|
+
</button>
|
|
246
|
+
</div>
|
|
247
|
+
) : (
|
|
248
|
+
typeof defaultValue === "string" &&
|
|
249
|
+
defaultValue && (
|
|
250
|
+
<div className="mb-2 flex items-center">
|
|
251
|
+
{renderPreview(defaultValue)}
|
|
252
|
+
<button
|
|
253
|
+
type="button"
|
|
254
|
+
onClick={deleteFile}
|
|
255
|
+
className="ml-2 text-red-600 underline text-sm"
|
|
256
|
+
>
|
|
257
|
+
Eliminar archivo
|
|
258
|
+
</button>
|
|
259
|
+
</div>
|
|
260
|
+
)
|
|
261
|
+
))}
|
|
178
262
|
|
|
179
|
-
{/* Input para cambiar/cargar archivo */}
|
|
180
263
|
<div className="flex relative items-center">
|
|
181
264
|
<input
|
|
182
265
|
type="file"
|
|
@@ -184,11 +267,11 @@ export const FileUploadField = (props: Props) => {
|
|
|
184
267
|
accept={fileTypes.map((ft) => `.${ft}`).join(", ")}
|
|
185
268
|
className={`block w-full rounded-md ${bgColor} ${borderColor} ${textColor} shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm`}
|
|
186
269
|
{...(register ?? {})}
|
|
270
|
+
ref={fileInputRef}
|
|
187
271
|
name={name}
|
|
188
272
|
onChange={handleInputChange}
|
|
189
273
|
/>
|
|
190
274
|
|
|
191
|
-
{/* Botón de ayuda con tooltip */}
|
|
192
275
|
{helpText && (
|
|
193
276
|
<div className="flex items-center relative ml-3">
|
|
194
277
|
<div
|
|
@@ -207,13 +290,25 @@ export const FileUploadField = (props: Props) => {
|
|
|
207
290
|
)}
|
|
208
291
|
</div>
|
|
209
292
|
|
|
210
|
-
{
|
|
293
|
+
{uploading && (
|
|
294
|
+
<div className="mt-2">
|
|
295
|
+
<progress
|
|
296
|
+
value={uploadProgress}
|
|
297
|
+
max="100"
|
|
298
|
+
className="w-full"
|
|
299
|
+
></progress>
|
|
300
|
+
<p className="text-sm text-blue-600 mt-1">
|
|
301
|
+
Subiendo archivo: {uploadProgress}%
|
|
302
|
+
</p>
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
|
|
211
306
|
{error && (
|
|
212
|
-
<p className=
|
|
213
|
-
<span className="font-medium"
|
|
307
|
+
<p className="mt-2 text-sm text-red-600">
|
|
308
|
+
<span className="font-medium">¡Oops!</span>{" "}
|
|
309
|
+
{error.message?.toString()}
|
|
214
310
|
</p>
|
|
215
311
|
)}
|
|
216
|
-
{/* Hint (si no hay error) */}
|
|
217
312
|
{!error && hintMessage && (
|
|
218
313
|
<p className={`mt-2 italic text-sm text-${color}-500`}>{hintMessage}</p>
|
|
219
314
|
)}
|