From 6c3ad31e15408e9f6e288b7a73b9aa26da67d4c1 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Fri, 19 Jun 2026 11:01:08 +0200 Subject: [PATCH 1/6] UoE/Add pagination to statistics tables (#726) The statistics tables (e.g. the repository-wide "Total visits" report) rendered every point in a single, ungoverned table and, combined with the backend cap of 10 items, users could only ever see the first 10 datasets. Add client-side pagination to the shared StatisticsTableComponent so that all reports paginate their points and every dataset can be browsed page by page. - Render only the current page of points (default 10 per page). - Show an ngb-pagination control when there are more points than fit on a page. - Add the `statistics.table.pagination.label` i18n key for accessibility. Co-Authored-By: Claude Opus 4.8 --- .../statistics-table.component.html | 16 +++++- .../statistics-table.component.spec.ts | 57 +++++++++++++++++++ .../statistics-table.component.ts | 37 +++++++++++- src/assets/i18n/en.json5 | 2 + 4 files changed, 110 insertions(+), 2 deletions(-) diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.html b/src/app/statistics-page/statistics-table/statistics-table.component.html index efa9ce43d99..d6f24bd4371 100644 --- a/src/app/statistics-page/statistics-table/statistics-table.component.html +++ b/src/app/statistics-page/statistics-table/statistics-table.component.html @@ -18,7 +18,7 @@

- {{ getLabel(point) | async }} @@ -33,4 +33,18 @@

+
+ + +
+ diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts b/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts index 105a7623d6b..7673507b294 100644 --- a/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts +++ b/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts @@ -96,5 +96,62 @@ describe('StatisticsTableComponent', () => { expect(de.query(By.css('td.item_2-downloads-data')).nativeElement.innerText) .toEqual('8'); }); + + it('should not display a pagination control when all points fit on a single page', () => { + expect(de.query(By.css('ngb-pagination'))).toBeNull(); + }); + }); + + describe('when the report has more points than the page size', () => { + + const numberOfPoints = 25; + + beforeEach(() => { + const points = []; + for (let i = 0; i < numberOfPoints; i++) { + points.push({ + id: `item_${i}`, + label: `item_${i}`, + values: { + views: i, + }, + }); + } + component.report = Object.assign(new UsageReport(), { points }); + component.ngOnInit(); + fixture.detectChanges(); + }); + + it('should only render the first page of points', () => { + expect(de.queryAll(By.css('[data-test="statistics-label"]')).length) + .toEqual(component.pageSize); + expect(de.query(By.css('td.item_0-views-data'))).toBeTruthy(); + expect(de.query(By.css('td.item_10-views-data'))).toBeNull(); + }); + + it('should display a pagination control', () => { + expect(de.query(By.css('ngb-pagination'))).toBeTruthy(); + }); + + it('should render the next page of points when the page changes', () => { + component.onPageChange(2); + fixture.detectChanges(); + + expect(de.query(By.css('td.item_0-views-data'))).toBeNull(); + expect(de.query(By.css('td.item_10-views-data'))).toBeTruthy(); + expect(de.queryAll(By.css('[data-test="statistics-label"]')).length) + .toEqual(component.pageSize); + }); + + it('should render the remaining points on the last page', () => { + const lastPage = Math.ceil(numberOfPoints / component.pageSize); + component.onPageChange(lastPage); + fixture.detectChanges(); + + const remaining = numberOfPoints - (lastPage - 1) * component.pageSize; + expect(de.queryAll(By.css('[data-test="statistics-label"]')).length) + .toEqual(remaining); + expect(de.query(By.css(`td.item_${numberOfPoints - 1}-views-data`))).toBeTruthy(); + }); }); }); diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.ts b/src/app/statistics-page/statistics-table/statistics-table.component.ts index cd717305681..26ad6d0821f 100644 --- a/src/app/statistics-page/statistics-table/statistics-table.component.ts +++ b/src/app/statistics-page/statistics-table/statistics-table.component.ts @@ -8,6 +8,7 @@ import { Input, OnInit, } from '@angular/core'; +import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule, TranslateService, @@ -38,7 +39,7 @@ import { isEmpty } from '../../shared/empty.util'; templateUrl: './statistics-table.component.html', styleUrls: ['./statistics-table.component.scss'], standalone: true, - imports: [NgIf, NgFor, AsyncPipe, TranslateModule], + imports: [NgIf, NgFor, AsyncPipe, TranslateModule, NgbPaginationModule], }) export class StatisticsTableComponent implements OnInit { @@ -48,6 +49,17 @@ export class StatisticsTableComponent implements OnInit { @Input() report: UsageReport; + /** + * The number of points (e.g. datasets, countries, cities) to show per page. + */ + @Input() + pageSize = 10; + + /** + * The currently displayed page (1-based, as expected by ngb-pagination). + */ + currentPage = 1; + /** * Boolean indicating whether the usage report has data */ @@ -73,6 +85,29 @@ export class StatisticsTableComponent implements OnInit { } } + /** + * The points to render for the currently selected page. + */ + get paginatedPoints(): Point[] { + const start = (this.currentPage - 1) * this.pageSize; + return this.report.points.slice(start, start + this.pageSize); + } + + /** + * Whether a pagination control is needed, i.e. there are more points than fit on a single page. + */ + get showPagination(): boolean { + return this.report.points.length > this.pageSize; + } + + /** + * Switch to the given page. + * @param page the 1-based page number to display + */ + onPageChange(page: number): void { + this.currentPage = page; + } + /** * Get the row label to display for a statistics point. * @param point the statistics point to get the label for diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 04d7384174b..4ad7e9578ed 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -4794,6 +4794,8 @@ "statistics.table.no-data": "No data available", + "statistics.table.pagination.label": "Statistics table pagination", + "statistics.table.title.TotalVisits": "Total visits", "statistics.table.title.TotalVisitsPerMonth": "Total visits per month", From 3ce9b29c46c088dfc71bfa9e637844dc5983966f Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Tue, 23 Jun 2026 11:13:41 +0200 Subject: [PATCH 2/6] UoE/Use standard ds-pagination for statistics tables (#726) Replace the raw ngb-pagination control with DSpace's standard PaginationComponent (ds-pagination) for consistency with the rest of the UI. Each statistics report table now paginates its points independently via a unique pagination id, the current page is driven through PaginationService, and the standard "Now showing X - Y of Z" detail and page-size selector are shown. - StatisticsTableComponent: build PaginationComponentOptions and derive the current page of points from PaginationService.getCurrentPagination(). - Template: wrap the table in and iterate the paged points. - Remove the custom statistics.table.pagination.label i18n key (ds-pagination provides its own accessible labels). - Provide a mocked PaginationService in the statistics page specs. Co-Authored-By: Claude Opus 4.8 --- ...llection-statistics-page.component.spec.ts | 2 + ...ommunity-statistics-page.component.spec.ts | 2 + .../item-statistics-page.component.spec.ts | 2 + .../site-statistics-page.component.spec.ts | 2 + .../statistics-table.component.html | 75 +++++++++---------- .../statistics-table.component.spec.ts | 67 ++++++++++++++--- .../statistics-table.component.ts | 55 +++++++------- src/assets/i18n/en.json5 | 2 - 8 files changed, 126 insertions(+), 81 deletions(-) diff --git a/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.spec.ts b/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.spec.ts index c78fef0521e..08c7c7c6b8c 100644 --- a/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.spec.ts +++ b/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.spec.ts @@ -16,6 +16,7 @@ import { of as observableOf } from 'rxjs'; import { AuthService } from '../../core/auth/auth.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; +import { PaginationService } from '../../core/pagination/pagination.service'; import { Collection } from '../../core/shared/collection.model'; import { UsageReport } from '../../core/statistics/models/usage-report.model'; import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; @@ -82,6 +83,7 @@ describe('CollectionStatisticsPageComponent', () => { { provide: DSpaceObjectDataService, useValue: {} }, { provide: DSONameService, useValue: nameService }, { provide: AuthService, useValue: authService }, + { provide: PaginationService, useValue: { getCurrentPagination: () => observableOf({ currentPage: 1, pageSize: 10 }) } }, ], }) .compileComponents(); diff --git a/src/app/statistics-page/community-statistics-page/community-statistics-page.component.spec.ts b/src/app/statistics-page/community-statistics-page/community-statistics-page.component.spec.ts index e29e37880f4..8ce3eb9cb1c 100644 --- a/src/app/statistics-page/community-statistics-page/community-statistics-page.component.spec.ts +++ b/src/app/statistics-page/community-statistics-page/community-statistics-page.component.spec.ts @@ -16,6 +16,7 @@ import { of as observableOf } from 'rxjs'; import { AuthService } from '../../core/auth/auth.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; +import { PaginationService } from '../../core/pagination/pagination.service'; import { Community } from '../../core/shared/community.model'; import { UsageReport } from '../../core/statistics/models/usage-report.model'; import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; @@ -82,6 +83,7 @@ describe('CommunityStatisticsPageComponent', () => { { provide: DSpaceObjectDataService, useValue: {} }, { provide: DSONameService, useValue: nameService }, { provide: AuthService, useValue: authService }, + { provide: PaginationService, useValue: { getCurrentPagination: () => observableOf({ currentPage: 1, pageSize: 10 }) } }, ], }) .compileComponents(); diff --git a/src/app/statistics-page/item-statistics-page/item-statistics-page.component.spec.ts b/src/app/statistics-page/item-statistics-page/item-statistics-page.component.spec.ts index f5f3361cce1..133e98acfc3 100644 --- a/src/app/statistics-page/item-statistics-page/item-statistics-page.component.spec.ts +++ b/src/app/statistics-page/item-statistics-page/item-statistics-page.component.spec.ts @@ -16,6 +16,7 @@ import { of as observableOf } from 'rxjs'; import { AuthService } from '../../core/auth/auth.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; +import { PaginationService } from '../../core/pagination/pagination.service'; import { Item } from '../../core/shared/item.model'; import { UsageReport } from '../../core/statistics/models/usage-report.model'; import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; @@ -82,6 +83,7 @@ describe('ItemStatisticsPageComponent', () => { { provide: DSpaceObjectDataService, useValue: {} }, { provide: DSONameService, useValue: nameService }, { provide: AuthService, useValue: authService }, + { provide: PaginationService, useValue: { getCurrentPagination: () => observableOf({ currentPage: 1, pageSize: 10 }) } }, ], }) .compileComponents(); diff --git a/src/app/statistics-page/site-statistics-page/site-statistics-page.component.spec.ts b/src/app/statistics-page/site-statistics-page/site-statistics-page.component.spec.ts index 9b23afdd74c..a7c5192fe88 100644 --- a/src/app/statistics-page/site-statistics-page/site-statistics-page.component.spec.ts +++ b/src/app/statistics-page/site-statistics-page/site-statistics-page.component.spec.ts @@ -17,6 +17,7 @@ import { AuthService } from '../../core/auth/auth.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { SiteDataService } from '../../core/data/site-data.service'; +import { PaginationService } from '../../core/pagination/pagination.service'; import { Site } from '../../core/shared/site.model'; import { UsageReport } from '../../core/statistics/models/usage-report.model'; import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; @@ -83,6 +84,7 @@ describe('SiteStatisticsPageComponent', () => { { provide: DSONameService, useValue: nameService }, { provide: SiteDataService, useValue: siteService }, { provide: AuthService, useValue: authService }, + { provide: PaginationService, useValue: { getCurrentPagination: () => observableOf({ currentPage: 1, pageSize: 10 }) } }, ], }) .compileComponents(); diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.html b/src/app/statistics-page/statistics-table/statistics-table.component.html index d6f24bd4371..879678049f2 100644 --- a/src/app/statistics-page/statistics-table/statistics-table.component.html +++ b/src/app/statistics-page/statistics-table/statistics-table.component.html @@ -5,46 +5,39 @@

{{ 'statistics.table.title.' + report.reportType | translate }}

- - - - - - - - - - - - - - - - -
- {{ header }} -
- {{ getLabel(point) | async }} - - {{ point.values[header] }} -
- -
- - -
+ + + + + + + + + + + + + + + + + + +
+ {{ header }} +
+ {{ getLabel(point) | async }} + + {{ point.values[header] }} +
+ +
diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts b/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts index 7673507b294..e00b82478ca 100644 --- a/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts +++ b/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts @@ -1,4 +1,8 @@ -import { DebugElement } from '@angular/core'; +import { + Component, + DebugElement, + Input, +} from '@angular/core'; import { ComponentFixture, TestBed, @@ -6,19 +10,50 @@ import { } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { TranslateModule } from '@ngx-translate/core'; +import { BehaviorSubject } from 'rxjs'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; +import { PaginationService } from '../../core/pagination/pagination.service'; import { UsageReport } from '../../core/statistics/models/usage-report.model'; +import { PaginationComponent } from '../../shared/pagination/pagination.component'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { StatisticsTableComponent } from './statistics-table.component'; +/** + * Lightweight stand-in for ds-pagination so the table can be tested in isolation; the current page is + * driven through the (mocked) PaginationService, exactly as the real component does via the URL. + */ +@Component({ + selector: 'ds-pagination', + standalone: true, + template: '', +}) +class MockPaginationComponent { + @Input() paginationOptions: PaginationComponentOptions; + @Input() collectionSize: number; + @Input() hideSortOptions: boolean; + @Input() retainScrollPosition: boolean; +} + describe('StatisticsTableComponent', () => { let component: StatisticsTableComponent; let de: DebugElement; let fixture: ComponentFixture; + let currentPagination$: BehaviorSubject; + + const paginationService = { + getCurrentPagination: (_id: string, _options: PaginationComponentOptions) => currentPagination$.asObservable(), + }; + + const setPage = (currentPage: number, pageSize = 10) => { + currentPagination$.next(Object.assign(new PaginationComponentOptions(), { currentPage, pageSize })); + }; beforeEach(waitForAsync(() => { + currentPagination$ = new BehaviorSubject(Object.assign(new PaginationComponentOptions(), { currentPage: 1, pageSize: 10 })); + TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), @@ -27,8 +62,13 @@ describe('StatisticsTableComponent', () => { providers: [ { provide: DSpaceObjectDataService, useValue: {} }, { provide: DSONameService, useValue: {} }, + { provide: PaginationService, useValue: paginationService }, ], }) + .overrideComponent(StatisticsTableComponent, { + remove: { imports: [PaginationComponent] }, + add: { imports: [MockPaginationComponent] }, + }) .compileComponents(); })); @@ -51,6 +91,10 @@ describe('StatisticsTableComponent', () => { it ('should not display a table', () => { expect(de.query(By.css('table'))).toBeNull(); }); + + it('should not display a pagination control', () => { + expect(de.query(By.directive(MockPaginationComponent))).toBeNull(); + }); }); describe('when the storage report has data', () => { @@ -97,8 +141,10 @@ describe('StatisticsTableComponent', () => { .toEqual('8'); }); - it('should not display a pagination control when all points fit on a single page', () => { - expect(de.query(By.css('ngb-pagination'))).toBeNull(); + it('should wrap the table in a ds-pagination control with the report size', () => { + const pagination = de.query(By.directive(MockPaginationComponent)); + expect(pagination).toBeTruthy(); + expect(pagination.componentInstance.collectionSize).toEqual(2); }); }); @@ -122,6 +168,11 @@ describe('StatisticsTableComponent', () => { fixture.detectChanges(); }); + it('should pass the full report size to the pagination control', () => { + expect(de.query(By.directive(MockPaginationComponent)).componentInstance.collectionSize) + .toEqual(numberOfPoints); + }); + it('should only render the first page of points', () => { expect(de.queryAll(By.css('[data-test="statistics-label"]')).length) .toEqual(component.pageSize); @@ -129,12 +180,8 @@ describe('StatisticsTableComponent', () => { expect(de.query(By.css('td.item_10-views-data'))).toBeNull(); }); - it('should display a pagination control', () => { - expect(de.query(By.css('ngb-pagination'))).toBeTruthy(); - }); - - it('should render the next page of points when the page changes', () => { - component.onPageChange(2); + it('should render the next page of points when the current page changes', () => { + setPage(2); fixture.detectChanges(); expect(de.query(By.css('td.item_0-views-data'))).toBeNull(); @@ -145,7 +192,7 @@ describe('StatisticsTableComponent', () => { it('should render the remaining points on the last page', () => { const lastPage = Math.ceil(numberOfPoints / component.pageSize); - component.onPageChange(lastPage); + setPage(lastPage); fixture.detectChanges(); const remaining = numberOfPoints - (lastPage - 1) * component.pageSize; diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.ts b/src/app/statistics-page/statistics-table/statistics-table.component.ts index 26ad6d0821f..7c42a2fa18d 100644 --- a/src/app/statistics-page/statistics-table/statistics-table.component.ts +++ b/src/app/statistics-page/statistics-table/statistics-table.component.ts @@ -8,7 +8,6 @@ import { Input, OnInit, } from '@angular/core'; -import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule, TranslateService, @@ -21,6 +20,7 @@ import { map } from 'rxjs/operators'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; +import { PaginationService } from '../../core/pagination/pagination.service'; import { getFinishedRemoteData, getRemoteDataPayload, @@ -30,6 +30,8 @@ import { UsageReport, } from '../../core/statistics/models/usage-report.model'; import { isEmpty } from '../../shared/empty.util'; +import { PaginationComponent } from '../../shared/pagination/pagination.component'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; /** * Component representing a statistics table for a given usage report. @@ -39,7 +41,7 @@ import { isEmpty } from '../../shared/empty.util'; templateUrl: './statistics-table.component.html', styleUrls: ['./statistics-table.component.scss'], standalone: true, - imports: [NgIf, NgFor, AsyncPipe, TranslateModule, NgbPaginationModule], + imports: [NgIf, NgFor, AsyncPipe, TranslateModule, PaginationComponent], }) export class StatisticsTableComponent implements OnInit { @@ -55,11 +57,6 @@ export class StatisticsTableComponent implements OnInit { @Input() pageSize = 10; - /** - * The currently displayed page (1-based, as expected by ngb-pagination). - */ - currentPage = 1; - /** * Boolean indicating whether the usage report has data */ @@ -70,9 +67,20 @@ export class StatisticsTableComponent implements OnInit { */ headers: string[]; + /** + * Configuration for the {@link PaginationComponent} (ds-pagination) used to page through the points. + */ + paginationOptions: PaginationComponentOptions; + + /** + * The points to render for the currently selected page. + */ + paginatedPoints$: Observable; + constructor( protected dsoService: DSpaceObjectDataService, protected nameService: DSONameService, + protected paginationService: PaginationService, private translateService: TranslateService, ) { @@ -83,29 +91,20 @@ export class StatisticsTableComponent implements OnInit { if (this.hasData) { this.headers = Object.keys(this.report.points[0].values); } - } - /** - * The points to render for the currently selected page. - */ - get paginatedPoints(): Point[] { - const start = (this.currentPage - 1) * this.pageSize; - return this.report.points.slice(start, start + this.pageSize); - } - - /** - * Whether a pagination control is needed, i.e. there are more points than fit on a single page. - */ - get showPagination(): boolean { - return this.report.points.length > this.pageSize; - } + this.paginationOptions = Object.assign(new PaginationComponentOptions(), { + // Unique per report so multiple tables on one statistics page paginate independently + id: `stats-${this.report.reportType}`, + pageSize: this.pageSize, + currentPage: 1, + }); - /** - * Switch to the given page. - * @param page the 1-based page number to display - */ - onPageChange(page: number): void { - this.currentPage = page; + this.paginatedPoints$ = this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions).pipe( + map((pagination) => { + const start = (pagination.currentPage - 1) * pagination.pageSize; + return this.report.points.slice(start, start + pagination.pageSize); + }), + ); } /** diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 4ad7e9578ed..04d7384174b 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -4794,8 +4794,6 @@ "statistics.table.no-data": "No data available", - "statistics.table.pagination.label": "Statistics table pagination", - "statistics.table.title.TotalVisits": "Total visits", "statistics.table.title.TotalVisitsPerMonth": "Total visits per month", From 8a67dfef0b2fe696f3e37fcbda662a7587b83229 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Tue, 23 Jun 2026 15:32:13 +0200 Subject: [PATCH 3/6] UoE/Address Copilot review on statistics pagination (#726) - Make the ds-pagination id unique per scope (use report.id, i.e. `_`) so navigating between scopes that share a report type (e.g. TotalVisits) no longer reuses a stale page from the URL. - Hide the page-size gear so pageSize behaves as a fixed, input-driven size. - Hide the pagination detail/bar for single-page reports, so the pagination UI only appears when there are more points than fit on one page. Co-Authored-By: Claude Opus 4.8 --- .../statistics-table/statistics-table.component.html | 2 ++ .../statistics-table/statistics-table.component.spec.ts | 2 ++ .../statistics-table/statistics-table.component.ts | 5 +++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.html b/src/app/statistics-page/statistics-table/statistics-table.component.html index 879678049f2..7124c09eaf8 100644 --- a/src/app/statistics-page/statistics-table/statistics-table.component.html +++ b/src/app/statistics-page/statistics-table/statistics-table.component.html @@ -7,6 +7,8 @@

diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts b/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts index e00b82478ca..c4ee0ea5522 100644 --- a/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts +++ b/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts @@ -32,6 +32,8 @@ import { StatisticsTableComponent } from './statistics-table.component'; class MockPaginationComponent { @Input() paginationOptions: PaginationComponentOptions; @Input() collectionSize: number; + @Input() hideGear: boolean; + @Input() hidePaginationDetail: boolean; @Input() hideSortOptions: boolean; @Input() retainScrollPosition: boolean; } diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.ts b/src/app/statistics-page/statistics-table/statistics-table.component.ts index 7c42a2fa18d..7cb5fd66c98 100644 --- a/src/app/statistics-page/statistics-table/statistics-table.component.ts +++ b/src/app/statistics-page/statistics-table/statistics-table.component.ts @@ -93,8 +93,9 @@ export class StatisticsTableComponent implements OnInit { } this.paginationOptions = Object.assign(new PaginationComponentOptions(), { - // Unique per report so multiple tables on one statistics page paginate independently - id: `stats-${this.report.reportType}`, + // Unique per report AND scope so multiple tables paginate independently and a report doesn't pick up + // another scope's page from the URL. report.id is `_`, e.g. `_TotalVisits`. + id: `stats-${this.report.id}`, pageSize: this.pageSize, currentPage: 1, }); From 4caec3bd4620ff990664a0c5ee941f5e579ace89 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Thu, 25 Jun 2026 11:08:58 +0200 Subject: [PATCH 4/6] UoE/Add results-per-page selector to statistics tables (#726) Re-enable the standard ds-pagination "results per page" selector (gear) so users can choose how many datasets to show per page (10/20/40/60/80/100), reusing the built-in PaginationComponent control. The gear and pagination detail are shown only when a report has more rows than fit on one page; pageSize remains the default. The page slicing already honours the selected size. Co-Authored-By: Claude Opus 4.8 --- .../statistics-table/statistics-table.component.html | 2 +- .../statistics-table.component.spec.ts | 10 ++++++++++ .../statistics-table/statistics-table.component.ts | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.html b/src/app/statistics-page/statistics-table/statistics-table.component.html index 7124c09eaf8..43c2702acdd 100644 --- a/src/app/statistics-page/statistics-table/statistics-table.component.html +++ b/src/app/statistics-page/statistics-table/statistics-table.component.html @@ -7,7 +7,7 @@

diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts b/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts index c4ee0ea5522..82d70c02759 100644 --- a/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts +++ b/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts @@ -148,6 +148,10 @@ describe('StatisticsTableComponent', () => { expect(pagination).toBeTruthy(); expect(pagination.componentInstance.collectionSize).toEqual(2); }); + + it('should hide the page-size selector when everything fits on a single page', () => { + expect(de.query(By.directive(MockPaginationComponent)).componentInstance.hideGear).toBeTrue(); + }); }); describe('when the report has more points than the page size', () => { @@ -175,6 +179,12 @@ describe('StatisticsTableComponent', () => { .toEqual(numberOfPoints); }); + it('should offer the page-size selector with its options when there is more than one page', () => { + const pagination = de.query(By.directive(MockPaginationComponent)).componentInstance; + expect(pagination.hideGear).toBeFalse(); + expect(pagination.paginationOptions.pageSizeOptions).toEqual([10, 20, 40, 60, 80, 100]); + }); + it('should only render the first page of points', () => { expect(de.queryAll(By.css('[data-test="statistics-label"]')).length) .toEqual(component.pageSize); diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.ts b/src/app/statistics-page/statistics-table/statistics-table.component.ts index 7cb5fd66c98..cb5f1c7fe12 100644 --- a/src/app/statistics-page/statistics-table/statistics-table.component.ts +++ b/src/app/statistics-page/statistics-table/statistics-table.component.ts @@ -96,7 +96,9 @@ export class StatisticsTableComponent implements OnInit { // Unique per report AND scope so multiple tables paginate independently and a report doesn't pick up // another scope's page from the URL. report.id is `_`, e.g. `_TotalVisits`. id: `stats-${this.report.id}`, + // pageSize is the default; users can change it via the ds-pagination "results per page" selector. pageSize: this.pageSize, + pageSizeOptions: [10, 20, 40, 60, 80, 100], currentPage: 1, }); From 499603467201628512bb5395683ac07fcb62b75c Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Thu, 25 Jun 2026 11:12:03 +0200 Subject: [PATCH 5/6] UoE/Test that a larger selected page size renders more rows (#726) Co-Authored-By: Claude Opus 4.8 --- .../statistics-table/statistics-table.component.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts b/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts index 82d70c02759..d8e010ecc05 100644 --- a/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts +++ b/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts @@ -185,6 +185,14 @@ describe('StatisticsTableComponent', () => { expect(pagination.paginationOptions.pageSizeOptions).toEqual([10, 20, 40, 60, 80, 100]); }); + it('should render more rows when a larger page size is selected', () => { + setPage(1, 20); + fixture.detectChanges(); + + expect(de.queryAll(By.css('[data-test="statistics-label"]')).length).toEqual(20); + expect(de.query(By.css('td.item_19-views-data'))).toBeTruthy(); + }); + it('should only render the first page of points', () => { expect(de.queryAll(By.css('[data-test="statistics-label"]')).length) .toEqual(component.pageSize); From 46d8a786714b6a5771ae7b4d8f7326d4335c70e4 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Thu, 25 Jun 2026 16:26:21 +0200 Subject: [PATCH 6/6] UoE/Add spacing below statistics pagination bar so the gear isn't glued to the table (#726) Co-Authored-By: Claude Opus 4.8 --- .../statistics-table/statistics-table.component.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.scss b/src/app/statistics-page/statistics-table/statistics-table.component.scss index 4e173c040ab..e3e68b5fc46 100644 --- a/src/app/statistics-page/statistics-table/statistics-table.component.scss +++ b/src/app/statistics-page/statistics-table/statistics-table.component.scss @@ -2,6 +2,12 @@ th, td { padding: 0.5rem; } +// Space below the pagination top bar (results-per-page gear + "showing" detail) +// so it isn't glued to the table above it. +:host ::ng-deep ds-pagination .pagination-masked.top { + margin-bottom: 0.75rem; +} + td { width: 50px; max-width: 50px;