@zauru-sdk/components 2.0.202 → 2.0.204
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/Containers/ButtonSectionContainer.d.ts +0 -1
- package/dist/DynamicTable/DynamicPrintTable.d.ts +0 -1
- package/dist/Form/DynamicBaculoForm/index.d.ts +2 -0
- package/dist/Form/FileUpload/index.d.ts +2 -1
- package/dist/Form/TimePicker/index.d.ts +0 -1
- package/dist/Form/YesNo/index.d.ts +0 -1
- package/dist/Modal/ItemModal.d.ts +1 -1
- package/dist/esm/BlockUI/BlockUI.js +1 -1
- package/dist/esm/Buttons/Button.js +7 -8
- package/dist/esm/Containers/ButtonSectionContainer.js +2 -2
- package/dist/esm/Containers/OutletContainer.js +2 -2
- package/dist/esm/DynamicTable/DynamicPrintTable.js +13 -14
- package/dist/esm/DynamicTable/GenericDynamicTable.js +1 -1
- package/dist/esm/Form/DynamicBaculoForm/index.js +21 -57
- package/dist/esm/Form/FileUpload/index.js +197 -103
- package/dist/esm/Layouts/errorLayout/index.js +3 -5
- package/dist/esm/Modal/ItemModal.js +21 -10
- package/dist/esm/Skeletons/LoadingWindow.js +8 -0
- package/package.json +6 -6
- package/src/BlockUI/BlockUI.tsx +2 -4
- package/src/Buttons/Button.tsx +28 -32
- package/src/Containers/ButtonSectionContainer.tsx +2 -5
- package/src/Containers/OutletContainer.tsx +3 -3
- package/src/DynamicTable/DynamicPrintTable.tsx +1 -4
- package/src/DynamicTable/GenericDynamicTable.tsx +1 -1
- package/src/Form/DynamicBaculoForm/index.tsx +55 -66
- package/src/Form/FileUpload/index.tsx +365 -150
- package/src/Form/TimePicker/index.tsx +0 -1
- package/src/Form/YesNo/index.tsx +0 -1
- package/src/Layouts/errorLayout/index.tsx +4 -11
- package/src/Modal/ItemModal.tsx +44 -13
- package/src/Skeletons/LoadingWindow.tsx +11 -1
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { DownloadIconSVG, IdeaIconSVG } from "@zauru-sdk/icons";
|
|
2
2
|
import React, { useState, useEffect, useRef } from "react";
|
|
3
3
|
import { useFormContext } from "react-hook-form";
|
|
4
|
-
|
|
5
|
-
import imageCompression from "browser-image-compression";
|
|
6
|
-
import type { Options } from "browser-image-compression";
|
|
7
|
-
*/
|
|
4
|
+
//import imageCompression, { Options } from "browser-image-compression";
|
|
8
5
|
|
|
9
6
|
type Props = {
|
|
10
7
|
id?: string;
|
|
11
8
|
name: string;
|
|
12
|
-
formName?: string;
|
|
13
9
|
title?: string;
|
|
14
10
|
helpText?: string;
|
|
15
11
|
hint?: string;
|
|
@@ -23,6 +19,8 @@ type Props = {
|
|
|
23
19
|
required?: boolean;
|
|
24
20
|
optimizeImages?: boolean;
|
|
25
21
|
zauruBaseURL?: string;
|
|
22
|
+
setProcessing?: (processing: boolean) => void;
|
|
23
|
+
signature?: boolean;
|
|
26
24
|
};
|
|
27
25
|
|
|
28
26
|
export const FileUploadField = (props: Props) => {
|
|
@@ -42,6 +40,8 @@ export const FileUploadField = (props: Props) => {
|
|
|
42
40
|
required = false,
|
|
43
41
|
optimizeImages = true,
|
|
44
42
|
zauruBaseURL = "https://zauru.herokuapp.com",
|
|
43
|
+
setProcessing,
|
|
44
|
+
signature = false,
|
|
45
45
|
} = props;
|
|
46
46
|
|
|
47
47
|
const {
|
|
@@ -55,11 +55,77 @@ export const FileUploadField = (props: Props) => {
|
|
|
55
55
|
|
|
56
56
|
const [showTooltip, setShowTooltip] = useState<boolean>(false);
|
|
57
57
|
const [previewSrc, setPreviewSrc] = useState<string | null>(null);
|
|
58
|
-
const [fileDeleted, setFileDeleted] = useState<boolean>(false);
|
|
59
58
|
const [uploading, setUploading] = useState<boolean>(false);
|
|
60
59
|
const [uploadProgress, setUploadProgress] = useState<number>(0);
|
|
60
|
+
const [openSignatureModal, setOpenSignatureModal] = useState<boolean>(false);
|
|
61
|
+
// Estados para la optimización de imagen
|
|
62
|
+
const [optimizing, setOptimizing] = useState<boolean>(false);
|
|
63
|
+
const [optimizationProgress, setOptimizationProgress] = useState<number>(0);
|
|
61
64
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
62
65
|
|
|
66
|
+
// For signature drawing mode
|
|
67
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
68
|
+
const isDrawing = useRef(false);
|
|
69
|
+
const lastX = useRef(0);
|
|
70
|
+
const lastY = useRef(0);
|
|
71
|
+
|
|
72
|
+
const handlePointerDown = (e: React.PointerEvent<HTMLCanvasElement>) => {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
const canvas = canvasRef.current;
|
|
75
|
+
if (!canvas) return;
|
|
76
|
+
const rect = canvas.getBoundingClientRect();
|
|
77
|
+
lastX.current = e.clientX - rect.left;
|
|
78
|
+
lastY.current = e.clientY - rect.top;
|
|
79
|
+
isDrawing.current = true;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const handlePointerMove = (e: React.PointerEvent<HTMLCanvasElement>) => {
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
if (!isDrawing.current) return;
|
|
85
|
+
const canvas = canvasRef.current;
|
|
86
|
+
if (!canvas) return;
|
|
87
|
+
const ctx = canvas.getContext("2d");
|
|
88
|
+
if (!ctx) return;
|
|
89
|
+
const rect = canvas.getBoundingClientRect();
|
|
90
|
+
const currentX = e.clientX - rect.left;
|
|
91
|
+
const currentY = e.clientY - rect.top;
|
|
92
|
+
ctx.beginPath();
|
|
93
|
+
ctx.moveTo(lastX.current, lastY.current);
|
|
94
|
+
ctx.lineTo(currentX, currentY);
|
|
95
|
+
ctx.strokeStyle = "black";
|
|
96
|
+
ctx.lineWidth = 2;
|
|
97
|
+
ctx.stroke();
|
|
98
|
+
lastX.current = currentX;
|
|
99
|
+
lastY.current = currentY;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const handlePointerUp = (e?: React.PointerEvent<HTMLCanvasElement>) => {
|
|
103
|
+
e?.preventDefault();
|
|
104
|
+
isDrawing.current = false;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const clearCanvas = () => {
|
|
108
|
+
const canvas = canvasRef.current;
|
|
109
|
+
if (!canvas) return;
|
|
110
|
+
const ctx = canvas.getContext("2d");
|
|
111
|
+
if (!ctx) return;
|
|
112
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const handleSignatureConfirm = () => {
|
|
116
|
+
const canvas = canvasRef.current;
|
|
117
|
+
if (!canvas) return;
|
|
118
|
+
canvas.toBlob(async (blob) => {
|
|
119
|
+
if (blob) {
|
|
120
|
+
const file = new File([blob], "signature.png", { type: "image/png" });
|
|
121
|
+
const objectUrl = URL.createObjectURL(file);
|
|
122
|
+
setPreviewSrc(objectUrl);
|
|
123
|
+
await processFile(file);
|
|
124
|
+
setOpenSignatureModal(false);
|
|
125
|
+
}
|
|
126
|
+
}, "image/png");
|
|
127
|
+
};
|
|
128
|
+
|
|
63
129
|
useEffect(() => {
|
|
64
130
|
return () => {
|
|
65
131
|
if (previewSrc) {
|
|
@@ -79,117 +145,126 @@ export const FileUploadField = (props: Props) => {
|
|
|
79
145
|
const textColor = isReadOnly ? "text-gray-700" : `text-${color}-900`;
|
|
80
146
|
const borderColor = error ? "border-red-500" : `border-${color}-500`;
|
|
81
147
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
* usando la librería browser-image-compression.
|
|
86
|
-
*/
|
|
87
|
-
const handleInputChange = async (
|
|
88
|
-
event: React.ChangeEvent<HTMLInputElement>
|
|
148
|
+
const processFile = async (
|
|
149
|
+
file: File,
|
|
150
|
+
event?: React.ChangeEvent<HTMLInputElement>
|
|
89
151
|
) => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
console.error("Error al convertir la imagen a WebP:", error);
|
|
120
|
-
const objectUrl = URL.createObjectURL(file);
|
|
121
|
-
setPreviewSrc(objectUrl);
|
|
122
|
-
}
|
|
123
|
-
} else if (file && file.type.startsWith("image/")) {
|
|
152
|
+
let processedFile = file;
|
|
153
|
+
|
|
154
|
+
/*
|
|
155
|
+
if (file && file.type.startsWith("image/") && optimizeImages) {
|
|
156
|
+
try {
|
|
157
|
+
setOptimizing(true);
|
|
158
|
+
setOptimizationProgress(0);
|
|
159
|
+
const options: Options = {
|
|
160
|
+
fileType: "image/webp",
|
|
161
|
+
initialQuality: 0.7,
|
|
162
|
+
maxSizeMB: 1,
|
|
163
|
+
maxWidthOrHeight: 1920,
|
|
164
|
+
useWebWorker: true,
|
|
165
|
+
onProgress: (progress: number) => {
|
|
166
|
+
setOptimizationProgress(progress);
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
const compressedFile = await imageCompression(file, options);
|
|
170
|
+
setOptimizing(false);
|
|
171
|
+
processedFile = new File(
|
|
172
|
+
[compressedFile],
|
|
173
|
+
file.name.replace(/\.[^.]+$/, ".webp"),
|
|
174
|
+
{ type: "image/webp" }
|
|
175
|
+
);
|
|
176
|
+
const objectUrl = URL.createObjectURL(processedFile);
|
|
177
|
+
setPreviewSrc(objectUrl);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error("Error al convertir la imagen a WebP:", error);
|
|
180
|
+
setOptimizing(false);
|
|
124
181
|
const objectUrl = URL.createObjectURL(file);
|
|
125
182
|
setPreviewSrc(objectUrl);
|
|
126
|
-
} else {
|
|
127
|
-
setPreviewSrc(null);
|
|
128
183
|
}
|
|
184
|
+
} else
|
|
185
|
+
*/
|
|
186
|
+
|
|
187
|
+
if (file && file.type.startsWith("image/")) {
|
|
188
|
+
const objectUrl = URL.createObjectURL(file);
|
|
189
|
+
setPreviewSrc(objectUrl);
|
|
190
|
+
} else {
|
|
191
|
+
setPreviewSrc(null);
|
|
192
|
+
}
|
|
129
193
|
|
|
130
|
-
|
|
194
|
+
if (event && event.target) {
|
|
131
195
|
const dataTransfer = new DataTransfer();
|
|
132
|
-
dataTransfer.items.add(
|
|
196
|
+
dataTransfer.items.add(processedFile);
|
|
133
197
|
event.target.files = dataTransfer.files;
|
|
198
|
+
}
|
|
134
199
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const directUpload = new DirectUpload(file, uploadUrl, {
|
|
144
|
-
directUploadWillStoreFileWithXHR: (xhr: XMLHttpRequest) => {
|
|
145
|
-
xhr.upload.addEventListener("progress", (event) => {
|
|
146
|
-
if (event.lengthComputable) {
|
|
147
|
-
const progress = Math.round(
|
|
148
|
-
(event.loaded / event.total) * 100
|
|
149
|
-
);
|
|
150
|
-
setUploadProgress(progress);
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
directUpload.create((error, blob) => {
|
|
157
|
-
setUploading(false);
|
|
158
|
-
if (error) {
|
|
159
|
-
console.error("Error al subir el archivo:", error);
|
|
160
|
-
} else {
|
|
161
|
-
setValue(name, blob.signed_id);
|
|
162
|
-
setValue(`${name}_file_type`, blob.content_type);
|
|
163
|
-
|
|
164
|
-
// Input hidden para el signed_id
|
|
165
|
-
const hiddenField = document.createElement("input");
|
|
166
|
-
hiddenField.setAttribute("type", "hidden");
|
|
167
|
-
hiddenField.setAttribute("value", blob.signed_id);
|
|
168
|
-
hiddenField.setAttribute("name", name);
|
|
169
|
-
// Input hidden para el content_type
|
|
170
|
-
const typeHiddenField = document.createElement("input");
|
|
171
|
-
typeHiddenField.setAttribute("type", "hidden");
|
|
172
|
-
typeHiddenField.setAttribute("value", blob.content_type);
|
|
173
|
-
typeHiddenField.setAttribute("name", `${name}_file_type`);
|
|
174
|
-
|
|
175
|
-
const formElement = document.querySelector("form");
|
|
176
|
-
if (formElement) {
|
|
177
|
-
formElement.appendChild(hiddenField);
|
|
178
|
-
formElement.appendChild(typeHiddenField);
|
|
179
|
-
}
|
|
200
|
+
// Proceso de subida mediante DirectUpload
|
|
201
|
+
import("@rails/activestorage")
|
|
202
|
+
.then(({ DirectUpload }) => {
|
|
203
|
+
const uploadUrl = `${zauruBaseURL}/api/direct_uploads`;
|
|
204
|
+
|
|
205
|
+
setProcessing && setProcessing(true);
|
|
206
|
+
setUploading(true);
|
|
207
|
+
setUploadProgress(0);
|
|
180
208
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
209
|
+
const directUpload = new DirectUpload(processedFile, uploadUrl, {
|
|
210
|
+
directUploadWillStoreFileWithXHR: (xhr: XMLHttpRequest) => {
|
|
211
|
+
xhr.upload.addEventListener("progress", (event) => {
|
|
212
|
+
if (event.lengthComputable) {
|
|
213
|
+
const progress = Math.round((event.loaded / event.total) * 100);
|
|
214
|
+
setUploadProgress(progress);
|
|
184
215
|
}
|
|
216
|
+
});
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
directUpload.create((error, blob) => {
|
|
221
|
+
setUploading(false);
|
|
222
|
+
setProcessing && setProcessing(false);
|
|
223
|
+
if (error) {
|
|
224
|
+
console.error("Error al subir el archivo:", error);
|
|
225
|
+
} else {
|
|
226
|
+
setValue(name, blob.signed_id);
|
|
227
|
+
setValue(`${name}_file_type`, blob.content_type);
|
|
228
|
+
|
|
229
|
+
const hiddenField = document.createElement("input");
|
|
230
|
+
hiddenField.setAttribute("type", "hidden");
|
|
231
|
+
hiddenField.setAttribute("value", blob.signed_id);
|
|
232
|
+
hiddenField.setAttribute("name", name);
|
|
233
|
+
const typeHiddenField = document.createElement("input");
|
|
234
|
+
typeHiddenField.setAttribute("type", "hidden");
|
|
235
|
+
typeHiddenField.setAttribute("value", blob.content_type);
|
|
236
|
+
typeHiddenField.setAttribute("name", `${name}_file_type`);
|
|
237
|
+
|
|
238
|
+
const formElement = document.querySelector("form");
|
|
239
|
+
if (formElement) {
|
|
240
|
+
formElement.appendChild(hiddenField);
|
|
241
|
+
formElement.appendChild(typeHiddenField);
|
|
185
242
|
}
|
|
186
|
-
});
|
|
187
|
-
})
|
|
188
|
-
.catch((err) => console.error("Error al cargar DirectUpload:", err));
|
|
189
|
-
}
|
|
190
243
|
|
|
244
|
+
if (fileInputRef.current) {
|
|
245
|
+
fileInputRef.current.removeAttribute("name");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
})
|
|
250
|
+
.catch((err) => {
|
|
251
|
+
setProcessing && setProcessing(false);
|
|
252
|
+
console.error("Error al cargar DirectUpload:", err);
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Función que se dispara cuando el usuario selecciona un archivo.
|
|
258
|
+
* Si optimizeImages está activo y se trata de una imagen, la convierte a WebP
|
|
259
|
+
* usando la librería browser-image-compression.
|
|
260
|
+
*/
|
|
261
|
+
const handleInputChange = async (
|
|
262
|
+
event: React.ChangeEvent<HTMLInputElement>
|
|
263
|
+
) => {
|
|
264
|
+
if (event.target.files && event.target.files.length > 0) {
|
|
265
|
+
await processFile(event.target.files[0], event);
|
|
266
|
+
}
|
|
191
267
|
onChange && onChange(event);
|
|
192
|
-
setFileDeleted(false);
|
|
193
268
|
};
|
|
194
269
|
|
|
195
270
|
/**
|
|
@@ -198,7 +273,6 @@ export const FileUploadField = (props: Props) => {
|
|
|
198
273
|
*/
|
|
199
274
|
const deleteFile = () => {
|
|
200
275
|
setPreviewSrc(null);
|
|
201
|
-
setFileDeleted(true);
|
|
202
276
|
if (fileInputRef.current) {
|
|
203
277
|
fileInputRef.current.value = "";
|
|
204
278
|
}
|
|
@@ -255,6 +329,117 @@ export const FileUploadField = (props: Props) => {
|
|
|
255
329
|
* - Si defaultValue es string, se muestra el preview (descarga/imagen).
|
|
256
330
|
* - Si no hay defaultValue (o es File), se muestra "Sin archivo".
|
|
257
331
|
*/
|
|
332
|
+
if (signature) {
|
|
333
|
+
if (readOnly) {
|
|
334
|
+
return (
|
|
335
|
+
<div className={`col-span-6 sm:col-span-3 ${className}`}>
|
|
336
|
+
{title && (
|
|
337
|
+
<label
|
|
338
|
+
htmlFor={name}
|
|
339
|
+
className="block mb-1 text-sm font-medium text-gray-700"
|
|
340
|
+
>
|
|
341
|
+
{title}
|
|
342
|
+
</label>
|
|
343
|
+
)}
|
|
344
|
+
{typeof defaultValue === "string" && defaultValue ? (
|
|
345
|
+
renderPreview(defaultValue)
|
|
346
|
+
) : (
|
|
347
|
+
<div className="text-sm italic text-gray-400">
|
|
348
|
+
No hay firma disponible
|
|
349
|
+
</div>
|
|
350
|
+
)}
|
|
351
|
+
</div>
|
|
352
|
+
);
|
|
353
|
+
} else {
|
|
354
|
+
return (
|
|
355
|
+
<div className={`col-span-6 sm:col-span-3 ${className}`}>
|
|
356
|
+
{title && (
|
|
357
|
+
<label
|
|
358
|
+
htmlFor={name}
|
|
359
|
+
className={`block mb-1 text-sm font-medium text-${color}-700`}
|
|
360
|
+
>
|
|
361
|
+
{title}
|
|
362
|
+
{required && <span className="text-red-500 ml-1">*</span>}
|
|
363
|
+
</label>
|
|
364
|
+
)}
|
|
365
|
+
{previewSrc ? (
|
|
366
|
+
<div>
|
|
367
|
+
{renderPreview(previewSrc)}
|
|
368
|
+
<button
|
|
369
|
+
type="button"
|
|
370
|
+
onClick={() => {
|
|
371
|
+
setPreviewSrc(null);
|
|
372
|
+
}}
|
|
373
|
+
className="ml-2 text-red-600 underline text-sm"
|
|
374
|
+
>
|
|
375
|
+
Eliminar firma
|
|
376
|
+
</button>
|
|
377
|
+
</div>
|
|
378
|
+
) : (
|
|
379
|
+
<div>
|
|
380
|
+
<button
|
|
381
|
+
type="button"
|
|
382
|
+
onClick={() => setOpenSignatureModal(true)}
|
|
383
|
+
className="px-4 py-2 bg-blue-600 text-white rounded"
|
|
384
|
+
>
|
|
385
|
+
Firmar
|
|
386
|
+
</button>
|
|
387
|
+
</div>
|
|
388
|
+
)}
|
|
389
|
+
{openSignatureModal && (
|
|
390
|
+
<div className="fixed inset-0 z-50 flex flex-col bg-white">
|
|
391
|
+
<div className="flex-grow flex items-center justify-center">
|
|
392
|
+
<canvas
|
|
393
|
+
ref={canvasRef}
|
|
394
|
+
width={400}
|
|
395
|
+
height={200}
|
|
396
|
+
style={{
|
|
397
|
+
border: "1px solid #ccc",
|
|
398
|
+
backgroundColor: "transparent",
|
|
399
|
+
touchAction: "none",
|
|
400
|
+
}}
|
|
401
|
+
onPointerDown={handlePointerDown}
|
|
402
|
+
onPointerMove={handlePointerMove}
|
|
403
|
+
onPointerUp={handlePointerUp}
|
|
404
|
+
onPointerLeave={handlePointerUp}
|
|
405
|
+
></canvas>
|
|
406
|
+
</div>
|
|
407
|
+
<div className="p-4 flex justify-center space-x-4">
|
|
408
|
+
<button
|
|
409
|
+
type="button"
|
|
410
|
+
onClick={clearCanvas}
|
|
411
|
+
className="text-blue-600 underline text-sm"
|
|
412
|
+
>
|
|
413
|
+
Reset
|
|
414
|
+
</button>
|
|
415
|
+
<button
|
|
416
|
+
type="button"
|
|
417
|
+
onClick={handleSignatureConfirm}
|
|
418
|
+
className="text-blue-600 underline text-sm"
|
|
419
|
+
>
|
|
420
|
+
Ok
|
|
421
|
+
</button>
|
|
422
|
+
<button
|
|
423
|
+
type="button"
|
|
424
|
+
onClick={() => setOpenSignatureModal(false)}
|
|
425
|
+
className="text-gray-600 underline text-sm"
|
|
426
|
+
>
|
|
427
|
+
Cancelar
|
|
428
|
+
</button>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
)}
|
|
432
|
+
</div>
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* 2) Modo editable (readOnly = false):
|
|
439
|
+
* - Se muestra siempre el recuadro de vista previa, ya sea con la imagen cargada o con un placeholder.
|
|
440
|
+
* - Si se ha seleccionado una imagen o existe defaultValue y no se ha eliminado,
|
|
441
|
+
* se muestra la imagen y un botón para eliminar el archivo.
|
|
442
|
+
*/
|
|
258
443
|
if (readOnly) {
|
|
259
444
|
return (
|
|
260
445
|
<div className={`col-span-6 sm:col-span-3 ${className}`}>
|
|
@@ -277,12 +462,6 @@ export const FileUploadField = (props: Props) => {
|
|
|
277
462
|
);
|
|
278
463
|
}
|
|
279
464
|
|
|
280
|
-
/**
|
|
281
|
-
* 2) Modo editable (readOnly = false):
|
|
282
|
-
* - Si se ha seleccionado una imagen o existe defaultValue y no se ha eliminado,
|
|
283
|
-
* se muestra la vista previa generada junto con un botón para eliminar el archivo.
|
|
284
|
-
* - Si se elimina el archivo o no hay ninguno, se muestra el input para cargar uno.
|
|
285
|
-
*/
|
|
286
465
|
return (
|
|
287
466
|
<div className={`col-span-6 sm:col-span-3 ${className}`}>
|
|
288
467
|
{title && (
|
|
@@ -295,33 +474,49 @@ export const FileUploadField = (props: Props) => {
|
|
|
295
474
|
</label>
|
|
296
475
|
)}
|
|
297
476
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
477
|
+
<div className="mb-2 flex items-center">
|
|
478
|
+
<div
|
|
479
|
+
role="button"
|
|
480
|
+
tabIndex={0}
|
|
481
|
+
onClick={() => {
|
|
482
|
+
const srcToOpen =
|
|
483
|
+
previewSrc || (typeof defaultValue === "string" && defaultValue);
|
|
484
|
+
if (srcToOpen) window.open(srcToOpen, "_blank");
|
|
485
|
+
}}
|
|
486
|
+
onKeyDown={(event) => {
|
|
487
|
+
const srcToOpen =
|
|
488
|
+
previewSrc || (typeof defaultValue === "string" && defaultValue);
|
|
489
|
+
if (event.key === "Enter" && srcToOpen)
|
|
490
|
+
window.open(srcToOpen, "_blank");
|
|
491
|
+
}}
|
|
492
|
+
className="cursor-pointer border border-gray-300 h-48 w-48 flex items-center justify-center"
|
|
493
|
+
>
|
|
494
|
+
{previewSrc || (typeof defaultValue === "string" && defaultValue) ? (
|
|
495
|
+
<img
|
|
496
|
+
src={
|
|
497
|
+
previewSrc ??
|
|
498
|
+
(typeof defaultValue === "string" && defaultValue
|
|
499
|
+
? defaultValue
|
|
500
|
+
: undefined)
|
|
501
|
+
}
|
|
502
|
+
alt={name}
|
|
503
|
+
className="h-48 w-48"
|
|
504
|
+
style={{ objectFit: "contain" }}
|
|
505
|
+
/>
|
|
506
|
+
) : (
|
|
507
|
+
<span className="text-gray-400">No image</span>
|
|
508
|
+
)}
|
|
509
|
+
</div>
|
|
510
|
+
{(previewSrc || (typeof defaultValue === "string" && defaultValue)) && (
|
|
511
|
+
<button
|
|
512
|
+
type="button"
|
|
513
|
+
onClick={deleteFile}
|
|
514
|
+
className="ml-2 text-red-600 underline text-sm"
|
|
515
|
+
>
|
|
516
|
+
Eliminar archivo
|
|
517
|
+
</button>
|
|
518
|
+
)}
|
|
519
|
+
</div>
|
|
325
520
|
|
|
326
521
|
<div className="flex relative items-center">
|
|
327
522
|
<input
|
|
@@ -353,18 +548,38 @@ export const FileUploadField = (props: Props) => {
|
|
|
353
548
|
)}
|
|
354
549
|
</div>
|
|
355
550
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
551
|
+
<div className="mt-2 min-h-[60px] flex items-center justify-center border border-dashed border-gray-200 rounded">
|
|
552
|
+
{optimizing || uploading ? (
|
|
553
|
+
<>
|
|
554
|
+
{optimizing && (
|
|
555
|
+
<div className="w-full">
|
|
556
|
+
<progress
|
|
557
|
+
value={optimizationProgress}
|
|
558
|
+
max="100"
|
|
559
|
+
className="w-full"
|
|
560
|
+
></progress>
|
|
561
|
+
<p className="text-sm text-blue-600 mt-1 text-center">
|
|
562
|
+
Optimizando imagen a webp: {optimizationProgress}%
|
|
563
|
+
</p>
|
|
564
|
+
</div>
|
|
565
|
+
)}
|
|
566
|
+
{uploading && (
|
|
567
|
+
<div className="w-full">
|
|
568
|
+
<progress
|
|
569
|
+
value={uploadProgress}
|
|
570
|
+
max="100"
|
|
571
|
+
className="w-full"
|
|
572
|
+
></progress>
|
|
573
|
+
<p className="text-sm text-blue-600 mt-1 text-center">
|
|
574
|
+
Subiendo archivo: {uploadProgress}%
|
|
575
|
+
</p>
|
|
576
|
+
</div>
|
|
577
|
+
)}
|
|
578
|
+
</>
|
|
579
|
+
) : (
|
|
580
|
+
<span className="text-sm text-gray-500">Información de progreso</span>
|
|
581
|
+
)}
|
|
582
|
+
</div>
|
|
368
583
|
|
|
369
584
|
{error && (
|
|
370
585
|
<p className="mt-2 text-sm text-red-600">
|
package/src/Form/YesNo/index.tsx
CHANGED
|
@@ -28,21 +28,14 @@ export const ErrorLayout = ({
|
|
|
28
28
|
<div className="w-full max-w-2xl flex flex-col items-center">
|
|
29
29
|
<p className="text-2xl text-gray-300 mb-8 text-center">
|
|
30
30
|
{isRouteErrorResponse(error)
|
|
31
|
-
? `Error
|
|
31
|
+
? `Error ${error.status}: ${error.statusText}`
|
|
32
32
|
: error instanceof Error
|
|
33
|
-
?
|
|
34
|
-
|
|
35
|
-
} - STACK: ${error.stack?.slice(0, 100)} - CAUSE: ${
|
|
36
|
-
error.cause
|
|
37
|
-
}`
|
|
38
|
-
: `Ha ocurrido un error inesperado: ${error}`}
|
|
33
|
+
? error.message
|
|
34
|
+
: "Ha ocurrido un error inesperado"}
|
|
39
35
|
</p>
|
|
40
36
|
{from && (
|
|
41
37
|
<p className="text-lg text-gray-400 mb-4 text-center">
|
|
42
|
-
{
|
|
43
|
-
? "(*) Error lanzado desde: "
|
|
44
|
-
: "Error lanzado desde: "}
|
|
45
|
-
{from}
|
|
38
|
+
Error lanzado desde: {from}
|
|
46
39
|
</p>
|
|
47
40
|
)}
|
|
48
41
|
{parentError && (
|