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/public/locales/en/file.json b/public/locales/en/file.json index 2b166abee..87e83f5be 100644 --- a/public/locales/en/file.json +++ b/public/locales/en/file.json @@ -32,6 +32,20 @@ "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." + } + }, "fileAccess": { "title": "File Access", "restricted": { diff --git a/public/locales/es/file.json b/public/locales/es/file.json index 6fb27e80d..641f616ab 100644 --- a/public/locales/es/file.json +++ b/public/locales/es/file.json @@ -32,6 +32,20 @@ "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." + } + }, "fileAccess": { "title": "Acceso al archivo", "restricted": { diff --git a/src/sections/file/File.tsx b/src/sections/file/File.tsx index a19dfcc80..947c3a614 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..0527edbd8 --- /dev/null +++ b/src/sections/file/file-metrics/FileMetrics.tsx @@ -0,0 +1,34 @@ +import { useTranslation } from 'react-i18next' +import { QuestionMarkTooltip } from '@iqss/dataverse-design-system' +import styles from './FileMetrics.module.scss' + +interface FileMetricsProps { + downloadCount: number +} + +export const FileMetrics = ({ downloadCount }: FileMetricsProps) => { + const { t, i18n } = useTranslation('file') + const formatNumber = (value: number) => + new Intl.NumberFormat(i18n.languages[0] || undefined).format(value) + + return ( +
+
+ + {t('metrics.title')}{' '} + + +
+ +
+ + {t('metrics.downloads.count.default', { + count: downloadCount, + formattedCount: formatNumber(downloadCount) + })}{' '} + + +
+
+ ) +} diff --git a/tests/component/sections/file/File.spec.tsx b/tests/component/sections/file/File.spec.tsx index 1f9cf81bd..d73aecbb0 100644 --- a/tests/component/sections/file/File.spec.tsx +++ b/tests/component/sections/file/File.spec.tsx @@ -10,12 +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', () => { - it('renders the File page title and details', () => { - const testFile = FileMother.createRealistic() + it('renders the File page title, details and metrics', () => { + const testFile = FileMother.createRealistic({ + metadata: FileMetadataMother.createDefault({ downloadCount: 8 }) + }) fileRepository.getById = cy.stub().resolves(testFile) cy.customMount( @@ -47,6 +50,8 @@ 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') }) 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..03f21382e 100644 --- a/tests/e2e-integration/e2e/sections/file/File.spec.tsx +++ b/tests/e2e-integration/e2e/sections/file/File.spec.tsx @@ -60,6 +60,52 @@ 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.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..6936cb362 100644 --- a/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts +++ b/tests/e2e-integration/integration/files/FileJSDataverseRepository.spec.ts @@ -932,6 +932,7 @@ describe('File JSDataverse Repository', () => { }) }) }) + describe('edit file metadata', () => { it('edits file metadata', async () => { const datasetResponse = await DatasetHelper.createWithFile(FileHelper.create())