diff --git a/src/app/+item-page/full/field-components/file-section/full-file-section.component.html b/src/app/+item-page/full/field-components/file-section/full-file-section.component.html index 7c1719eb82..c6f9f8e944 100644 --- a/src/app/+item-page/full/field-components/file-section/full-file-section.component.html +++ b/src/app/+item-page/full/field-components/file-section/full-file-section.component.html @@ -1,29 +1,87 @@ -
-
- -
-
-
-
{{"item.page.filesection.name" | translate}}
-
{{file.name}}
- -
{{"item.page.filesection.size" | translate}}
-
{{(file.sizeBytes) | dsFileSize }}
+
+
{{"item.page.filesection.original.bundle" | translate}}
+ -
{{"item.page.filesection.format" | translate}}
-
{{(file.format | async)?.payload?.description}}
+ +
+
+ +
+
+
+
{{"item.page.filesection.name" | translate}}
+
{{file.name}}
+ +
{{"item.page.filesection.size" | translate}}
+
{{(file.sizeBytes) | dsFileSize }}
-
{{"item.page.filesection.description" | translate}}
-
{{file.firstMetadataValue("dc.description")}}
-
-
-
- - {{"item.page.filesection.download" | translate}} - -
+
{{"item.page.filesection.format" | translate}}
+
{{(file.format | async)?.payload?.description}}
+ + +
{{"item.page.filesection.description" | translate}}
+
{{file.firstMetadataValue("dc.description")}}
+
+
+
+ + {{"item.page.filesection.download" | translate}} + +
+
+ + + +
+
{{"item.page.filesection.license.bundle" | translate}}
+ + + + +
+
+ +
+
+
+
{{"item.page.filesection.name" | translate}}
+
{{file.name}}
+ +
{{"item.page.filesection.size" | translate}}
+
{{(file.sizeBytes) | dsFileSize }}
+ + +
{{"item.page.filesection.format" | translate}}
+
{{(file.format | async)?.payload?.description}}
+ + +
{{"item.page.filesection.description" | translate}}
+
{{file.firstMetadataValue("dc.description")}}
+
+
+
+ + {{"item.page.filesection.download" | translate}} + +
+
+
diff --git a/src/app/+item-page/full/field-components/file-section/full-file-section.component.spec.ts b/src/app/+item-page/full/field-components/file-section/full-file-section.component.spec.ts new file mode 100644 index 0000000000..970420f252 --- /dev/null +++ b/src/app/+item-page/full/field-components/file-section/full-file-section.component.spec.ts @@ -0,0 +1,117 @@ +import {FullFileSectionComponent} from './full-file-section.component'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {createSuccessfulRemoteDataObject$} from '../../../../shared/remote-data.utils'; +import {createPaginatedList} from '../../../../shared/testing/utils.test'; +import {TranslateLoader, TranslateModule} from '@ngx-translate/core'; +import {TranslateLoaderMock} from '../../../../shared/mocks/translate-loader.mock'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {VarDirective} from '../../../../shared/utils/var.directive'; +import {FileSizePipe} from '../../../../shared/utils/file-size-pipe'; +import {MetadataFieldWrapperComponent} from '../../../field-components/metadata-field-wrapper/metadata-field-wrapper.component'; +import {BitstreamDataService} from '../../../../core/data/bitstream-data.service'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {Bitstream} from '../../../../core/shared/bitstream.model'; +import {of as observableOf} from 'rxjs'; +import {MockBitstreamFormat1} from '../../../../shared/mocks/item.mock'; +import {By} from '@angular/platform-browser'; + +describe('FullFileSectionComponent', () => { + let comp: FullFileSectionComponent; + let fixture: ComponentFixture; + + const mockBitstream: Bitstream = Object.assign(new Bitstream(), + { + sizeBytes: 10201, + content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content', + format: observableOf(MockBitstreamFormat1), + bundleName: 'ORIGINAL', + _links: { + self: { + href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713' + }, + content: { + href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content' + } + }, + id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713', + uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713', + type: 'bitstream', + metadata: { + 'dc.title': [ + { + language: null, + value: 'test_word.docx' + } + ] + } + }); + + const bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', { + findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(createPaginatedList([mockBitstream, mockBitstream, mockBitstream])) + }); + + beforeEach(async(() => { + + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), BrowserAnimationsModule], + declarations: [FullFileSectionComponent, VarDirective, FileSizePipe, MetadataFieldWrapperComponent], + providers: [ + {provide: BitstreamDataService, useValue: bitstreamDataService} + ], + + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(async(() => { + fixture = TestBed.createComponent(FullFileSectionComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + })); + + describe('when the full file section gets loaded with bitstreams available', () => { + it ('should contain a list with bitstreams', () => { + const fileSection = fixture.debugElement.queryAll(By.css('.file-section')); + expect(fileSection.length).toEqual(6); + }); + + describe('when we press the pageChange button for original bundle', () => { + beforeEach(() => { + comp.switchOriginalPage(2); + fixture.detectChanges(); + }); + + it ('should give the value to the currentpage', () => { + expect(comp.originalOptions.currentPage).toBe(2); + }) + it ('should call the next function on the originalCurrentPage', (done) => { + comp.originalCurrentPage$.subscribe((event) => { + expect(event).toEqual(2); + done(); + }) + }) + }) + + describe('when we press the pageChange button for license bundle', () => { + beforeEach(() => { + comp.switchLicensePage(2); + fixture.detectChanges(); + }); + + it ('should give the value to the currentpage', () => { + expect(comp.licenseOptions.currentPage).toBe(2); + }) + it ('should call the next function on the licenseCurrentPage', (done) => { + comp.licenseCurrentPage$.subscribe((event) => { + expect(event).toEqual(2); + done(); + }) + }) + }) + }) +}) diff --git a/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts b/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts index f18fccd7e9..fdbe662ed9 100644 --- a/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts +++ b/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts @@ -1,13 +1,15 @@ -import { Component, Injector, Input, OnInit } from '@angular/core'; -import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { map, startWith } from 'rxjs/operators'; +import { Component, Input, OnInit } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Item } from '../../../../core/shared/item.model'; -import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators'; import { followLink } from '../../../../shared/utils/follow-link-config.model'; import { FileSectionComponent } from '../../../simple/field-components/file-section/file-section.component'; +import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; +import { PaginatedList } from '../../../../core/data/paginated-list'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { switchMap } from 'rxjs/operators'; /** * This component renders the file section of the item @@ -25,7 +27,23 @@ export class FullFileSectionComponent extends FileSectionComponent implements On label: string; - bitstreams$: Observable; + originals$: Observable>>; + licenses$: Observable>>; + + pageSize = 5; + originalOptions = Object.assign(new PaginationComponentOptions(),{ + id: 'original-bitstreams-options', + currentPage: 1, + pageSize: this.pageSize + }); + originalCurrentPage$ = new BehaviorSubject(1); + + licenseOptions = Object.assign(new PaginationComponentOptions(),{ + id: 'license-bitstreams-options', + currentPage: 1, + pageSize: this.pageSize + }); + licenseCurrentPage$ = new BehaviorSubject(1); constructor( bitstreamDataService: BitstreamDataService @@ -34,40 +52,45 @@ export class FullFileSectionComponent extends FileSectionComponent implements On } ngOnInit(): void { - super.ngOnInit(); + this.initialize(); } initialize(): void { - // TODO pagination - const originals$ = this.bitstreamDataService.findAllByItemAndBundleName( - this.item, - 'ORIGINAL', - { elementsPerPage: Number.MAX_SAFE_INTEGER }, - followLink( 'format') - ).pipe( - getFirstSucceededRemoteListPayload(), - startWith([]) + this.originals$ = this.originalCurrentPage$.pipe( + switchMap((pageNumber: number) => this.bitstreamDataService.findAllByItemAndBundleName( + this.item, + 'ORIGINAL', + { elementsPerPage: this.pageSize, currentPage: pageNumber }, + followLink( 'format') + )) ); - const licenses$ = this.bitstreamDataService.findAllByItemAndBundleName( - this.item, - 'LICENSE', - { elementsPerPage: Number.MAX_SAFE_INTEGER }, - followLink( 'format') - ).pipe( - getFirstSucceededRemoteListPayload(), - startWith([]) - ); - this.bitstreams$ = observableCombineLatest(originals$, licenses$).pipe( - map(([o, l]) => [...o, ...l]), - map((files: Bitstream[]) => - files.map( - (original) => { - original.thumbnail = this.bitstreamDataService.getMatchingThumbnail(this.item, original); - return original; - } - ) - ) + + this.licenses$ = this.licenseCurrentPage$.pipe( + switchMap((pageNumber: number) => this.bitstreamDataService.findAllByItemAndBundleName( + this.item, + 'LICENSE', + { elementsPerPage: this.pageSize, currentPage: pageNumber }, + followLink( 'format') + )) ); + } + /** + * Update the current page for the original bundle bitstreams + * @param page + */ + switchOriginalPage(page: number) { + this.originalOptions.currentPage = page; + this.originalCurrentPage$.next(page); + } + + /** + * Update the current page for the license bundle bitstreams + * @param page + */ + switchLicensePage(page: number) { + this.licenseOptions.currentPage = page; + this.licenseCurrentPage$.next(page); + } } diff --git a/src/app/+item-page/simple/field-components/file-section/file-section.component.html b/src/app/+item-page/simple/field-components/file-section/file-section.component.html index 17e4a795e7..1fdee6dc4d 100644 --- a/src/app/+item-page/simple/field-components/file-section/file-section.component.html +++ b/src/app/+item-page/simple/field-components/file-section/file-section.component.html @@ -6,6 +6,13 @@ ({{(file?.sizeBytes) | dsFileSize }}) + + + diff --git a/src/app/+item-page/simple/field-components/file-section/file-section.component.spec.ts b/src/app/+item-page/simple/field-components/file-section/file-section.component.spec.ts new file mode 100644 index 0000000000..1b7fa75ce5 --- /dev/null +++ b/src/app/+item-page/simple/field-components/file-section/file-section.component.spec.ts @@ -0,0 +1,169 @@ +import {FileSectionComponent} from './file-section.component'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {TranslateLoader, TranslateModule} from '@ngx-translate/core'; +import {TranslateLoaderMock} from '../../../../shared/mocks/translate-loader.mock'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {VarDirective} from '../../../../shared/utils/var.directive'; +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {BitstreamDataService} from '../../../../core/data/bitstream-data.service'; +import {createSuccessfulRemoteDataObject$} from '../../../../shared/remote-data.utils'; +import {By} from '@angular/platform-browser'; +import {Bitstream} from '../../../../core/shared/bitstream.model'; +import {of as observableOf} from 'rxjs'; +import {MockBitstreamFormat1} from '../../../../shared/mocks/item.mock'; +import {FileSizePipe} from '../../../../shared/utils/file-size-pipe'; +import {PageInfo} from '../../../../core/shared/page-info.model'; +import {MetadataFieldWrapperComponent} from '../../../field-components/metadata-field-wrapper/metadata-field-wrapper.component'; +import {createPaginatedList} from '../../../../shared/testing/utils.test'; + +describe('FileSectionComponent', () => { + let comp: FileSectionComponent; + let fixture: ComponentFixture; + + const bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', { + findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(createPaginatedList([])) + }); + + const mockBitstream: Bitstream = Object.assign(new Bitstream(), + { + sizeBytes: 10201, + content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content', + format: observableOf(MockBitstreamFormat1), + bundleName: 'ORIGINAL', + _links: { + self: { + href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713' + }, + content: { + href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content' + } + }, + id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713', + uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713', + type: 'bitstream', + metadata: { + 'dc.title': [ + { + language: null, + value: 'test_word.docx' + } + ] + } + }); + + beforeEach(async(() => { + + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }), BrowserAnimationsModule], + declarations: [FileSectionComponent, VarDirective, FileSizePipe, MetadataFieldWrapperComponent], + providers: [ + {provide: BitstreamDataService, useValue: bitstreamDataService} + ], + + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(async(() => { + fixture = TestBed.createComponent(FileSectionComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + })); + + describe('when the bitstreams are loading', () => { + beforeEach(() => { + comp.bitstreams$.next([mockBitstream]); + comp.isLoading = true; + fixture.detectChanges(); + }); + + it('should display a loading component', () => { + const loading = fixture.debugElement.query(By.css('ds-loading')); + expect(loading.nativeElement).toBeDefined(); + }); + }); + + describe('when the "Show more" button is clicked', () => { + + beforeEach(() => { + comp.bitstreams$.next([mockBitstream]); + comp.currentPage = 1; + comp.isLastPage = false; + fixture.detectChanges(); + }); + + it('should call the service to retrieve more bitstreams', () => { + const viewMore = fixture.debugElement.query(By.css('.bitstream-view-more')); + viewMore.triggerEventHandler('click', null); + expect(bitstreamDataService.findAllByItemAndBundleName).toHaveBeenCalled() + }) + + it('one bitstream should be on the page', () => { + const viewMore = fixture.debugElement.query(By.css('.bitstream-view-more')); + viewMore.triggerEventHandler('click', null); + const fileDownloadLink = fixture.debugElement.queryAll(By.css('ds-file-download-link')); + expect(fileDownloadLink.length).toEqual(1); + }) + + describe('when it is then clicked again', () => { + beforeEach(() => { + bitstreamDataService.findAllByItemAndBundleName.and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList([mockBitstream]))); + const viewMore = fixture.debugElement.query(By.css('.bitstream-view-more')); + viewMore.triggerEventHandler('click', null); + fixture.detectChanges(); + + }) + it('should contain another bitstream', () => { + const fileDownloadLink = fixture.debugElement.queryAll(By.css('ds-file-download-link')); + expect(fileDownloadLink.length).toEqual(2); + }) + }) + }); + + describe('when its the last page of bitstreams', () => { + beforeEach(() => { + comp.bitstreams$.next([mockBitstream]); + comp.isLastPage = true; + comp.currentPage = 2; + fixture.detectChanges(); + }); + + it('should not contain a view more link', () => { + const viewMore = fixture.debugElement.query(By.css('.bitstream-view-more')); + expect(viewMore).toBeNull(); + }) + + it('should contain a view less link', () => { + const viewLess = fixture.debugElement.query(By.css('.bitstream-collapse')); + expect(viewLess).toBeDefined(); + }) + + it('clicking on the view less link should reset the pages and call getNextPage()', () => { + const pageInfo = Object.assign(new PageInfo(), { + elementsPerPage: 3, + totalElements: 5, + totalPages: 2, + currentPage: 1, + _links: { + self: {href: 'https://rest.api/core/bitstreams/'}, + next: {href: 'https://rest.api/core/bitstreams?page=2'} + } + }); + const PaginatedList = Object.assign(createPaginatedList([mockBitstream]), { + pageInfo: pageInfo + }); + bitstreamDataService.findAllByItemAndBundleName.and.returnValue(createSuccessfulRemoteDataObject$(PaginatedList)); + const viewLess = fixture.debugElement.query(By.css('.bitstream-collapse')); + viewLess.triggerEventHandler('click', null); + expect(bitstreamDataService.findAllByItemAndBundleName).toHaveBeenCalled(); + expect(comp.currentPage).toBe(1); + expect(comp.isLastPage).toBeFalse(); + }) + + }) +}) diff --git a/src/app/+item-page/simple/field-components/file-section/file-section.component.ts b/src/app/+item-page/simple/field-components/file-section/file-section.component.ts index 2e09c1cd49..25b214e200 100644 --- a/src/app/+item-page/simple/field-components/file-section/file-section.component.ts +++ b/src/app/+item-page/simple/field-components/file-section/file-section.component.ts @@ -1,10 +1,13 @@ import { Component, Input, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; +import { BehaviorSubject } from 'rxjs'; import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Item } from '../../../../core/shared/item.model'; -import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators'; +import { filter, takeWhile } from 'rxjs/operators'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { hasNoValue, hasValue } from '../../../../shared/empty.util'; +import { PaginatedList } from '../../../../core/data/paginated-list'; /** * This component renders the file section of the item @@ -22,7 +25,15 @@ export class FileSectionComponent implements OnInit { separator = '
'; - bitstreams$: Observable; + bitstreams$: BehaviorSubject; + + currentPage: number; + + isLoading: boolean; + + isLastPage: boolean; + + pageSize = 5; constructor( protected bitstreamDataService: BitstreamDataService @@ -30,13 +41,31 @@ export class FileSectionComponent implements OnInit { } ngOnInit(): void { - this.initialize(); + this.getNextPage(); } - initialize(): void { - this.bitstreams$ = this.bitstreamDataService.findAllByItemAndBundleName(this.item, 'ORIGINAL').pipe( - getFirstSucceededRemoteListPayload() - ); + /** + * This method will retrieve the next page of Bitstreams from the external BitstreamDataService call. + * It'll retrieve the currentPage from the class variables and it'll add the next page of bitstreams with the + * already existing one. + * If the currentPage variable is undefined, we'll set it to 1 and retrieve the first page of Bitstreams + */ + getNextPage(): void { + this.isLoading = true; + if (this.currentPage === undefined) { + this.currentPage = 1; + this.bitstreams$ = new BehaviorSubject([]); + } else { + this.currentPage++; + } + this.bitstreamDataService.findAllByItemAndBundleName(this.item, 'ORIGINAL', { currentPage: this.currentPage, elementsPerPage: this.pageSize }).pipe( + filter((bitstreamsRD: RemoteData>) => hasValue(bitstreamsRD)), + takeWhile((bitstreamsRD: RemoteData>) => hasNoValue(bitstreamsRD.payload) && hasNoValue(bitstreamsRD.error), true) + ).subscribe((bitstreamsRD: RemoteData>) => { + const current: Bitstream[] = this.bitstreams$.getValue(); + this.bitstreams$.next([...current, ...bitstreamsRD.payload.page]); + this.isLoading = false; + this.isLastPage = this.currentPage === bitstreamsRD.payload.totalPages; + }); } - } diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index ae3b0e4fd1..83cecca502 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -139,8 +139,8 @@ export class RemoteDataBuildService { const pageInfo$ = requestEntry$.pipe( filterSuccessfulResponses(), map((response: DSOSuccessResponse) => { - if (hasValue((response as DSOSuccessResponse).pageInfo)) { - return (response as DSOSuccessResponse).pageInfo; + if (hasValue(response.pageInfo)) { + return Object.assign(new PageInfo(), response.pageInfo); } }) ); diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index e498e0c1fe..b2540162ab 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1600,6 +1600,13 @@ "item.page.uri": "URI", + "item.page.bitstreams.view-more": "Show more", + + "item.page.bitstreams.collapse": "Collapse", + + "item.page.filesection.original.bundle" : "Original bundle", + + "item.page.filesection.license.bundle" : "License bundle", "item.select.confirm": "Confirm selected",