From a9d61e5d6ef241f9b5f45a6b5f2ef23123cf5a4a Mon Sep 17 00:00:00 2001 From: Denys Vuika Date: Mon, 27 Nov 2017 10:14:08 +0000 Subject: [PATCH] [ADF-2016] pagination integration for document list (#2718) * pagination integration for document list * reload data only if needed * unit tests for document list * pagination tests * update docs --- .../app/components/files/files.component.html | 5 +- .../app/components/files/files.component.ts | 13 -- docs/pagination.component.md | 31 +++- .../document-list.component.spec.ts | 43 +++++ .../components/document-list.component.ts | 47 ++++- .../paginated-component.interface.ts | 28 +++ .../pagination/pagination.component.spec.ts | 167 +++++++++++------- lib/core/pagination/pagination.component.ts | 20 ++- lib/core/pagination/public-api.ts | 2 + 9 files changed, 263 insertions(+), 93 deletions(-) create mode 100644 lib/core/pagination/paginated-component.interface.ts diff --git a/demo-shell/src/app/components/files/files.component.html b/demo-shell/src/app/components/files/files.component.html index 6052ba00f1..23384c1c9e 100644 --- a/demo-shell/src/app/components/files/files.component.html +++ b/demo-shell/src/app/components/files/files.component.html @@ -116,10 +116,9 @@ + { @@ -446,32 +443,22 @@ export class FilesComponent implements OnInit, OnChanges, OnDestroy { onChangePageSize(event: Pagination): void { this.preference.paginationSize = event.maxItems; - this.currentMaxItems = event.maxItems; - this.currentSkipCount = event.skipCount; this.changedPageSize.emit(event); } onChangePageNumber(event: Pagination): void { - this.currentMaxItems = event.maxItems; - this.currentSkipCount = event.skipCount; this.changedPageNumber.emit(event); } onNextPage(event: Pagination): void { - this.currentMaxItems = event.maxItems; - this.currentSkipCount = event.skipCount; this.turnedNextPage.emit(event); } loadNextBatch(event: Pagination) { - this.currentMaxItems = event.maxItems; - this.currentSkipCount = event.skipCount; this.loadNext.emit(event); } onPrevPage(event: Pagination): void { - this.currentMaxItems = event.maxItems; - this.currentSkipCount = event.skipCount; this.turnedPreviousPage.emit(event); } } diff --git a/docs/pagination.component.md b/docs/pagination.component.md index 9e3c5976fe..290c778f5a 100644 --- a/docs/pagination.component.md +++ b/docs/pagination.component.md @@ -31,12 +31,22 @@ Adds pagination to the component it is used with. ``` +## Integrating with Document List + +```html + + + + +``` + ### Properties | Name | Type | Default | Description | | --- | --- | --- | --- | | pagination | Pagination | | Pagination object | | supportedPageSizes | Array<number> | [ 25, 50, 100 ] | An array of page sizes | +| target | PaginatedComponent | | Component that provides custom pagination support | ### Events @@ -54,6 +64,23 @@ The pagination object is a generic component to paginate component. The Alfresco Each event helps to detect the certain action that user have made using the component. -For `change` event, a [PaginationQueryParams](https://github.com/Alfresco/alfresco-ng2-components/blob/development/ng2-components/ng2-alfresco-core/src/components/pagination/pagination-query-params.interface.ts) (including the query params supported by the REST API, `skipCount` and `maxItems`) is returned. +For `change` event, a [PaginationQueryParams](https://github.com/Alfresco/alfresco-ng2-components/blob/development/ng2-components/ng2-alfresco-core/src/components/pagination/pagination-query-params.interface.ts) (including the query parameters supported by the REST API, `skipCount` and `maxItems`) is returned. -For all events other than `change`, a new Pagination object is returned as in the folowing example, with updated properties to be used to query further. +For all events other than `change`, a new Pagination object is returned as in the following example, with updated properties to be used to query further. + +### Custom pagination + +The component also provides light integration with external implementations of the pagination. +Any component can implement the `PaginatedComponent` and be used as a value for the `target` property. + +```js +export interface PaginatedComponent { + + pagination: Subject; + updatePagination(params: PaginationQueryParams); + +} +``` + +Your component needs to provide a `pagination` subject to allow Pagination component to reflect to changes. +Every time user interacts with the Pagination, it will call the `updatePagination` method and pass the parameters. \ No newline at end of file diff --git a/lib/content-services/document-list/components/document-list.component.spec.ts b/lib/content-services/document-list/components/document-list.component.spec.ts index cae800bec8..1cb7220c80 100644 --- a/lib/content-services/document-list/components/document-list.component.spec.ts +++ b/lib/content-services/document-list/components/document-list.component.spec.ts @@ -1103,4 +1103,47 @@ describe('DocumentList', () => { expect(documentList.folderNode).toBeNull(); }); + + it('should update pagination settings', () => { + spyOn(documentList, 'reload').and.stub(); + + documentList.maxItems = 0; + documentList.skipCount = 0; + + documentList.updatePagination({ + maxItems: 10, + skipCount: 10 + }); + + expect(documentList.maxItems).toBe(10); + expect(documentList.skipCount).toBe(10); + }); + + it('should reload data upon changing pagination settings', () => { + spyOn(documentList, 'reload').and.stub(); + + documentList.maxItems = 0; + documentList.skipCount = 0; + + documentList.updatePagination({ + maxItems: 10, + skipCount: 10 + }); + + expect(documentList.reload).toHaveBeenCalled(); + }); + + it('should not reload data if pagination settings are same', () => { + spyOn(documentList, 'reload').and.stub(); + + documentList.maxItems = 10; + documentList.skipCount = 10; + + documentList.updatePagination({ + maxItems: 10, + skipCount: 10 + }); + + expect(documentList.reload).not.toHaveBeenCalled(); + }); }); diff --git a/lib/content-services/document-list/components/document-list.component.ts b/lib/content-services/document-list/components/document-list.component.ts index 2dd6adafeb..ed6f9f3145 100644 --- a/lib/content-services/document-list/components/document-list.component.ts +++ b/lib/content-services/document-list/components/document-list.component.ts @@ -21,7 +21,9 @@ import { DataRowActionEvent, DataSorting, DataTableComponent, - ObjectDataColumn + ObjectDataColumn, + PaginatedComponent, + PaginationQueryParams } from '@alfresco/adf-core'; import { AlfrescoApiService, AppConfigService, DataColumnListComponent, UserPreferencesService } from '@alfresco/adf-core'; import { @@ -34,7 +36,8 @@ import { MinimalNodeEntryEntity, NodePaging, PersonEntry, - SitePaging + SitePaging, + Pagination } from 'alfresco-js-api'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; @@ -58,7 +61,7 @@ export enum PaginationStrategy { templateUrl: './document-list.component.html', encapsulation: ViewEncapsulation.None }) -export class DocumentListComponent implements OnInit, OnChanges, AfterContentInit { +export class DocumentListComponent implements OnInit, OnChanges, AfterContentInit, PaginatedComponent { static SINGLE_CLICK_NAVIGATION: string = 'click'; static DOUBLE_CLICK_NAVIGATION: string = 'dblclick'; @@ -169,6 +172,7 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni infiniteLoading: boolean = false; noPermission: boolean = false; selection = new Array(); + pagination = new Subject(); private layoutPresets = {}; private currentNodeAllowableOperations: string[] = []; @@ -180,7 +184,14 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni private apiService: AlfrescoApiService, private appConfig: AppConfigService, private preferences: UserPreferencesService) { - this.maxItems = this.preferences.paginationSize; + this.maxItems = this.preferences.paginationSize; + + this.pagination.next( { + maxItems: this.preferences.paginationSize, + skipCount: 0, + totalItems: 0, + hasMoreItems: false + }); } getContextActions(node: MinimalNodeEntity) { @@ -287,7 +298,7 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni this.loadFolderByNodeId(this.currentFolderId, merge); } else if (this.node) { this.data.loadPage(this.node); - this.ready.emit(this.node); + this.onDataReady(this.node); } }); } @@ -487,7 +498,7 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni this.data.loadPage( val, merge); this.loading = false; this.infiniteLoading = false; - this.ready.emit(val); + this.onDataReady(val); resolve(true); }, error => { @@ -643,7 +654,7 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni if (page) { this.data.loadPage(page, merge); this.loading = false; - this.ready.emit(page); + this.onDataReady(page); } } @@ -830,10 +841,30 @@ export class DocumentListComponent implements OnInit, OnChanges, AfterContentIni } else { this.layoutPresets = presetsDefaultModel; } - } private getLayoutPreset(name: string = 'default'): DataColumn[] { return (this.layoutPresets[name] || this.layoutPresets['default']).map(col => new ObjectDataColumn(col)); } + + private onDataReady(page: NodePaging) { + this.ready.emit(page); + + if (page && page.list && page.list.pagination) { + this.pagination.next(page.list.pagination); + } else { + this.pagination.next(null); + } + } + + updatePagination(params: PaginationQueryParams) { + const needsReload = this.maxItems !== params.maxItems || this.skipCount !== params.skipCount; + + this.maxItems = params.maxItems; + this.skipCount = params.skipCount; + + if (needsReload) { + this.reload(this.enableInfiniteScrolling); + } + } } diff --git a/lib/core/pagination/paginated-component.interface.ts b/lib/core/pagination/paginated-component.interface.ts new file mode 100644 index 0000000000..ee81965c23 --- /dev/null +++ b/lib/core/pagination/paginated-component.interface.ts @@ -0,0 +1,28 @@ +/*! + * @license + * Copyright 2016 Alfresco Software, Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Pagination } from 'alfresco-js-api'; +import { Subject } from 'rxjs/Subject'; + +import { PaginationQueryParams } from './pagination-query-params.interface'; + +export interface PaginatedComponent { + + pagination: Subject; + updatePagination(params: PaginationQueryParams); + +} diff --git a/lib/core/pagination/pagination.component.spec.ts b/lib/core/pagination/pagination.component.spec.ts index 8848ba8681..ed2a6b3737 100644 --- a/lib/core/pagination/pagination.component.spec.ts +++ b/lib/core/pagination/pagination.component.spec.ts @@ -19,18 +19,19 @@ import { HttpClientModule } from '@angular/common/http'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { Pagination } from 'alfresco-js-api'; import { MaterialModule } from '../material.module'; import { AppConfigService } from '../app-config/app-config.service'; import { LogService } from '../services/log.service'; import { TranslateLoaderService } from '../services/translate-loader.service'; import { TranslationService } from '../services/translation.service'; import { PaginationComponent } from './pagination.component'; +import { PaginatedComponent } from './public-api'; +import { Subject } from 'rxjs/Subject'; -declare let jasmine: any; - -class FakePaginationInput { - count: string = 'Not applicable / not used'; - hasMoreItems: string = 'Not applicable / not used'; +class FakePaginationInput implements Pagination { + count: number; + hasMoreItems: boolean; totalItems: number = null; skipCount: number = null; maxItems: number = 25; @@ -44,14 +45,12 @@ class FakePaginationInput { describe('PaginationComponent', () => { let fixture: ComponentFixture; + let component: PaginationComponent; - beforeEach(() => { - jasmine.Ajax.install(); - }); - - afterEach(() => { - jasmine.Ajax.uninstall(); - }); + let changePageNumberSpy: jasmine.Spy; + let changePageSizeSpy: jasmine.Spy; + let nextPageSpy: jasmine.Spy; + let prevPageSpy: jasmine.Spy; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -77,19 +76,16 @@ describe('PaginationComponent', () => { }).compileComponents() .then(() => { fixture = TestBed.createComponent(PaginationComponent); - const component: PaginationComponent = fixture.componentInstance; + component = fixture.componentInstance; ( component).ngAfterViewInit = jasmine .createSpy('ngAfterViewInit').and .callThrough(); - spyOn(component.changePageNumber, 'emit'); - spyOn(component.changePageSize, 'emit'); - spyOn(component.nextPage, 'emit'); - spyOn(component.prevPage, 'emit'); - - this.fixture = fixture; - this.component = component; + changePageNumberSpy = spyOn(component.changePageNumber, 'emit'); + changePageSizeSpy = spyOn(component.changePageSize, 'emit'); + nextPageSpy = spyOn(component.nextPage, 'emit'); + prevPageSpy = spyOn(component.prevPage, 'emit'); fixture.detectChanges(); }); @@ -97,38 +93,38 @@ describe('PaginationComponent', () => { describe('Single page', () => { beforeEach(() => { - this.component.pagination = new FakePaginationInput(1, 1, 10); + component.pagination = new FakePaginationInput(1, 1, 10); }); it('has a single page', () => { - expect(this.component.pages.length).toBe(1); + expect(component.pages.length).toBe(1); }); it('has current page 1', () => { - expect(this.component.current).toBe(1); + expect(component.current).toBe(1); }); it('is first and last page', () => { - expect(this.component.isFirstPage).toBe(true); - expect(this.component.isLastPage).toBe(true); + expect(component.isFirstPage).toBe(true); + expect(component.isLastPage).toBe(true); }); it('has range', () => { - expect(this.component.range).toEqual([ 1, 10 ]); + expect(component.range).toEqual([ 1, 10 ]); }); }); describe('Single full page', () => { beforeEach(() => { - this.component.pagination = new FakePaginationInput(1, 1, 25); + component.pagination = new FakePaginationInput(1, 1, 25); }); it('has a single page', () => { - expect(this.component.pages.length).toBe(1); + expect(component.pages.length).toBe(1); }); it('has range', () => { - expect(this.component.range).toEqual([ 1, 25 ]); + expect(component.range).toEqual([ 1, 25 ]); }); }); @@ -138,74 +134,63 @@ describe('PaginationComponent', () => { // and last page has 5 items beforeEach(() => { - this.component.pagination = new FakePaginationInput(6, 3, 5); + component.pagination = new FakePaginationInput(6, 3, 5); }); it('has more pages', () => { - expect(this.component.pages.length).toBe(6); + expect(component.pages.length).toBe(6); }); it('has the last page', () => { - expect(this.component.lastPage).toBe(6); + expect(component.lastPage).toBe(6); }); it('is on the 3rd page', () => { - expect(this.component.current).toBe(3); + expect(component.current).toBe(3); }); it('has previous and next page', () => { - expect(this.component.previous).toBe(2); - expect(this.component.next).toBe(4); + expect(component.previous).toBe(2); + expect(component.next).toBe(4); }); it('is not first, nor last', () => { - expect(this.component.isFirstPage).toBe(false); - expect(this.component.isLastPage).toBe(false); + expect(component.isFirstPage).toBe(false); + expect(component.isLastPage).toBe(false); }); it('has range', () => { - expect(this.component.range).toEqual([ 51, 75 ]); + expect(component.range).toEqual([ 51, 75 ]); }); it('goes next', () => { - const { component } = this; - component.goNext(); - const { emit: { calls } } = component.nextPage; - const { skipCount } = calls.mostRecent().args[0]; + const { skipCount } = nextPageSpy.calls.mostRecent().args[0]; expect(skipCount).toBe(75); }); it('goes previous', () => { - const { component } = this; - component.goPrevious(); - const { emit: { calls } } = component.prevPage; - const { skipCount } = calls.mostRecent().args[0]; + const { skipCount } = prevPageSpy.calls.mostRecent().args[0]; expect(skipCount).toBe(25); }); it('changes page size', () => { - const { component } = this; component.onChangePageSize(50); - const { emit: { calls } } = component.changePageSize; - const { maxItems } = calls.mostRecent().args[0]; + const { maxItems } = changePageSizeSpy.calls.mostRecent().args[0]; expect(maxItems).toBe(50); }); it('changes page number', () => { - const { component } = this; - component.onChangePageNumber(5); - const { emit: { calls } } = component.changePageNumber; - const { skipCount } = calls.mostRecent().args[0]; + const { skipCount } = changePageNumberSpy.calls.mostRecent().args[0]; expect(skipCount).toBe(100); }); @@ -216,24 +201,24 @@ describe('PaginationComponent', () => { // This test describes 10 pages being on the first page beforeEach(() => { - this.component.pagination = new FakePaginationInput(10, 1, 5); + component.pagination = new FakePaginationInput(10, 1, 5); }); it('is on the first page', () => { - expect(this.component.current).toBe(1); - expect(this.component.isFirstPage).toBe(true); + expect(component.current).toBe(1); + expect(component.isFirstPage).toBe(true); }); it('has the same, previous page', () => { - expect(this.component.previous).toBe(1); + expect(component.previous).toBe(1); }); it('has next page', () => { - expect(this.component.next).toBe(2); + expect(component.next).toBe(2); }); it('has range', () => { - expect(this.component.range).toEqual([ 1, 25 ]); + expect(component.range).toEqual([ 1, 25 ]); }); }); @@ -242,24 +227,24 @@ describe('PaginationComponent', () => { // This test describes 10 pages being on the last page beforeEach(() => { - this.component.pagination = new FakePaginationInput(10, 10, 5); + component.pagination = new FakePaginationInput(10, 10, 5); }); it('is on the last page', () => { - expect(this.component.current).toBe(10); - expect(this.component.isLastPage).toBe(true); + expect(component.current).toBe(10); + expect(component.isLastPage).toBe(true); }); it('has the same, next page', () => { - expect(this.component.next).toBe(10); + expect(component.next).toBe(10); }); it('has previous page', () => { - expect(this.component.previous).toBe(9); + expect(component.previous).toBe(9); }); it('has range', () => { - expect(this.component.range).toEqual([ 226, 230 ]); + expect(component.range).toEqual([ 226, 230 ]); }); }); @@ -268,7 +253,7 @@ describe('PaginationComponent', () => { const { current, lastPage, isFirstPage, isLastPage, next, previous, range, pages - } = this.component; + } = component; expect(lastPage).toBe(1, 'lastPage'); expect(previous).toBe(1, 'previous'); @@ -282,4 +267,54 @@ describe('PaginationComponent', () => { expect(pages).toEqual([ 1 ], 'pages'); }); }); + + describe('with paginated component', () => { + + it('should take pagination from the external component', () => { + const pagination: Pagination = {}; + + const customComponent = { + pagination: new Subject() + }; + + component.target = customComponent; + component.ngOnInit(); + + customComponent.pagination.next(pagination); + expect(component.pagination).toBe(pagination); + }); + + it('should update pagination by subscription', () => { + const pagination1: Pagination = {}; + const pagination2: Pagination = {}; + + const customComponent = { + pagination: new Subject() + }; + + component.target = customComponent; + component.ngOnInit(); + + customComponent.pagination.next(pagination1); + expect(component.pagination).toBe(pagination1); + + customComponent.pagination.next(pagination2); + expect(component.pagination).toBe(pagination2); + }); + + it('should send pagination event to paginated component', () => { + const customComponent = { + pagination: new Subject(), + updatePagination() {} + }; + spyOn(customComponent, 'updatePagination').and.stub(); + + component.target = customComponent; + component.ngOnInit(); + + component.goNext(); + expect(customComponent.updatePagination).toHaveBeenCalled(); + }); + + }); }); diff --git a/lib/core/pagination/pagination.component.ts b/lib/core/pagination/pagination.component.ts index 8989742fb6..d063c3644e 100644 --- a/lib/core/pagination/pagination.component.ts +++ b/lib/core/pagination/pagination.component.ts @@ -22,11 +22,13 @@ import { Input, OnInit, Output, - ViewEncapsulation + ViewEncapsulation, + ChangeDetectorRef } from '@angular/core'; import { Pagination } from 'alfresco-js-api'; import { PaginationQueryParams } from './pagination-query-params.interface'; +import { PaginatedComponent } from './paginated-component.interface'; @Component({ selector: 'adf-pagination', @@ -53,6 +55,9 @@ export class PaginationComponent implements OnInit { CHANGE_PAGE_NUMBER: 'CHANGE_PAGE_NUMBER' }; + @Input() + target: PaginatedComponent; + @Input() supportedPageSizes: number[] = [ 25, 50, 100 ]; @@ -74,7 +79,16 @@ export class PaginationComponent implements OnInit { @Output() prevPage: EventEmitter = new EventEmitter(); + constructor(private cdr: ChangeDetectorRef) { + } + ngOnInit() { + if (this.target) { + this.target.pagination.subscribe(page => { + this.pagination = page; + this.cdr.detectChanges(); + }); + } if (!this.pagination) { this.pagination = PaginationComponent.DEFAULT_PAGINATION; } @@ -201,5 +215,9 @@ export class PaginationComponent implements OnInit { } change.emit(params); + + if (this.target) { + this.target.updatePagination(params); + } } } diff --git a/lib/core/pagination/public-api.ts b/lib/core/pagination/public-api.ts index fd59f51c96..e701c8e20f 100644 --- a/lib/core/pagination/public-api.ts +++ b/lib/core/pagination/public-api.ts @@ -17,3 +17,5 @@ export * from './pagination.component'; export * from './infinite-pagination.component'; +export * from './paginated-component.interface'; +export * from './pagination-query-params.interface';