Skip to content

Commit 9fe5e6a

Browse files
UoE/Add pagination to statistics tables (DSpace#726)
UoE/Add pagination to statistics tables (DSpace#726)
2 parents 700d7c5 + 46d8a78 commit 9fe5e6a

8 files changed

Lines changed: 213 additions & 29 deletions

File tree

src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { of as observableOf } from 'rxjs';
1616
import { AuthService } from '../../core/auth/auth.service';
1717
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
1818
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
19+
import { PaginationService } from '../../core/pagination/pagination.service';
1920
import { Collection } from '../../core/shared/collection.model';
2021
import { UsageReport } from '../../core/statistics/models/usage-report.model';
2122
import { UsageReportDataService } from '../../core/statistics/usage-report-data.service';
@@ -82,6 +83,7 @@ describe('CollectionStatisticsPageComponent', () => {
8283
{ provide: DSpaceObjectDataService, useValue: {} },
8384
{ provide: DSONameService, useValue: nameService },
8485
{ provide: AuthService, useValue: authService },
86+
{ provide: PaginationService, useValue: { getCurrentPagination: () => observableOf({ currentPage: 1, pageSize: 10 }) } },
8587
],
8688
})
8789
.compileComponents();

src/app/statistics-page/community-statistics-page/community-statistics-page.component.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { of as observableOf } from 'rxjs';
1616
import { AuthService } from '../../core/auth/auth.service';
1717
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
1818
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
19+
import { PaginationService } from '../../core/pagination/pagination.service';
1920
import { Community } from '../../core/shared/community.model';
2021
import { UsageReport } from '../../core/statistics/models/usage-report.model';
2122
import { UsageReportDataService } from '../../core/statistics/usage-report-data.service';
@@ -82,6 +83,7 @@ describe('CommunityStatisticsPageComponent', () => {
8283
{ provide: DSpaceObjectDataService, useValue: {} },
8384
{ provide: DSONameService, useValue: nameService },
8485
{ provide: AuthService, useValue: authService },
86+
{ provide: PaginationService, useValue: { getCurrentPagination: () => observableOf({ currentPage: 1, pageSize: 10 }) } },
8587
],
8688
})
8789
.compileComponents();

src/app/statistics-page/item-statistics-page/item-statistics-page.component.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { of as observableOf } from 'rxjs';
1616
import { AuthService } from '../../core/auth/auth.service';
1717
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
1818
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
19+
import { PaginationService } from '../../core/pagination/pagination.service';
1920
import { Item } from '../../core/shared/item.model';
2021
import { UsageReport } from '../../core/statistics/models/usage-report.model';
2122
import { UsageReportDataService } from '../../core/statistics/usage-report-data.service';
@@ -82,6 +83,7 @@ describe('ItemStatisticsPageComponent', () => {
8283
{ provide: DSpaceObjectDataService, useValue: {} },
8384
{ provide: DSONameService, useValue: nameService },
8485
{ provide: AuthService, useValue: authService },
86+
{ provide: PaginationService, useValue: { getCurrentPagination: () => observableOf({ currentPage: 1, pageSize: 10 }) } },
8587
],
8688
})
8789
.compileComponents();

src/app/statistics-page/site-statistics-page/site-statistics-page.component.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { AuthService } from '../../core/auth/auth.service';
1717
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
1818
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
1919
import { SiteDataService } from '../../core/data/site-data.service';
20+
import { PaginationService } from '../../core/pagination/pagination.service';
2021
import { Site } from '../../core/shared/site.model';
2122
import { UsageReport } from '../../core/statistics/models/usage-report.model';
2223
import { UsageReportDataService } from '../../core/statistics/usage-report-data.service';
@@ -83,6 +84,7 @@ describe('SiteStatisticsPageComponent', () => {
8384
{ provide: DSONameService, useValue: nameService },
8485
{ provide: SiteDataService, useValue: siteService },
8586
{ provide: AuthService, useValue: authService },
87+
{ provide: PaginationService, useValue: { getCurrentPagination: () => observableOf({ currentPage: 1, pageSize: 10 }) } },
8688
],
8789
})
8890
.compileComponents();

src/app/statistics-page/statistics-table/statistics-table.component.html

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,41 @@ <h2 class="m-1">
55
{{ 'statistics.table.title.' + report.reportType | translate }}
66
</h2>
77

