@zauru-sdk/components 2.0.196 → 2.0.198

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
@@ -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.198](https://github.com/intuitiva/zauru-typescript-sdk/compare/v2.0.197...v2.0.198) (2025-03-17)
7
+
8
+ **Note:** Version bump only for package @zauru-sdk/components
9
+
10
+
11
+
12
+
13
+
14
+ ## [2.0.197](https://github.com/intuitiva/zauru-typescript-sdk/compare/v2.0.196...v2.0.197) (2025-03-14)
15
+
16
+ **Note:** Version bump only for package @zauru-sdk/components
17
+
18
+
19
+
20
+
21
+
6
22
  ## [2.0.196](https://github.com/intuitiva/zauru-typescript-sdk/compare/v2.0.195...v2.0.196) (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
- // Para mostrar en el hint los tipos de archivo permitidos (opcional)
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
- * onChange normal del input.
26
- * Sólo se llama cuando readOnly es false (porque si es true ni renderizamos el input).
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
- // Si usas register, la parte interna de react-hook-form también se encargará
31
- // del cambio, no necesitas llamarlo manualmente.
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
- * Para el "preview" cuando `defaultValue` es string:
35
- * - Si `download` es true, mostramos un icono de descarga.
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
- function renderPreview(defaultValue) {
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
- // Botón de descarga
42
- return (_jsxs("div", { role: "button", tabIndex: 0, onClick: () => window.open(defaultValue, "_blank"), onKeyDown: (event) => {
43
- if (event.key === "Enter") {
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
- // Vista previa como imagen
50
- return (_jsx("div", { role: "button", tabIndex: 0, onClick: () => window.open(defaultValue, "_blank"), onKeyDown: (event) => {
51
- if (event.key === "Enter") {
52
- window.open(defaultValue, "_blank");
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) Si readOnly = true:
62
- * - Si defaultValue es string -> Sólo mostramos el preview (descarga/imagen).
63
- * - Si no hay defaultValue (o es File) -> mostramos "nada" o un texto de "Sin archivo".
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: `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" }))] }));
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 hay defaultValue y es string, mostramos la vista previa + el input
71
- * - Si no hay defaultValue (o no es string) mostramos solo el input
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: "*" })] })), typeof defaultValue === "string" && defaultValue && (_jsx("div", { className: "mb-2", children: renderPreview(defaultValue) })), _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 ?? {}), 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 }))] }) }))] }), error && (_jsxs("p", { className: `mt-2 text-sm text-red-600`, children: [_jsx("span", { className: "font-medium", children: "Oops!" }), " ", error.message?.toString()] })), !error && hintMessage && (_jsx("p", { className: `mt-2 italic text-sm text-${color}-500`, children: hintMessage }))] }));
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.196",
3
+ "version": "2.0.198",
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.196",
37
- "@zauru-sdk/hooks": "^2.0.196",
38
+ "@zauru-sdk/common": "^2.0.198",
39
+ "@zauru-sdk/hooks": "^2.0.198",
38
40
  "@zauru-sdk/icons": "^2.0.188",
39
- "@zauru-sdk/types": "^2.0.196",
40
- "@zauru-sdk/utils": "^2.0.196",
41
+ "@zauru-sdk/types": "^2.0.197",
42
+ "@zauru-sdk/utils": "^2.0.198",
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": "44b2674d630e996212a1654751c5b8393e9a08e0"
54
+ "gitHead": "9eb96c25762ab502d51ac347e735fe46a7772952"
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; // <-- Usamos readOnly en lugar de disabled
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
- * onChange normal del input.
66
- * Sólo se llama cuando readOnly es false (porque si es true ni renderizamos el input).
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
- // Si usas register, la parte interna de react-hook-form también se encargará
71
- // del cambio, no necesitas llamarlo manualmente.
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
- * Para el "preview" cuando `defaultValue` es string:
76
- * - Si `download` es true, mostramos un icono de descarga.
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
- function renderPreview(defaultValue: string) {
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(defaultValue, "_blank")}
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(defaultValue, "_blank")}
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={defaultValue}
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) Si readOnly = true:
131
- * - Si defaultValue es string -> Sólo mostramos el preview (descarga/imagen).
132
- * - Si no hay defaultValue (o es File) -> mostramos "nada" o un texto de "Sin archivo".
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={`block mb-1 text-sm font-medium text-gray-700`}
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 hay defaultValue y es string, mostramos la vista previa + el input
160
- * - Si no hay defaultValue (o no es string) mostramos solo el input
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
- {/* Mostrar la vista previa si defaultValue es string */}
175
- {typeof defaultValue === "string" && defaultValue && (
176
- <div className="mb-2">{renderPreview(defaultValue)}</div>
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
- {/* Mensaje de error */}
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={`mt-2 text-sm text-red-600`}>
213
- <span className="font-medium">Oops!</span> {error.message?.toString()}
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
  )}