From 4804426120975ecdffe067d20f05568144435e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Tue, 26 May 2026 23:11:37 -0300 Subject: [PATCH 1/4] fix: add image preview and download link for upload input v3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- .../inputs/upload-input-v3/index.js | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/components/inputs/upload-input-v3/index.js b/src/components/inputs/upload-input-v3/index.js index d90e6a75..861d12b7 100644 --- a/src/components/inputs/upload-input-v3/index.js +++ b/src/components/inputs/upload-input-v3/index.js @@ -25,6 +25,7 @@ import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; import CloseIcon from "@mui/icons-material/Close"; import { DropzoneV3 } from './dropzone-v3'; +import file_icon from '../upload-input/file.png'; import './index.less'; const UploadInputV3 = ({ @@ -371,21 +372,37 @@ const UploadInputV3 = ({ {value.map((file, index) => { const filename = file.filename; const fileSize = formatFileSize(file.size); + let src = file?.private_url || file?.public_url; + if (src === '#') src = file?.public_url; + // custom replace for dropbox case ( download vs raw) + const previewSrc = src ? src.replace("?dl=0", "?raw=1") : filename; return ( - - + + + {filename} { e.target.src = file_icon; }} + style={{ width: 70, height: 70, objectFit: 'contain', display: 'block', borderRadius: 4 }} + /> + {filename} From 4eb649ec22c6d7e36054aaf1ec4c79240c65c7b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Tue, 26 May 2026 23:19:24 -0300 Subject: [PATCH 2/4] fix: add unit test cases for preview image and download link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- .../__tests__/upload-input-v3.test.js | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js b/src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js index 78e26f44..5e061057 100644 --- a/src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js +++ b/src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js @@ -288,6 +288,52 @@ describe('UploadInputV3', () => { }); }); + describe('File Preview and Download', () => { + test('renders a preview image linked to the file', () => { + const files = [{ filename: 'image.png', size: 102400, public_url: 'https://cdn.example.com/image.png' }]; + render(); + const img = screen.getByRole('img', { name: 'image.png' }); + expect(img).toHaveAttribute('src', 'https://cdn.example.com/image.png'); + const previewLink = img.closest('a'); + expect(previewLink).toHaveAttribute('href', 'https://cdn.example.com/image.png'); + expect(previewLink).toHaveAttribute('target', '_blank'); + expect(previewLink).not.toHaveAttribute('download'); + }); + + test('prefers private_url and falls back to public_url when private_url is "#"', () => { + const { rerender } = render(); + expect(screen.getByRole('img', { name: 'a.png' })).toHaveAttribute('src', 'https://private.example.com/a.png'); + + rerender(); + expect(screen.getByRole('img', { name: 'a.png' })).toHaveAttribute('src', 'https://cdn.example.com/a.png'); + }); + + test('filename is a download link with correct href', () => { + const files = [{ filename: 'document.pdf', size: 102400, public_url: 'https://cdn.example.com/document.pdf' }]; + const { container } = render(); + const downloadLink = container.querySelector('a[download]'); + expect(downloadLink).toHaveAttribute('href', 'https://cdn.example.com/document.pdf'); + expect(downloadLink).toHaveAttribute('target', '_blank'); + expect(downloadLink).toHaveTextContent('document.pdf'); + }); + + test('preview image falls back to file_icon on load error', () => { + const files = [{ filename: 'document.pdf', size: 102400, public_url: 'https://cdn.example.com/document.pdf' }]; + render(); + const img = screen.getByRole('img', { name: 'document.pdf' }); + fireEvent.error(img); + expect(img).not.toHaveAttribute('src', 'https://cdn.example.com/document.pdf'); + }); + }); + describe('Edge Cases', () => { test('handles empty value array', () => { const { container } = render(); From 43d5e68cd943fb4ad99607d5ae8c15fbd5b0ec02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Tue, 26 May 2026 23:37:05 -0300 Subject: [PATCH 3/4] fix: adjust route for fileMock at jest config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ca3a5580..126127a6 100644 --- a/package.json +++ b/package.json @@ -169,7 +169,7 @@ "src/**/*.{js,jsx,mjs}" ], "moduleNameMapper": { - "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/src/__mocks__/fileMock.js", "\\.(css|scss)$": "identity-obj-proxy" }, "testMatch": [ From 427d0b8e4ef5de0fb81d98951ff082ee5a103fea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Wed, 27 May 2026 11:48:35 -0300 Subject: [PATCH 4/4] fix: change preview component, fix src, adjust tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- .../__tests__/upload-input-v3.test.js | 7 +++---- src/components/inputs/upload-input-v3/index.js | 15 ++++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js b/src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js index 5e061057..e7a940ae 100644 --- a/src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js +++ b/src/components/inputs/upload-input-v3/__tests__/upload-input-v3.test.js @@ -293,7 +293,6 @@ describe('UploadInputV3', () => { const files = [{ filename: 'image.png', size: 102400, public_url: 'https://cdn.example.com/image.png' }]; render(); const img = screen.getByRole('img', { name: 'image.png' }); - expect(img).toHaveAttribute('src', 'https://cdn.example.com/image.png'); const previewLink = img.closest('a'); expect(previewLink).toHaveAttribute('href', 'https://cdn.example.com/image.png'); expect(previewLink).toHaveAttribute('target', '_blank'); @@ -301,19 +300,19 @@ describe('UploadInputV3', () => { }); test('prefers private_url and falls back to public_url when private_url is "#"', () => { - const { rerender } = render(); - expect(screen.getByRole('img', { name: 'a.png' })).toHaveAttribute('src', 'https://private.example.com/a.png'); + expect(container.querySelector('a[download]')).toHaveAttribute('href', 'https://private.example.com/a.png'); rerender(); - expect(screen.getByRole('img', { name: 'a.png' })).toHaveAttribute('src', 'https://cdn.example.com/a.png'); + expect(container.querySelector('a[download]')).toHaveAttribute('href', 'https://cdn.example.com/a.png'); }); test('filename is a download link with correct href', () => { diff --git a/src/components/inputs/upload-input-v3/index.js b/src/components/inputs/upload-input-v3/index.js index 861d12b7..7d95d79d 100644 --- a/src/components/inputs/upload-input-v3/index.js +++ b/src/components/inputs/upload-input-v3/index.js @@ -25,6 +25,7 @@ import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; import CloseIcon from "@mui/icons-material/Close"; import { DropzoneV3 } from './dropzone-v3'; +import ProgressiveImg from '../../progressive-img'; import file_icon from '../upload-input/file.png'; import './index.less'; @@ -42,7 +43,7 @@ const UploadInputV3 = ({ id, parallelChunkUploads = false, maxConcurrentChunks = 6, - onError = () => {}, + onError = () => { }, getAllowedExtensions = null, getMaxSize = null, error, @@ -372,7 +373,7 @@ const UploadInputV3 = ({ {value.map((file, index) => { const filename = file.filename; const fileSize = formatFileSize(file.size); - let src = file?.private_url || file?.public_url; + let src = file?.private_url || file?.public_url || file?.file_url; if (src === '#') src = file?.public_url; // custom replace for dropbox case ( download vs raw) const previewSrc = src ? src.replace("?dl=0", "?raw=1") : filename; @@ -383,12 +384,11 @@ const UploadInputV3 = ({ sx={fileRowSx} > - - + { e.target.src = file_icon; }} - style={{ width: 70, height: 70, objectFit: 'contain', display: 'block', borderRadius: 4 }} + src={previewSrc} + placeholderSrc={file_icon} /> @@ -399,6 +399,7 @@ const UploadInputV3 = ({ href={src} target="_blank" rel="noreferrer" + title="Preview file" download variant="body2" fontWeight={500}