From 00480a3d26bed6d228aff7a15eba99761d713d7c Mon Sep 17 00:00:00 2001 From: Juraj Roka <95219754+jr-rk@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:06:05 +0200 Subject: [PATCH 1/4] Added new feature for downloads of item's bitstreams --- src/app/item-page/item-page.module.ts | 6 ++++ .../total-downloads.component.html | 7 ++++ .../total-downloads.component.scss | 0 .../file-section/total-downloads.component.ts | 33 +++++++++++++++++++ .../publication/publication.component.html | 3 +- .../untyped-item/untyped-item.component.html | 3 +- src/assets/i18n/cs.json5 | 5 ++- src/assets/i18n/en.json5 | 4 ++- 8 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 src/app/item-page/simple/field-components/file-section/total-downloads.component.html create mode 100644 src/app/item-page/simple/field-components/file-section/total-downloads.component.scss create mode 100644 src/app/item-page/simple/field-components/file-section/total-downloads.component.ts diff --git a/src/app/item-page/item-page.module.ts b/src/app/item-page/item-page.module.ts index 18bbc4fd606..26040cfcb18 100644 --- a/src/app/item-page/item-page.module.ts +++ b/src/app/item-page/item-page.module.ts @@ -57,6 +57,7 @@ import { ItemAlertsComponent } from './alerts/item-alerts.component'; import { ItemVersionsModule } from './versions/item-versions.module'; import { BitstreamRequestACopyPageComponent } from './bitstreams/request-a-copy/bitstream-request-a-copy-page.component'; import { FileSectionComponent } from './simple/field-components/file-section/file-section.component'; +import { TotalDownloadsComponent } from './simple/field-components/file-section/total-downloads.component'; import { ItemSharedModule } from './item-shared.module'; import { DsoPageModule } from '../shared/dso-page/dso-page.module'; import { ThemedItemAlertsComponent } from './alerts/themed-item-alerts.component'; @@ -90,6 +91,7 @@ import { ClarinIdentifierItemFieldComponent } from './simple/field-components/cl import { ClarinDateItemFieldComponent } from './simple/field-components/clarin-date-item-field/clarin-date-item-field.component'; import { ClarinDescriptionItemFieldComponent } from './simple/field-components/clarin-description-item-field/clarin-description-item-field.component'; import { ClarinFilesSectionComponent } from './clarin-files-section/clarin-files-section.component'; +import { UsageReportDataService } from '../core/statistics/usage-report-data.service'; const ENTRY_COMPONENTS = [ // put only entry components that use custom decorator @@ -99,6 +101,7 @@ const ENTRY_COMPONENTS = [ const DECLARATIONS = [ FileSectionComponent, + TotalDownloadsComponent, ThemedFileSectionComponent, ItemPageComponent, ThemedItemPageComponent, @@ -181,6 +184,9 @@ const DECLARATIONS = [ ], exports: [ ...DECLARATIONS, + ], + providers: [ + UsageReportDataService, ] }) export class ItemPageModule { diff --git a/src/app/item-page/simple/field-components/file-section/total-downloads.component.html b/src/app/item-page/simple/field-components/file-section/total-downloads.component.html new file mode 100644 index 00000000000..498a810f42b --- /dev/null +++ b/src/app/item-page/simple/field-components/file-section/total-downloads.component.html @@ -0,0 +1,7 @@ + +
+
+ {{ totalDownloads }} +
+
+
diff --git a/src/app/item-page/simple/field-components/file-section/total-downloads.component.scss b/src/app/item-page/simple/field-components/file-section/total-downloads.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts b/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts new file mode 100644 index 00000000000..aeba7cc97d2 --- /dev/null +++ b/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts @@ -0,0 +1,33 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { UsageReport } from 'src/app/core/statistics/models/usage-report.model'; +import { UsageReportDataService } from 'src/app/core/statistics/usage-report-data.service'; + +@Component({ + selector: 'ds-total-downloads', + templateUrl: './total-downloads.component.html', + styleUrls: ['./total-downloads.component.scss'] +}) +export class TotalDownloadsComponent implements OnInit { + + @Input() itemUuid: string; + + totalDownloads: number | null = null; + + label = 'item.page.files.downloads'; + + constructor(private usageReportDataService: UsageReportDataService) {} + + ngOnInit(): void { + if (this.itemUuid) { + const reportType = 'TotalDownloads'; + this.usageReportDataService.getStatistic(this.itemUuid, reportType) + .subscribe((report: UsageReport) => { + this.totalDownloads = report.points.reduce((total, point) => { + const values = point.values as any; + const views = values['views'] || values.views || 0; + return total + views; + }, 0); + }); + } + } +} diff --git a/src/app/item-page/simple/item-types/publication/publication.component.html b/src/app/item-page/simple/item-types/publication/publication.component.html index 584938f8cdf..5c00b504060 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.html +++ b/src/app/item-page/simple/item-types/publication/publication.component.html @@ -23,7 +23,8 @@
- + + - + + Date: Thu, 21 Aug 2025 19:12:03 +0200 Subject: [PATCH 2/4] fixed Copilot's suggestions: any type & redundant property access pattern --- .../file-section/total-downloads.component.ts | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts b/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts index aeba7cc97d2..191c0287159 100644 --- a/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts +++ b/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts @@ -1,7 +1,23 @@ import { Component, OnInit, Input } from '@angular/core'; -import { UsageReport } from 'src/app/core/statistics/models/usage-report.model'; +import { UsageReport, Point } from 'src/app/core/statistics/models/usage-report.model'; import { UsageReportDataService } from 'src/app/core/statistics/usage-report-data.service'; +/** + * Interface representing the actual structure of point.values as returned by the API + * This differs from the interface definition which shows it as an array (in usage-report.model.ts) + */ +interface PointValues { + views: number; + [key: string]: number; // Allow for other potential numeric properties +} + +/** + * Extended Point interface with correct values typing + */ +interface DownloadPoint extends Omit { + values: PointValues; +} + @Component({ selector: 'ds-total-downloads', templateUrl: './total-downloads.component.html', @@ -22,9 +38,12 @@ export class TotalDownloadsComponent implements OnInit { const reportType = 'TotalDownloads'; this.usageReportDataService.getStatistic(this.itemUuid, reportType) .subscribe((report: UsageReport) => { - this.totalDownloads = report.points.reduce((total, point) => { - const values = point.values as any; - const views = values['views'] || values.views || 0; + // Type assertion to match the actual API response structure + // The API returns values as an object, not an array as defined in the interface + const points = report.points as unknown as DownloadPoint[]; + + this.totalDownloads = points.reduce((total, point) => { + const views = point.values.views || 0; return total + views; }, 0); }); From 111b3fbf646b55a4f00adddc90761cc28d300a9f Mon Sep 17 00:00:00 2001 From: Juraj Roka <95219754+jr-rk@users.noreply.github.com> Date: Thu, 21 Aug 2025 19:46:10 +0200 Subject: [PATCH 3/4] another copilots refactoring --- .../statistics/models/usage-report.model.ts | 3 +- .../file-section/total-downloads.component.ts | 47 ++++++++----------- 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/src/app/core/statistics/models/usage-report.model.ts b/src/app/core/statistics/models/usage-report.model.ts index da976a8b903..ff8ae6fcdcb 100644 --- a/src/app/core/statistics/models/usage-report.model.ts +++ b/src/app/core/statistics/models/usage-report.model.ts @@ -47,5 +47,6 @@ export interface Point { type: string; values: { views: number; - }[]; + [key: string]: number; + }; } diff --git a/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts b/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts index 191c0287159..c378a87fd7b 100644 --- a/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts +++ b/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts @@ -1,22 +1,7 @@ import { Component, OnInit, Input } from '@angular/core'; -import { UsageReport, Point } from 'src/app/core/statistics/models/usage-report.model'; import { UsageReportDataService } from 'src/app/core/statistics/usage-report-data.service'; - -/** - * Interface representing the actual structure of point.values as returned by the API - * This differs from the interface definition which shows it as an array (in usage-report.model.ts) - */ -interface PointValues { - views: number; - [key: string]: number; // Allow for other potential numeric properties -} - -/** - * Extended Point interface with correct values typing - */ -interface DownloadPoint extends Omit { - values: PointValues; -} +import { catchError } from 'rxjs/operators'; +import { of } from 'rxjs'; @Component({ selector: 'ds-total-downloads', @@ -25,11 +10,11 @@ interface DownloadPoint extends Omit { }) export class TotalDownloadsComponent implements OnInit { - @Input() itemUuid: string; + @Input() itemUuid!: string; totalDownloads: number | null = null; - label = 'item.page.files.downloads'; + readonly label = 'item.page.files.downloads'; constructor(private usageReportDataService: UsageReportDataService) {} @@ -37,15 +22,21 @@ export class TotalDownloadsComponent implements OnInit { if (this.itemUuid) { const reportType = 'TotalDownloads'; this.usageReportDataService.getStatistic(this.itemUuid, reportType) - .subscribe((report: UsageReport) => { - // Type assertion to match the actual API response structure - // The API returns values as an object, not an array as defined in the interface - const points = report.points as unknown as DownloadPoint[]; - - this.totalDownloads = points.reduce((total, point) => { - const views = point.values.views || 0; - return total + views; - }, 0); + .pipe( + catchError(error => { + console.error('Failed to fetch total downloads statistics:', error); + return of(null); + }) + ) + .subscribe(report => { + if (report) { + this.totalDownloads = report.points.reduce((total, point) => { + const views = point.values.views || 0; + return total + views; + }, 0); + } else { + this.totalDownloads = null; + } }); } } From 3b636f456bc9ecaa8c1449098a8f64054e857eb5 Mon Sep 17 00:00:00 2001 From: Juraj Roka <95219754+jr-rk@users.noreply.github.com> Date: Mon, 25 Aug 2025 12:32:20 +0200 Subject: [PATCH 4/4] Changed Milan's requests and comments --- .../total-downloads.component.html | 4 +- .../file-section/total-downloads.component.ts | 74 +++++++++++++------ 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/src/app/item-page/simple/field-components/file-section/total-downloads.component.html b/src/app/item-page/simple/field-components/file-section/total-downloads.component.html index 498a810f42b..72db7109aa0 100644 --- a/src/app/item-page/simple/field-components/file-section/total-downloads.component.html +++ b/src/app/item-page/simple/field-components/file-section/total-downloads.component.html @@ -1,5 +1,5 @@ - -
+ +
{{ totalDownloads }}
diff --git a/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts b/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts index c378a87fd7b..d5cc17cbd08 100644 --- a/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts +++ b/src/app/item-page/simple/field-components/file-section/total-downloads.component.ts @@ -3,6 +3,13 @@ import { UsageReportDataService } from 'src/app/core/statistics/usage-report-dat import { catchError } from 'rxjs/operators'; import { of } from 'rxjs'; +/** + * Component that displays the total number of downloads for all bitstreams within a DSpace item. + * + * This component fetches download statistics for a given item using its UUID and aggregates + * the download counts from all bitstreams associated with that item. The result is displayed + * as a single total download count. + */ @Component({ selector: 'ds-total-downloads', templateUrl: './total-downloads.component.html', @@ -10,34 +17,57 @@ import { of } from 'rxjs'; }) export class TotalDownloadsComponent implements OnInit { + /** + * The UUID of the DSpace item for which to fetch download statistics. + */ @Input() itemUuid!: string; - totalDownloads: number | null = null; + /** + * The total number of downloads across all bitstreams for the item. + * Defaults to 0 and will show 0 if no data is available or an error occurs. + */ + totalDownloads: number = 0; - readonly label = 'item.page.files.downloads'; + /** + * The translation key for the downloadsLabel displayed alongside the download count. + */ + readonly downloadsLabel = 'item.page.files.downloads'; - constructor(private usageReportDataService: UsageReportDataService) {} + constructor(private usageReportDataService: UsageReportDataService) { } + + /** + * Fetches the total download statistics for the item specified by itemUuid. + * The component will: + * 1. Call the UsageReportDataService with the item UUID and 'TotalDownloads' report type + * 2. Aggregate all download counts (views) from all bitstreams in the response + * 3. Set the totalDownloads property with the sum + * 4. Handle errors gracefully by setting totalDownloads to 0 and logging the error + * + * @throws Will log an error to console if the API call fails, but won't throw an exception + */ ngOnInit(): void { - if (this.itemUuid) { - const reportType = 'TotalDownloads'; - this.usageReportDataService.getStatistic(this.itemUuid, reportType) - .pipe( - catchError(error => { - console.error('Failed to fetch total downloads statistics:', error); - return of(null); - }) - ) - .subscribe(report => { - if (report) { - this.totalDownloads = report.points.reduce((total, point) => { - const views = point.values.views || 0; - return total + views; - }, 0); - } else { - this.totalDownloads = null; - } - }); + if (!this.itemUuid) { + return; } + + const reportType = 'TotalDownloads'; + this.usageReportDataService.getStatistic(this.itemUuid, reportType) + .pipe( + catchError(error => { + console.error('Failed to fetch total downloads statistics:', error); + return of(null); + }) + ) + .subscribe(report => { + if (report) { + this.totalDownloads = report.points.reduce((total, point) => { + const views = point.values.views || 0; + return total + views; + }, 0); + } else { + this.totalDownloads = 0; // Show 0 instead of null when no data is available + } + }); } }