From 7def746a875c982871e399615193464e4b1f1b77 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 2 Jul 2026 10:57:53 -0400 Subject: [PATCH 1/6] feat: file download count --- public/locales/en/file.json | 15 +++++ public/locales/es/file.json | 15 +++++ .../domain/repositories/FileRepository.ts | 1 + .../domain/useCases/getFileDownloadCount.ts | 10 +++ .../FileJSDataverseRepository.ts | 9 +++ src/sections/file/File.tsx | 2 + .../file/file-metrics/FileMetrics.module.scss | 28 ++++++++ .../file/file-metrics/FileMetrics.tsx | 66 +++++++++++++++++++ .../file-metrics/useGetFileDownloadCount.ts | 49 ++++++++++++++ src/stories/file/FileMockRepository.ts | 8 +++ tests/component/sections/file/File.spec.tsx | 9 ++- .../e2e/sections/file/File.spec.tsx | 47 +++++++++++++ .../files/FileJSDataverseRepository.spec.ts | 25 +++++++ 13 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 src/files/domain/useCases/getFileDownloadCount.ts create mode 100644 src/sections/file/file-metrics/FileMetrics.module.scss create mode 100644 src/sections/file/file-metrics/FileMetrics.tsx create mode 100644 src/sections/file/file-metrics/useGetFileDownloadCount.ts diff --git a/public/locales/en/file.json b/public/locales/en/file.json index 2b166abee..37d1de8b1 100644 --- a/public/locales/en/file.json +++ b/public/locales/en/file.json @@ -32,6 +32,21 @@ "subtext": "This file is part of \"{{datasetTitle}}\".", "datasetCitationTitle": "Dataset Citation", "fileCitationTitle": "File Citation", + "metrics": { + "title": "File Metrics", + "tip": { + "default": "Metrics for this individual file." + }, + "downloads": { + "count": { + "default_zero": "{{formattedCount}} Downloads", + "default_one": "{{formattedCount}} Download", + "default_other": "{{formattedCount}} Downloads" + }, + "defaultTip": "Total downloads for this file." + }, + "defaultGetDownloadCountError": "Something went wrong while getting the file download count. Try again later." + }, "fileAccess": { "title": "File Access", "restricted": { diff --git a/public/locales/es/file.json b/public/locales/es/file.json index 6fb27e80d..0a609872a 100644 --- a/public/locales/es/file.json +++ b/public/locales/es/file.json @@ -32,6 +32,21 @@ "subtext": "Este fichero es parte de \"{{datasetTitle}}\".", "datasetCitationTitle": "Cita del dataset", "fileCitationTitle": "Cita del archivo", + "metrics": { + "title": "Métricas del archivo", + "tip": { + "default": "Métricas de este archivo." + }, + "downloads": { + "count": { + "default_zero": "{{formattedCount}} Descargas", + "default_one": "{{formattedCount}} Descarga", + "default_other": "{{formattedCount}} Descargas" + }, + "defaultTip": "Descargas totales de este archivo." + }, + "defaultGetDownloadCountError": "Algo salió mal al obtener el recuento de descargas del archivo. Intenta nuevamente más tarde." + }, "fileAccess": { "title": "Acceso al archivo", "restricted": { diff --git a/src/files/domain/repositories/FileRepository.ts b/src/files/domain/repositories/FileRepository.ts index e5761c21e..0e8ec474b 100644 --- a/src/files/domain/repositories/FileRepository.ts +++ b/src/files/domain/repositories/FileRepository.ts @@ -33,6 +33,7 @@ export interface FileRepository { criteria?: FileCriteria, includeDeaccessioned?: boolean ) => Promise + getFileDownloadCount: (fileId: number | string) => Promise getFileVersionSummaries: ( fileId: number | string, paginationInfo?: FileVersionPaginationInfo diff --git a/src/files/domain/useCases/getFileDownloadCount.ts b/src/files/domain/useCases/getFileDownloadCount.ts new file mode 100644 index 000000000..563f5e570 --- /dev/null +++ b/src/files/domain/useCases/getFileDownloadCount.ts @@ -0,0 +1,10 @@ +import { FileRepository } from '../repositories/FileRepository' + +export async function getFileDownloadCount( + fileRepository: FileRepository, + fileId: number | string +): Promise { + return fileRepository.getFileDownloadCount(fileId).catch((error: Error) => { + throw new Error(error.message) + }) +} diff --git a/src/files/infrastructure/FileJSDataverseRepository.ts b/src/files/infrastructure/FileJSDataverseRepository.ts index d37d012db..f60c9c12d 100644 --- a/src/files/infrastructure/FileJSDataverseRepository.ts +++ b/src/files/infrastructure/FileJSDataverseRepository.ts @@ -202,6 +202,15 @@ export class FileJSDataverseRepository implements FileRepository { : Promise.resolve(0) } + getFileDownloadCount(fileId: number | string): Promise { + return getFileDownloadCount + .execute(fileId) + .then((downloadCount) => Number(downloadCount)) + .catch((error: ReadError) => { + throw new Error(error.message) + }) + } + private static getAllThumbnails(jsFiles: JSFile[]): Promise<(string | undefined)[]> { return Promise.all(jsFiles.map((jsFile) => this.getThumbnailById(jsFile.id))) } diff --git a/src/sections/file/File.tsx b/src/sections/file/File.tsx index a19dfcc80..deaa29a04 100644 --- a/src/sections/file/File.tsx +++ b/src/sections/file/File.tsx @@ -26,6 +26,7 @@ import { DataverseInfoRepository } from '@/info/domain/repositories/DataverseInf import { ContactRepository } from '@/contact/domain/repositories/ContactRepository' import { ContactButton } from '../shared/contact/ContactButton' import { ShareFileButton } from './share-file-button/ShareFileButton' +import { FileMetrics } from './file-metrics/FileMetrics' interface FileProps { id: number @@ -187,6 +188,7 @@ export function File({ {!isDeaccessioned && } + diff --git a/src/sections/file/file-metrics/FileMetrics.module.scss b/src/sections/file/file-metrics/FileMetrics.module.scss new file mode 100644 index 000000000..d9f3b0edb --- /dev/null +++ b/src/sections/file/file-metrics/FileMetrics.module.scss @@ -0,0 +1,28 @@ +@use 'sass:color'; +@import 'node_modules/@iqss/dataverse-design-system/src/lib/assets/styles/design-tokens/colors.module'; + +.file-metrics { + margin-bottom: 0.5rem; + border: solid 1px $dv-border-color; + border-radius: 4px; + + .title { + display: flex; + flex-direction: column; + padding: 0.25rem 0.5rem; + background: color.adjust($dv-secondary-color, $alpha: -0.7); + border-bottom: solid 1px $dv-border-color; + + span { + font-weight: 500; + } + } + + .results { + display: flex; + flex-direction: column; + gap: 0.25rem; + padding: 0.25rem 0.5rem; + padding-left: 1rem; + } +} diff --git a/src/sections/file/file-metrics/FileMetrics.tsx b/src/sections/file/file-metrics/FileMetrics.tsx new file mode 100644 index 000000000..4b1de04fc --- /dev/null +++ b/src/sections/file/file-metrics/FileMetrics.tsx @@ -0,0 +1,66 @@ +import { useTranslation } from 'react-i18next' +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton' +import { QuestionMarkTooltip } from '@iqss/dataverse-design-system' +import { FileRepository } from '@/files/domain/repositories/FileRepository' +import { useGetFileDownloadCount } from './useGetFileDownloadCount' +import styles from './FileMetrics.module.scss' + +interface FileMetricsProps { + fileRepository: FileRepository + fileId: number | string +} + +export const FileMetrics = ({ fileRepository, fileId }: FileMetricsProps) => { + const { t, i18n } = useTranslation('file') + const { downloadCount, isLoadingDownloadCount, errorLoadingDownloadCount } = + useGetFileDownloadCount({ + fileRepository, + fileId + }) + + const count = downloadCount ?? 0 + const formatNumber = (value: number) => + new Intl.NumberFormat(i18n.languages[0] || undefined).format(value) + + if (isLoadingDownloadCount) { + return + } + + if (errorLoadingDownloadCount) { + return null + } + + return ( +
+
+ + {t('metrics.title')}{' '} + + +
+ +
+ + {t('metrics.downloads.count.default', { + count, + formattedCount: formatNumber(count) + })}{' '} + + +
+
+ ) +} + +const FileMetricsSkeleton = () => ( + +
+
+ +
+
+ +
+
+
+) diff --git a/src/sections/file/file-metrics/useGetFileDownloadCount.ts b/src/sections/file/file-metrics/useGetFileDownloadCount.ts new file mode 100644 index 000000000..994f0bf65 --- /dev/null +++ b/src/sections/file/file-metrics/useGetFileDownloadCount.ts @@ -0,0 +1,49 @@ +import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { FileRepository } from '@/files/domain/repositories/FileRepository' +import { getFileDownloadCount } from '@/files/domain/useCases/getFileDownloadCount' + +interface UseGetFileDownloadCountProps { + fileRepository: FileRepository + fileId: number | string +} + +interface UseGetFileDownloadCountReturn { + downloadCount: number | null + isLoadingDownloadCount: boolean + errorLoadingDownloadCount: string | null +} + +export const useGetFileDownloadCount = ({ + fileRepository, + fileId +}: UseGetFileDownloadCountProps): UseGetFileDownloadCountReturn => { + const { t } = useTranslation('file') + const [downloadCount, setDownloadCount] = useState(null) + const [isLoadingDownloadCount, setIsLoadingDownloadCount] = useState(true) + const [errorLoadingDownloadCount, setErrorLoadingDownloadCount] = useState(null) + + useEffect(() => { + const handleGetDownloadCount = async () => { + setIsLoadingDownloadCount(true) + + try { + const res = await getFileDownloadCount(fileRepository, fileId) + + setDownloadCount(res) + } catch { + setErrorLoadingDownloadCount(t('metrics.defaultGetDownloadCountError')) + } finally { + setIsLoadingDownloadCount(false) + } + } + + void handleGetDownloadCount() + }, [fileRepository, fileId, t]) + + return { + downloadCount, + isLoadingDownloadCount, + errorLoadingDownloadCount + } +} diff --git a/src/stories/file/FileMockRepository.ts b/src/stories/file/FileMockRepository.ts index e4987b47a..3296088ae 100644 --- a/src/stories/file/FileMockRepository.ts +++ b/src/stories/file/FileMockRepository.ts @@ -89,6 +89,14 @@ export class FileMockRepository implements FileRepository { return FileMetadataMother.createDownloadUrl() } + getFileDownloadCount(_fileId: number | string): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve(0) + }, FakerHelper.loadingTimout()) + }) + } + uploadFile( _datasetId: number | string, _file: FileHolder, diff --git a/tests/component/sections/file/File.spec.tsx b/tests/component/sections/file/File.spec.tsx index 1f9cf81bd..5eb789657 100644 --- a/tests/component/sections/file/File.spec.tsx +++ b/tests/component/sections/file/File.spec.tsx @@ -14,7 +14,11 @@ import { WithRepositories } from '@tests/component/WithRepositories' const fileRepository: FileRepository = {} as FileRepository describe('File', () => { - it('renders the File page title and details', () => { + beforeEach(() => { + fileRepository.getFileDownloadCount = cy.stub().resolves(8) + }) + + it('renders the File page title, details and metrics', () => { const testFile = FileMother.createRealistic() fileRepository.getById = cy.stub().resolves(testFile) @@ -47,6 +51,9 @@ describe('File', () => { cy.findByRole('tab', { name: 'Versions' }).should('exist') cy.findByRole('button', { name: 'File Metadata' }).should('exist') cy.findByRole('group', { name: 'File Action Buttons' }).should('exist') + cy.findByText('File Metrics').should('exist') + cy.findByTestId('file-download-count').should('contain.text', '8 Downloads') + cy.wrap(fileRepository.getFileDownloadCount).should('be.calledWith', testFile.id) }) it('renders skeleton while loading', () => { diff --git a/tests/e2e-integration/e2e/sections/file/File.spec.tsx b/tests/e2e-integration/e2e/sections/file/File.spec.tsx index 1c965c7a4..9252d2e9e 100644 --- a/tests/e2e-integration/e2e/sections/file/File.spec.tsx +++ b/tests/e2e-integration/e2e/sections/file/File.spec.tsx @@ -60,6 +60,53 @@ describe('File', () => { ) }) + it('shows the file metrics download count on the file page', () => { + cy.wrap(DatasetHelper.createWithFileAndPublish(FileHelper.create()), { timeout: 6000 }).then( + (datasetResponse) => { + if (!datasetResponse.file) { + throw new Error('Expected created dataset to include a file') + } + + cy.visit(`${FRONTEND_BASE_PATH}/files?id=${datasetResponse.file.id}`) + + cy.findByText('File Metrics').should('exist') + cy.findByTestId('file-download-count').should('contain.text', '0 Downloads') + } + ) + }) + + it('updates the file metrics download count after downloading the file', () => { + cy.wrap(DatasetHelper.createWithFileAndPublish(FileHelper.create()), { timeout: 6000 }).then( + (datasetResponse) => { + if (!datasetResponse.file) { + throw new Error('Expected created dataset to include a file') + } + + const fileId = datasetResponse.file.id + + cy.visit(`${FRONTEND_BASE_PATH}/files?id=${fileId}`) + cy.findByTestId('file-download-count').should('contain.text', '0 Downloads') + + cy.window().then((window) => { + cy.stub(window.HTMLAnchorElement.prototype, 'click').as('anchorClick') + }) + + cy.findByRole('button', { name: 'Access File' }).as('accessButton') + cy.get('@accessButton').should('be.visible') + cy.wait(500) + cy.get('@accessButton').click() + cy.findByTestId('download-original-file').should('exist').click({ force: true }) + + cy.get('@anchorClick').should('have.been.calledOnce') + cy.findByText('Your download has started.').should('exist') + + cy.wait(5000) + cy.visit(`${FRONTEND_BASE_PATH}/files?id=${fileId}`) + cy.findByTestId('file-download-count').should('contain.text', '1 Download') + } + ) + }) + it('loads version summaries when clicking on the version tab', () => { cy.wrap( DatasetHelper.createWithFileAndPublish(FileHelper.create()).then( diff --git a/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts index bf0f418a7..d34db8218 100644 --- a/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts @@ -35,6 +35,7 @@ import { File } from '../../../../src/files/domain/models/File' import { FileIngest, FileIngestStatus } from '../../../../src/files/domain/models/FileIngest' import { DateHelper } from '@/shared/helpers/DateHelper' import { FileMetadataDTO } from '@/files/domain/useCases/DTOs/FileMetadataDTO' +import { getFileDownloadCount } from '@/files/domain/useCases/getFileDownloadCount' const DRAFT_PARAM = DatasetNonNumericVersion.DRAFT @@ -932,6 +933,30 @@ describe('File JSDataverse Repository', () => { }) }) }) + + describe('getFileDownloadCount', () => { + it('gets zero downloads for a published file with no downloads by database id', async () => { + const datasetResponse = await DatasetHelper.createWithFileAndPublish(FileHelper.create()) + if (!datasetResponse.file) throw new Error('File not found') + + const downloadCount = await getFileDownloadCount(fileRepository, datasetResponse.file.id) + + expect(downloadCount).to.deep.equal(0) + }) + + it('gets the download count by database id after the file has been downloaded', async () => { + const datasetResponse = await DatasetHelper.createWithFileAndPublish(FileHelper.create()) + if (!datasetResponse.file) throw new Error('File not found') + + await FileHelper.download(datasetResponse.file.id) + await TestsUtils.wait(5000) // Wait for the file download count to be updated + + const downloadCountById = await getFileDownloadCount(fileRepository, datasetResponse.file.id) + + expect(downloadCountById).to.deep.equal(1) + }) + }) + describe('edit file metadata', () => { it('edits file metadata', async () => { const datasetResponse = await DatasetHelper.createWithFile(FileHelper.create()) From a31156888c664d59e7b7615e76bd028c5a8370d6 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 2 Jul 2026 13:17:40 -0400 Subject: [PATCH 2/6] feat: update changelog --- CHANGELOG.md | 3 +- .../useGetFileDownloadCount.spec.tsx | 73 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 tests/component/sections/file/file-metrics/useGetFileDownloadCount.spec.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 46e13f875..e9e26b06e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel ### Added - Dataset Templates UI integration, including create/edit flows, previews, and skeleton states. -- Dataset Page: added a sidebar to show dataset reviews +- Dataset Page: added a sidebar to show dataset reviews. +- File Page: added a File Metrics: File Download Count. ### Changed diff --git a/tests/component/sections/file/file-metrics/useGetFileDownloadCount.spec.tsx b/tests/component/sections/file/file-metrics/useGetFileDownloadCount.spec.tsx new file mode 100644 index 000000000..61f814d15 --- /dev/null +++ b/tests/component/sections/file/file-metrics/useGetFileDownloadCount.spec.tsx @@ -0,0 +1,73 @@ +import { FileRepository } from '@/files/domain/repositories/FileRepository' +import { useGetFileDownloadCount } from '@/sections/file/file-metrics/useGetFileDownloadCount' +import { renderHook } from '@testing-library/react' + +const fileRepository: FileRepository = {} as FileRepository + +describe('useGetFileDownloadCount', () => { + it('should return the file download count', () => { + fileRepository.getFileDownloadCount = cy.stub().resolves(8) + + const { result } = renderHook(() => + useGetFileDownloadCount({ + fileRepository, + fileId: 1 + }) + ) + + expect(result.current.isLoadingDownloadCount).to.deep.equal(true) + expect(result.current.errorLoadingDownloadCount).to.deep.equal(null) + expect(result.current.downloadCount).to.deep.equal(null) + + cy.wrap(null).should(() => { + expect(result.current.isLoadingDownloadCount).to.deep.equal(false) + expect(result.current.errorLoadingDownloadCount).to.deep.equal(null) + expect(result.current.downloadCount).to.deep.equal(8) + expect(fileRepository.getFileDownloadCount).to.have.been.calledWith(1) + }) + }) + + it('should call the repository with a persistent id', () => { + const persistentId = 'doi:10.5072/FK2/AAA000' + fileRepository.getFileDownloadCount = cy.stub().resolves(3) + + const { result } = renderHook(() => + useGetFileDownloadCount({ + fileRepository, + fileId: persistentId + }) + ) + + expect(result.current.isLoadingDownloadCount).to.deep.equal(true) + expect(result.current.downloadCount).to.deep.equal(null) + + cy.wrap(null).should(() => { + expect(result.current.isLoadingDownloadCount).to.deep.equal(false) + expect(result.current.downloadCount).to.deep.equal(3) + expect(fileRepository.getFileDownloadCount).to.have.been.calledWith(persistentId) + }) + }) + + it('should return an error message when the repository fails', () => { + fileRepository.getFileDownloadCount = cy.stub().rejects(new Error('Error message')) + + const { result } = renderHook(() => + useGetFileDownloadCount({ + fileRepository, + fileId: 1 + }) + ) + + expect(result.current.isLoadingDownloadCount).to.deep.equal(true) + expect(result.current.errorLoadingDownloadCount).to.deep.equal(null) + expect(result.current.downloadCount).to.deep.equal(null) + + cy.wrap(null).should(() => { + expect(result.current.isLoadingDownloadCount).to.deep.equal(false) + expect(result.current.downloadCount).to.deep.equal(null) + expect(result.current.errorLoadingDownloadCount).to.deep.equal( + 'Something went wrong while getting the file download count. Try again later.' + ) + }) + }) +}) From 058defe221c3fc427a694a393b518a034f1229ba Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 2 Jul 2026 13:19:57 -0400 Subject: [PATCH 3/6] fix: update error handling --- src/files/domain/useCases/getFileDownloadCount.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/files/domain/useCases/getFileDownloadCount.ts b/src/files/domain/useCases/getFileDownloadCount.ts index 563f5e570..017cb6c97 100644 --- a/src/files/domain/useCases/getFileDownloadCount.ts +++ b/src/files/domain/useCases/getFileDownloadCount.ts @@ -4,7 +4,5 @@ export async function getFileDownloadCount( fileRepository: FileRepository, fileId: number | string ): Promise { - return fileRepository.getFileDownloadCount(fileId).catch((error: Error) => { - throw new Error(error.message) - }) + return fileRepository.getFileDownloadCount(fileId) } From 5adb9d42889392be549fb2610bbfc901628345c0 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 2 Jul 2026 14:05:40 -0400 Subject: [PATCH 4/6] fix: copilot reviews --- .../file/file-metrics/FileMetrics.tsx | 9 ++-- .../file-metrics/useGetFileDownloadCount.ts | 2 + tests/component/sections/file/File.spec.tsx | 22 ++++++++ .../useGetFileDownloadCount.spec.tsx | 52 +++++++++++++++++++ .../e2e/sections/file/File.spec.tsx | 1 - 5 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/sections/file/file-metrics/FileMetrics.tsx b/src/sections/file/file-metrics/FileMetrics.tsx index 4b1de04fc..1c6d0c3fe 100644 --- a/src/sections/file/file-metrics/FileMetrics.tsx +++ b/src/sections/file/file-metrics/FileMetrics.tsx @@ -13,10 +13,7 @@ interface FileMetricsProps { export const FileMetrics = ({ fileRepository, fileId }: FileMetricsProps) => { const { t, i18n } = useTranslation('file') const { downloadCount, isLoadingDownloadCount, errorLoadingDownloadCount } = - useGetFileDownloadCount({ - fileRepository, - fileId - }) + useGetFileDownloadCount({ fileRepository, fileId }) const count = downloadCount ?? 0 const formatNumber = (value: number) => @@ -56,10 +53,10 @@ const FileMetricsSkeleton = () => (
- +
- +
diff --git a/src/sections/file/file-metrics/useGetFileDownloadCount.ts b/src/sections/file/file-metrics/useGetFileDownloadCount.ts index 994f0bf65..16bd0cf9a 100644 --- a/src/sections/file/file-metrics/useGetFileDownloadCount.ts +++ b/src/sections/file/file-metrics/useGetFileDownloadCount.ts @@ -26,6 +26,8 @@ export const useGetFileDownloadCount = ({ useEffect(() => { const handleGetDownloadCount = async () => { setIsLoadingDownloadCount(true) + setErrorLoadingDownloadCount(null) + setDownloadCount(null) try { const res = await getFileDownloadCount(fileRepository, fileId) diff --git a/tests/component/sections/file/File.spec.tsx b/tests/component/sections/file/File.spec.tsx index 5eb789657..8d1a6ae8c 100644 --- a/tests/component/sections/file/File.spec.tsx +++ b/tests/component/sections/file/File.spec.tsx @@ -56,6 +56,28 @@ describe('File', () => { cy.wrap(fileRepository.getFileDownloadCount).should('be.calledWith', testFile.id) }) + it('does not render file metrics when the download count API returns an error', () => { + const testFile = FileMother.createRealistic() + fileRepository.getById = cy.stub().resolves(testFile) + fileRepository.getFileDownloadCount = cy.stub().rejects(new Error('Error loading metric')) + + cy.customMount( + + + + ) + + cy.findAllByText(testFile.name).should('exist') + cy.findByTestId('file-metrics-skeleton').should('not.exist') + cy.findByText('File Metrics').should('not.exist') + cy.findByTestId('file-download-count').should('not.exist') + }) + it('renders skeleton while loading', () => { const testFile = FileMother.createRealistic() fileRepository.getById = cy.stub().resolves(testFile) diff --git a/tests/component/sections/file/file-metrics/useGetFileDownloadCount.spec.tsx b/tests/component/sections/file/file-metrics/useGetFileDownloadCount.spec.tsx index 61f814d15..6392f2440 100644 --- a/tests/component/sections/file/file-metrics/useGetFileDownloadCount.spec.tsx +++ b/tests/component/sections/file/file-metrics/useGetFileDownloadCount.spec.tsx @@ -70,4 +70,56 @@ describe('useGetFileDownloadCount', () => { ) }) }) + + it('should clear stale error and download count before fetching again', () => { + let resolveSecondRequest: (downloadCount: number) => void = () => undefined + const secondRequest = new Promise((resolve) => { + resolveSecondRequest = resolve + }) + + fileRepository.getFileDownloadCount = cy + .stub() + .onFirstCall() + .rejects(new Error('Error message')) + .onSecondCall() + .returns(secondRequest) + + const { result, rerender } = renderHook( + ({ fileId }) => + useGetFileDownloadCount({ + fileRepository, + fileId + }), + { + initialProps: { + fileId: 1 + } + } + ) + + cy.wrap(null).should(() => { + expect(result.current.isLoadingDownloadCount).to.deep.equal(false) + expect(result.current.downloadCount).to.deep.equal(null) + expect(result.current.errorLoadingDownloadCount).to.deep.equal( + 'Something went wrong while getting the file download count. Try again later.' + ) + }) + + cy.then(() => { + rerender({ fileId: 2 }) + + expect(result.current.isLoadingDownloadCount).to.deep.equal(true) + expect(result.current.downloadCount).to.deep.equal(null) + expect(result.current.errorLoadingDownloadCount).to.deep.equal(null) + + resolveSecondRequest(4) + }) + + cy.wrap(null).should(() => { + expect(result.current.isLoadingDownloadCount).to.deep.equal(false) + expect(result.current.downloadCount).to.deep.equal(4) + expect(result.current.errorLoadingDownloadCount).to.deep.equal(null) + expect(fileRepository.getFileDownloadCount).to.have.been.calledWith(2) + }) + }) }) diff --git a/tests/e2e-integration/e2e/sections/file/File.spec.tsx b/tests/e2e-integration/e2e/sections/file/File.spec.tsx index 9252d2e9e..03f21382e 100644 --- a/tests/e2e-integration/e2e/sections/file/File.spec.tsx +++ b/tests/e2e-integration/e2e/sections/file/File.spec.tsx @@ -100,7 +100,6 @@ describe('File', () => { cy.get('@anchorClick').should('have.been.calledOnce') cy.findByText('Your download has started.').should('exist') - cy.wait(5000) cy.visit(`${FRONTEND_BASE_PATH}/files?id=${fileId}`) cy.findByTestId('file-download-count').should('contain.text', '1 Download') } From b692f2657b2fcab478f1d7467f8e7660bb12f4bf Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 2 Jul 2026 14:40:19 -0400 Subject: [PATCH 5/6] fix: remove the duplicated use case --- public/locales/en/file.json | 3 +- public/locales/es/file.json | 3 +- src/sections/file/File.tsx | 2 +- .../file/file-metrics/FileMetrics.tsx | 37 +----- .../file-metrics/useGetFileDownloadCount.ts | 51 ------- tests/component/sections/file/File.spec.tsx | 32 +---- .../useGetFileDownloadCount.spec.tsx | 125 ------------------ 7 files changed, 11 insertions(+), 242 deletions(-) delete mode 100644 src/sections/file/file-metrics/useGetFileDownloadCount.ts delete mode 100644 tests/component/sections/file/file-metrics/useGetFileDownloadCount.spec.tsx diff --git a/public/locales/en/file.json b/public/locales/en/file.json index 37d1de8b1..87e83f5be 100644 --- a/public/locales/en/file.json +++ b/public/locales/en/file.json @@ -44,8 +44,7 @@ "default_other": "{{formattedCount}} Downloads" }, "defaultTip": "Total downloads for this file." - }, - "defaultGetDownloadCountError": "Something went wrong while getting the file download count. Try again later." + } }, "fileAccess": { "title": "File Access", diff --git a/public/locales/es/file.json b/public/locales/es/file.json index 0a609872a..641f616ab 100644 --- a/public/locales/es/file.json +++ b/public/locales/es/file.json @@ -44,8 +44,7 @@ "default_other": "{{formattedCount}} Descargas" }, "defaultTip": "Descargas totales de este archivo." - }, - "defaultGetDownloadCountError": "Algo salió mal al obtener el recuento de descargas del archivo. Intenta nuevamente más tarde." + } }, "fileAccess": { "title": "Acceso al archivo", diff --git a/src/sections/file/File.tsx b/src/sections/file/File.tsx index deaa29a04..947c3a614 100644 --- a/src/sections/file/File.tsx +++ b/src/sections/file/File.tsx @@ -188,7 +188,7 @@ export function File({ {!isDeaccessioned && } - + diff --git a/src/sections/file/file-metrics/FileMetrics.tsx b/src/sections/file/file-metrics/FileMetrics.tsx index 1c6d0c3fe..0527edbd8 100644 --- a/src/sections/file/file-metrics/FileMetrics.tsx +++ b/src/sections/file/file-metrics/FileMetrics.tsx @@ -1,32 +1,16 @@ import { useTranslation } from 'react-i18next' -import Skeleton, { SkeletonTheme } from 'react-loading-skeleton' import { QuestionMarkTooltip } from '@iqss/dataverse-design-system' -import { FileRepository } from '@/files/domain/repositories/FileRepository' -import { useGetFileDownloadCount } from './useGetFileDownloadCount' import styles from './FileMetrics.module.scss' interface FileMetricsProps { - fileRepository: FileRepository - fileId: number | string + downloadCount: number } -export const FileMetrics = ({ fileRepository, fileId }: FileMetricsProps) => { +export const FileMetrics = ({ downloadCount }: FileMetricsProps) => { const { t, i18n } = useTranslation('file') - const { downloadCount, isLoadingDownloadCount, errorLoadingDownloadCount } = - useGetFileDownloadCount({ fileRepository, fileId }) - - const count = downloadCount ?? 0 const formatNumber = (value: number) => new Intl.NumberFormat(i18n.languages[0] || undefined).format(value) - if (isLoadingDownloadCount) { - return - } - - if (errorLoadingDownloadCount) { - return null - } - return (
@@ -39,8 +23,8 @@ export const FileMetrics = ({ fileRepository, fileId }: FileMetricsProps) => {
{t('metrics.downloads.count.default', { - count, - formattedCount: formatNumber(count) + count: downloadCount, + formattedCount: formatNumber(downloadCount) })}{' '} @@ -48,16 +32,3 @@ export const FileMetrics = ({ fileRepository, fileId }: FileMetricsProps) => {
) } - -const FileMetricsSkeleton = () => ( - -
-
- -
-
- -
-
-
-) diff --git a/src/sections/file/file-metrics/useGetFileDownloadCount.ts b/src/sections/file/file-metrics/useGetFileDownloadCount.ts deleted file mode 100644 index 16bd0cf9a..000000000 --- a/src/sections/file/file-metrics/useGetFileDownloadCount.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { FileRepository } from '@/files/domain/repositories/FileRepository' -import { getFileDownloadCount } from '@/files/domain/useCases/getFileDownloadCount' - -interface UseGetFileDownloadCountProps { - fileRepository: FileRepository - fileId: number | string -} - -interface UseGetFileDownloadCountReturn { - downloadCount: number | null - isLoadingDownloadCount: boolean - errorLoadingDownloadCount: string | null -} - -export const useGetFileDownloadCount = ({ - fileRepository, - fileId -}: UseGetFileDownloadCountProps): UseGetFileDownloadCountReturn => { - const { t } = useTranslation('file') - const [downloadCount, setDownloadCount] = useState(null) - const [isLoadingDownloadCount, setIsLoadingDownloadCount] = useState(true) - const [errorLoadingDownloadCount, setErrorLoadingDownloadCount] = useState(null) - - useEffect(() => { - const handleGetDownloadCount = async () => { - setIsLoadingDownloadCount(true) - setErrorLoadingDownloadCount(null) - setDownloadCount(null) - - try { - const res = await getFileDownloadCount(fileRepository, fileId) - - setDownloadCount(res) - } catch { - setErrorLoadingDownloadCount(t('metrics.defaultGetDownloadCountError')) - } finally { - setIsLoadingDownloadCount(false) - } - } - - void handleGetDownloadCount() - }, [fileRepository, fileId, t]) - - return { - downloadCount, - isLoadingDownloadCount, - errorLoadingDownloadCount - } -} diff --git a/tests/component/sections/file/File.spec.tsx b/tests/component/sections/file/File.spec.tsx index 8d1a6ae8c..d73aecbb0 100644 --- a/tests/component/sections/file/File.spec.tsx +++ b/tests/component/sections/file/File.spec.tsx @@ -10,16 +10,15 @@ import { DataverseInfoMockRepository } from '@/stories/shared-mock-repositories/ import { ContactMockRepository } from '@/stories/shared-mock-repositories/contact/ContactMockRepository' import { DatasetVersionMother } from '@tests/component/dataset/domain/models/DatasetMother' import { WithRepositories } from '@tests/component/WithRepositories' +import { FileMetadataMother } from '@tests/component/files/domain/models/FileMetadataMother' const fileRepository: FileRepository = {} as FileRepository describe('File', () => { - beforeEach(() => { - fileRepository.getFileDownloadCount = cy.stub().resolves(8) - }) - it('renders the File page title, details and metrics', () => { - const testFile = FileMother.createRealistic() + const testFile = FileMother.createRealistic({ + metadata: FileMetadataMother.createDefault({ downloadCount: 8 }) + }) fileRepository.getById = cy.stub().resolves(testFile) cy.customMount( @@ -53,29 +52,6 @@ describe('File', () => { cy.findByRole('group', { name: 'File Action Buttons' }).should('exist') cy.findByText('File Metrics').should('exist') cy.findByTestId('file-download-count').should('contain.text', '8 Downloads') - cy.wrap(fileRepository.getFileDownloadCount).should('be.calledWith', testFile.id) - }) - - it('does not render file metrics when the download count API returns an error', () => { - const testFile = FileMother.createRealistic() - fileRepository.getById = cy.stub().resolves(testFile) - fileRepository.getFileDownloadCount = cy.stub().rejects(new Error('Error loading metric')) - - cy.customMount( - - - - ) - - cy.findAllByText(testFile.name).should('exist') - cy.findByTestId('file-metrics-skeleton').should('not.exist') - cy.findByText('File Metrics').should('not.exist') - cy.findByTestId('file-download-count').should('not.exist') }) it('renders skeleton while loading', () => { diff --git a/tests/component/sections/file/file-metrics/useGetFileDownloadCount.spec.tsx b/tests/component/sections/file/file-metrics/useGetFileDownloadCount.spec.tsx deleted file mode 100644 index 6392f2440..000000000 --- a/tests/component/sections/file/file-metrics/useGetFileDownloadCount.spec.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { FileRepository } from '@/files/domain/repositories/FileRepository' -import { useGetFileDownloadCount } from '@/sections/file/file-metrics/useGetFileDownloadCount' -import { renderHook } from '@testing-library/react' - -const fileRepository: FileRepository = {} as FileRepository - -describe('useGetFileDownloadCount', () => { - it('should return the file download count', () => { - fileRepository.getFileDownloadCount = cy.stub().resolves(8) - - const { result } = renderHook(() => - useGetFileDownloadCount({ - fileRepository, - fileId: 1 - }) - ) - - expect(result.current.isLoadingDownloadCount).to.deep.equal(true) - expect(result.current.errorLoadingDownloadCount).to.deep.equal(null) - expect(result.current.downloadCount).to.deep.equal(null) - - cy.wrap(null).should(() => { - expect(result.current.isLoadingDownloadCount).to.deep.equal(false) - expect(result.current.errorLoadingDownloadCount).to.deep.equal(null) - expect(result.current.downloadCount).to.deep.equal(8) - expect(fileRepository.getFileDownloadCount).to.have.been.calledWith(1) - }) - }) - - it('should call the repository with a persistent id', () => { - const persistentId = 'doi:10.5072/FK2/AAA000' - fileRepository.getFileDownloadCount = cy.stub().resolves(3) - - const { result } = renderHook(() => - useGetFileDownloadCount({ - fileRepository, - fileId: persistentId - }) - ) - - expect(result.current.isLoadingDownloadCount).to.deep.equal(true) - expect(result.current.downloadCount).to.deep.equal(null) - - cy.wrap(null).should(() => { - expect(result.current.isLoadingDownloadCount).to.deep.equal(false) - expect(result.current.downloadCount).to.deep.equal(3) - expect(fileRepository.getFileDownloadCount).to.have.been.calledWith(persistentId) - }) - }) - - it('should return an error message when the repository fails', () => { - fileRepository.getFileDownloadCount = cy.stub().rejects(new Error('Error message')) - - const { result } = renderHook(() => - useGetFileDownloadCount({ - fileRepository, - fileId: 1 - }) - ) - - expect(result.current.isLoadingDownloadCount).to.deep.equal(true) - expect(result.current.errorLoadingDownloadCount).to.deep.equal(null) - expect(result.current.downloadCount).to.deep.equal(null) - - cy.wrap(null).should(() => { - expect(result.current.isLoadingDownloadCount).to.deep.equal(false) - expect(result.current.downloadCount).to.deep.equal(null) - expect(result.current.errorLoadingDownloadCount).to.deep.equal( - 'Something went wrong while getting the file download count. Try again later.' - ) - }) - }) - - it('should clear stale error and download count before fetching again', () => { - let resolveSecondRequest: (downloadCount: number) => void = () => undefined - const secondRequest = new Promise((resolve) => { - resolveSecondRequest = resolve - }) - - fileRepository.getFileDownloadCount = cy - .stub() - .onFirstCall() - .rejects(new Error('Error message')) - .onSecondCall() - .returns(secondRequest) - - const { result, rerender } = renderHook( - ({ fileId }) => - useGetFileDownloadCount({ - fileRepository, - fileId - }), - { - initialProps: { - fileId: 1 - } - } - ) - - cy.wrap(null).should(() => { - expect(result.current.isLoadingDownloadCount).to.deep.equal(false) - expect(result.current.downloadCount).to.deep.equal(null) - expect(result.current.errorLoadingDownloadCount).to.deep.equal( - 'Something went wrong while getting the file download count. Try again later.' - ) - }) - - cy.then(() => { - rerender({ fileId: 2 }) - - expect(result.current.isLoadingDownloadCount).to.deep.equal(true) - expect(result.current.downloadCount).to.deep.equal(null) - expect(result.current.errorLoadingDownloadCount).to.deep.equal(null) - - resolveSecondRequest(4) - }) - - cy.wrap(null).should(() => { - expect(result.current.isLoadingDownloadCount).to.deep.equal(false) - expect(result.current.downloadCount).to.deep.equal(4) - expect(result.current.errorLoadingDownloadCount).to.deep.equal(null) - expect(fileRepository.getFileDownloadCount).to.have.been.calledWith(2) - }) - }) -}) From f23421ba98f680232b27c98c5b04b95d3f009811 Mon Sep 17 00:00:00 2001 From: Cheng Shi Date: Thu, 2 Jul 2026 14:43:44 -0400 Subject: [PATCH 6/6] fix: remove the duplicated use case --- .../domain/repositories/FileRepository.ts | 1 - .../domain/useCases/getFileDownloadCount.ts | 8 ------- .../FileJSDataverseRepository.ts | 9 ------- src/stories/file/FileMockRepository.ts | 8 ------- .../files/FileJSDataverseRepository.spec.ts | 24 ------------------- 5 files changed, 50 deletions(-) delete mode 100644 src/files/domain/useCases/getFileDownloadCount.ts diff --git a/src/files/domain/repositories/FileRepository.ts b/src/files/domain/repositories/FileRepository.ts index 0e8ec474b..e5761c21e 100644 --- a/src/files/domain/repositories/FileRepository.ts +++ b/src/files/domain/repositories/FileRepository.ts @@ -33,7 +33,6 @@ export interface FileRepository { criteria?: FileCriteria, includeDeaccessioned?: boolean ) => Promise - getFileDownloadCount: (fileId: number | string) => Promise getFileVersionSummaries: ( fileId: number | string, paginationInfo?: FileVersionPaginationInfo diff --git a/src/files/domain/useCases/getFileDownloadCount.ts b/src/files/domain/useCases/getFileDownloadCount.ts deleted file mode 100644 index 017cb6c97..000000000 --- a/src/files/domain/useCases/getFileDownloadCount.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { FileRepository } from '../repositories/FileRepository' - -export async function getFileDownloadCount( - fileRepository: FileRepository, - fileId: number | string -): Promise { - return fileRepository.getFileDownloadCount(fileId) -} diff --git a/src/files/infrastructure/FileJSDataverseRepository.ts b/src/files/infrastructure/FileJSDataverseRepository.ts index f60c9c12d..d37d012db 100644 --- a/src/files/infrastructure/FileJSDataverseRepository.ts +++ b/src/files/infrastructure/FileJSDataverseRepository.ts @@ -202,15 +202,6 @@ export class FileJSDataverseRepository implements FileRepository { : Promise.resolve(0) } - getFileDownloadCount(fileId: number | string): Promise { - return getFileDownloadCount - .execute(fileId) - .then((downloadCount) => Number(downloadCount)) - .catch((error: ReadError) => { - throw new Error(error.message) - }) - } - private static getAllThumbnails(jsFiles: JSFile[]): Promise<(string | undefined)[]> { return Promise.all(jsFiles.map((jsFile) => this.getThumbnailById(jsFile.id))) } diff --git a/src/stories/file/FileMockRepository.ts b/src/stories/file/FileMockRepository.ts index 3296088ae..e4987b47a 100644 --- a/src/stories/file/FileMockRepository.ts +++ b/src/stories/file/FileMockRepository.ts @@ -89,14 +89,6 @@ export class FileMockRepository implements FileRepository { return FileMetadataMother.createDownloadUrl() } - getFileDownloadCount(_fileId: number | string): Promise { - return new Promise((resolve) => { - setTimeout(() => { - resolve(0) - }, FakerHelper.loadingTimout()) - }) - } - uploadFile( _datasetId: number | string, _file: FileHolder, diff --git a/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts b/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts index d34db8218..6936cb362 100644 --- a/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts @@ -35,7 +35,6 @@ import { File } from '../../../../src/files/domain/models/File' import { FileIngest, FileIngestStatus } from '../../../../src/files/domain/models/FileIngest' import { DateHelper } from '@/shared/helpers/DateHelper' import { FileMetadataDTO } from '@/files/domain/useCases/DTOs/FileMetadataDTO' -import { getFileDownloadCount } from '@/files/domain/useCases/getFileDownloadCount' const DRAFT_PARAM = DatasetNonNumericVersion.DRAFT @@ -934,29 +933,6 @@ describe('File JSDataverse Repository', () => { }) }) - describe('getFileDownloadCount', () => { - it('gets zero downloads for a published file with no downloads by database id', async () => { - const datasetResponse = await DatasetHelper.createWithFileAndPublish(FileHelper.create()) - if (!datasetResponse.file) throw new Error('File not found') - - const downloadCount = await getFileDownloadCount(fileRepository, datasetResponse.file.id) - - expect(downloadCount).to.deep.equal(0) - }) - - it('gets the download count by database id after the file has been downloaded', async () => { - const datasetResponse = await DatasetHelper.createWithFileAndPublish(FileHelper.create()) - if (!datasetResponse.file) throw new Error('File not found') - - await FileHelper.download(datasetResponse.file.id) - await TestsUtils.wait(5000) // Wait for the file download count to be updated - - const downloadCountById = await getFileDownloadCount(fileRepository, datasetResponse.file.id) - - expect(downloadCountById).to.deep.equal(1) - }) - }) - describe('edit file metadata', () => { it('edits file metadata', async () => { const datasetResponse = await DatasetHelper.createWithFile(FileHelper.create())