8-
<table class="table table-striped" [attr.data-test]="report.reportType">
9-
10-
<tbody>
11-
12-
<tr>
13-
<td></td>
14-
<th scope="col"
15-
*ngFor="let header of headers"
16-
class="{{header}}-header">
17-
{{ header }}
18-
</th>
19-
</tr>
20-
21-
<tr *ngFor="let point of report.points"
22-
class="{{point.id}}-data">
23-
<th scope="row" data-test="statistics-label">
24-
{{ getLabel(point) | async }}
25-
</th>
26-
<td *ngFor="let header of headers"
27-
class="{{point.id}}-{{header}}-data">
28-
{{ point.values[header] }}
29-
</td>
30-
</tr>
31-
32-
</tbody>
33-
34-
</table>
8+
<ds-pagination [paginationOptions]="paginationOptions"
9+
[collectionSize]="report.points.length"
10+
[hideGear]="report.points.length <= pageSize"
11+
[hidePaginationDetail]="report.points.length <= pageSize"
12+
[hideSortOptions]="true"
13+
[retainScrollPosition]="true">
14+
15+
<table class="table table-striped" [attr.data-test]="report.reportType">
16+
17+
<tbody>
18+
19+
<tr>
20+
<td></td>
21+
<th scope="col"
22+
*ngFor="let header of headers"
23+
class="{{header}}-header">
24+
{{ header }}
25+
</th>
26+
</tr>
27+
28+
<tr *ngFor="let point of (paginatedPoints$ | async)"
29+
class="{{point.id}}-data">
30+
<th scope="row" data-test="statistics-label">
31+
{{ getLabel(point) | async }}
32+
</th>
33+
<td *ngFor="let header of headers"
34+
class="{{point.id}}-{{header}}-data">
35+
{{ point.values[header] }}
36+
</td>
37+
</tr>
38+
39+
</tbody>
40+
41+
</table>
42+
43+
</ds-pagination>
3544

3645
</div>

src/app/statistics-page/statistics-table/statistics-table.component.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ th, td {
22
padding: 0.5rem;
33
}
44

5+
// Space below the pagination top bar (results-per-page gear + "showing" detail)
6+
// so it isn't glued to the table above it.
7+
:host ::ng-deep ds-pagination .pagination-masked.top {
8+
margin-bottom: 0.75rem;
9+
}
10+
511
td {
612
width: 50px;
713
max-width: 50px;

src/app/statistics-page/statistics-table/statistics-table.component.spec.ts

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,61 @@
1-
import { DebugElement } from '@angular/core';
1+
import {
2+
Component,
3+
DebugElement,
4+
Input,
5+
} from '@angular/core';
26
import {
37
ComponentFixture,
48
TestBed,
59
waitForAsync,
610
} from '@angular/core/testing';
711
import { By } from '@angular/platform-browser';
812
import { TranslateModule } from '@ngx-translate/core';
13+
import { BehaviorSubject } from 'rxjs';
914

1015
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
1116
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
17+
import { PaginationService } from '../../core/pagination/pagination.service';
1218
import { UsageReport } from '../../core/statistics/models/usage-report.model';
19+
import { PaginationComponent } from '../../shared/pagination/pagination.component';
20+
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
1321
import { StatisticsTableComponent } from './statistics-table.component';
1422

23+
/**
24+
* Lightweight stand-in for ds-pagination so the table can be tested in isolation; the current page is
25+
* driven through the (mocked) PaginationService, exactly as the real component does via the URL.
26+
*/
27+
@Component({
28+
selector: 'ds-pagination',
29+
standalone: true,
30+
template: '<ng-content></ng-content>',
31+
})
32+
class MockPaginationComponent {
33+
@Input() paginationOptions: PaginationComponentOptions;
34+
@Input() collectionSize: number;
35+
@Input() hideGear: boolean;
36+
@Input() hidePaginationDetail: boolean;
37+
@Input() hideSortOptions: boolean;
38+
@Input() retainScrollPosition: boolean;
39+
}
40+
1541
describe('StatisticsTableComponent', () => {
1642

1743
let component: StatisticsTableComponent;
1844
let de: DebugElement;
1945
let fixture: ComponentFixture<StatisticsTableComponent>;
46+
let currentPagination$: BehaviorSubject<PaginationComponentOptions>;
47+
48+
const paginationService = {
49+
getCurrentPagination: (_id: string, _options: PaginationComponentOptions) => currentPagination$.asObservable(),
50+
};
51+
52+
const setPage = (currentPage: number, pageSize = 10) => {
53+
currentPagination$.next(Object.assign(new PaginationComponentOptions(), { currentPage, pageSize }));
54+
};
2055

2156
beforeEach(waitForAsync(() => {
57+
currentPagination$ = new BehaviorSubject(Object.assign(new PaginationComponentOptions(), { currentPage: 1, pageSize: 10 }));
58+
2259
TestBed.configureTestingModule({
2360
imports: [
2461
TranslateModule.forRoot(),
@@ -27,8 +64,13 @@ describe('StatisticsTableComponent', () => {
2764
providers: [
2865
{ provide: DSpaceObjectDataService, useValue: {} },
2966
{ provide: DSONameService, useValue: {} },
67+
{ provide: PaginationService, useValue: paginationService },
3068
],
3169
})
70+
.overrideComponent(StatisticsTableComponent, {
71+
remove: { imports: [PaginationComponent] },
72+
add: { imports: [MockPaginationComponent] },
73+
})
3274
.compileComponents();
3375
}));
3476

@@ -51,6 +93,10 @@ describe('StatisticsTableComponent', () => {
5193
it ('should not display a table', () => {
5294
expect(de.query(By.css('table'))).toBeNull();
5395
});
96+
97+
it('should not display a pagination control', () => {
98+
expect(de.query(By.directive(MockPaginationComponent))).toBeNull();
99+
});
54100
});
55101

56102
describe('when the storage report has data', () => {
@@ -96,5 +142,83 @@ describe('StatisticsTableComponent', () => {
96142
expect(de.query(By.css('td.item_2-downloads-data')).nativeElement.innerText)
97143
.toEqual('8');
98144
});
145+
146+
it('should wrap the table in a ds-pagination control with the report size', () => {
147+
const pagination = de.query(By.directive(MockPaginationComponent));
148+
expect(pagination).toBeTruthy();
149+
expect(pagination.componentInstance.collectionSize).toEqual(2);
150+
});
151+
152+
it('should hide the page-size selector when everything fits on a single page', () => {
153+
expect(de.query(By.directive(MockPaginationComponent)).componentInstance.hideGear).toBeTrue();
154+
});
155+
});
156+
157+
describe('when the report has more points than the page size', () => {
158+
159+
const numberOfPoints = 25;
160+
161+
beforeEach(() => {
162+
const points = [];
163+
for (let i = 0; i < numberOfPoints; i++) {
164+
points.push({
165+
id: `item_${i}`,
166+
label: `item_${i}`,
167+
values: {
168+
views: i,
169+
},
170+
});
171+
}
172+
component.report = Object.assign(new UsageReport(), { points });
173+
component.ngOnInit();
174+
fixture.detectChanges();
175+
});
176+
177+
it('should pass the full report size to the pagination control', () => {
178+
expect(de.query(By.directive(MockPaginationComponent)).componentInstance.collectionSize)
179+
.toEqual(numberOfPoints);
180+
});
181+
182+
it('should offer the page-size selector with its options when there is more than one page', () => {
183+
const pagination = de.query(By.directive(MockPaginationComponent)).componentInstance;
184+
expect(pagination.hideGear).toBeFalse();
185+
expect(pagination.paginationOptions.pageSizeOptions).toEqual([10, 20, 40, 60, 80, 100]);
186+
});
187+
188+
it('should render more rows when a larger page size is selected', () => {
189+
setPage(1, 20);
190+
fixture.detectChanges();
191+
192+
expect(de.queryAll(By.css('[data-test="statistics-label"]')).length).toEqual(20);
193+
expect(de.query(By.css('td.item_19-views-data'))).toBeTruthy();
194+
});
195+
196+
it('should only render the first page of points', () => {
197+
expect(de.queryAll(By.css('[data-test="statistics-label"]')).length)
198+
.toEqual(component.pageSize);
199+
expect(de.query(By.css('td.item_0-views-data'))).toBeTruthy();
200+
expect(de.query(By.css('td.item_10-views-data'))).toBeNull();
201+
});
202+
203+
it('should render the next page of points when the current page changes', () => {
204+
setPage(2);
205+
fixture.detectChanges();
206+
207+
expect(de.query(By.css('td.item_0-views-data'))).toBeNull();
208+
expect(de.query(By.css('td.item_10-views-data'))).toBeTruthy();
209+
expect(de.queryAll(By.css('[data-test="statistics-label"]')).length)
210+
.toEqual(component.pageSize);
211+
});
212+
213+
it('should render the remaining points on the last page', () => {
214+
const lastPage = Math.ceil(numberOfPoints / component.pageSize);
215+
setPage(lastPage);
216+
fixture.detectChanges();
217+
218+
const remaining = numberOfPoints - (lastPage - 1) * component.pageSize;
219+
expect(de.queryAll(By.css('[data-test="statistics-label"]')).length)
220+
.toEqual(remaining);
221+
expect(de.query(By.css(`td.item_${numberOfPoints - 1}-views-data`))).toBeTruthy();
222+
});
99223
});
100224
});

0 commit comments

Comments
 (0)