diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 924d59faf97b44e6a5ce234e8280ca8e67fcd59d..299c2afe634626e79f0f017cec35d230b8e2ac74 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -829,9 +829,9 @@ "item.page.person.search.title": "Articles by this author", - "item.page.related-items.view-more": "View more", + "item.page.related-items.view-more": "Show {{ amount }} more", - "item.page.related-items.view-less": "View less", + "item.page.related-items.view-less": "Hide last {{ amount }}", "item.page.relationships.isAuthorOfPublication": "Publications", @@ -1565,6 +1565,10 @@ "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Volume": "Search for Journal Volumes", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Funding Agency": "Search for Funding Agencies", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Funding": "Search for Funding", + "submission.sections.describe.relationship-lookup.selection-tab.tab-title": "Current Selection ({{ count }})", "submission.sections.describe.relationship-lookup.title.Journal Issue": "Journal Issues", @@ -1575,6 +1579,10 @@ "submission.sections.describe.relationship-lookup.title.Author": "Authors", + "submission.sections.describe.relationship-lookup.title.Funding Agency": "Funding Agency", + + "submission.sections.describe.relationship-lookup.title.Funding": "Funding", + "submission.sections.describe.relationship-lookup.search-tab.toggle-dropdown": "Toggle dropdown", "submission.sections.describe.relationship-lookup.selection-tab.settings": "Settings", diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts index 940868a0c6dc488e50c8dd21ab8a5f2509ab0856..5c54becddee4bf51b8998faa0b855ae9961f7dae 100644 --- a/src/app/+item-page/item-page.module.ts +++ b/src/app/+item-page/item-page.module.ts @@ -28,6 +28,7 @@ import { MetadataValuesComponent } from './field-components/metadata-values/meta import { MetadataFieldWrapperComponent } from './field-components/metadata-field-wrapper/metadata-field-wrapper.component'; import { TabbedRelatedEntitiesSearchComponent } from './simple/related-entities/tabbed-related-entities-search/tabbed-related-entities-search.component'; import { StatisticsModule } from '../statistics/statistics.module'; +import { AbstractIncrementalListComponent } from './simple/abstract-incremental-list/abstract-incremental-list.component'; @NgModule({ imports: [ @@ -57,7 +58,8 @@ import { StatisticsModule } from '../statistics/statistics.module'; GenericItemPageFieldComponent, MetadataRepresentationListComponent, RelatedEntitiesSearchComponent, - TabbedRelatedEntitiesSearchComponent + TabbedRelatedEntitiesSearchComponent, + AbstractIncrementalListComponent ], exports: [ ItemComponent, diff --git a/src/app/+item-page/simple/abstract-incremental-list/abstract-incremental-list.component.ts b/src/app/+item-page/simple/abstract-incremental-list/abstract-incremental-list.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e2c0823bf803fa334ccd46baf7b1e787da44fd9b --- /dev/null +++ b/src/app/+item-page/simple/abstract-incremental-list/abstract-incremental-list.component.ts @@ -0,0 +1,73 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs/internal/Subscription'; +import { hasValue, isNotEmpty } from '../../../shared/empty.util'; + +@Component({ + selector: 'ds-abstract-incremental-list', + template: ``, +}) +/** + * An abstract component for displaying an incremental list of objects + */ +export class AbstractIncrementalListComponent<T> implements OnInit, OnDestroy { + /** + * The amount to increment the list by + * Define this amount in the child component overriding this component + */ + incrementBy: number; + + /** + * All pages of objects to display as an array + */ + objects: T[]; + + /** + * A list of open subscriptions + */ + subscriptions: Subscription[]; + + ngOnInit(): void { + this.objects = []; + this.subscriptions = []; + this.increase(); + } + + /** + * Get a specific page + * > Override this method to return a specific page + * @param page The page to fetch + */ + getPage(page: number): T { + return undefined; + } + + /** + * Increase the amount displayed + */ + increase() { + const page = this.getPage(this.objects.length + 1); + if (hasValue(page)) { + this.objects.push(page); + } + } + + /** + * Decrease the amount displayed + */ + decrease() { + if (this.objects.length > 1) { + this.objects.pop(); + } + } + + /** + * Unsubscribe from any open subscriptions + */ + ngOnDestroy(): void { + if (isNotEmpty(this.subscriptions)) { + this.subscriptions.forEach((sub: Subscription) => { + sub.unsubscribe(); + }); + } + } +} diff --git a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.html b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.html index 750029b58b81f5f17daaf3f54770afec882b6f24..d1281f450a3d879efcaa43a0239535aaf91ae5f8 100644 --- a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.html +++ b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.html @@ -1,11 +1,20 @@ -<ds-metadata-field-wrapper *ngIf="representations$ && (representations$ | async)?.length > 0" [label]="label"> - <ds-metadata-representation-loader *ngFor="let rep of (representations$ | async)" - [mdRepresentation]="rep"> - </ds-metadata-representation-loader> - <div *ngIf="(representations$ | async)?.length < total" class="mt-2"> - <a [routerLink]="" (click)="viewMore()">{{'item.page.related-items.view-more' | translate}}</a> - </div> - <div *ngIf="limit > originalLimit" class="mt-2"> - <a [routerLink]="" (click)="viewLess()">{{'item.page.related-items.view-less' | translate}}</a> - </div> +<ds-metadata-field-wrapper [label]="label"> + <ng-container *ngFor="let objectPage of objects; let i = index"> + <ng-container *ngVar="(objectPage | async) as representations"> + <ds-metadata-representation-loader *ngFor="let rep of representations" + [mdRepresentation]="rep"> + </ds-metadata-representation-loader> + <ds-loading *ngIf="(i + 1) === objects.length && (i > 0) && (!representations || representations?.length === 0)" message="{{'loading.default' | translate}}"></ds-loading> + <div class="d-inline-block w-100 mt-2" *ngIf="(i + 1) === objects.length && representations?.length > 0"> + <div *ngIf="(objects.length * incrementBy) < total" class="float-left"> + <a [routerLink]="" (click)="increase()">{{'item.page.related-items.view-more' | + translate:{ amount: (total - (objects.length * incrementBy) < incrementBy) ? total - (objects.length * incrementBy) : incrementBy } }}</a> + </div> + <div *ngIf="objects.length > 1" class="float-right"> + <a [routerLink]="" (click)="decrease()">{{'item.page.related-items.view-less' | + translate:{ amount: representations?.length } }}</a> + </div> + </div> + </ng-container> + </ng-container> </ds-metadata-field-wrapper> diff --git a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.spec.ts b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.spec.ts index 7beabdceba231482714e3e1566035fdefffba8b9..ad62ce44180be4aedaf63b6ee34ee343e786ff2e 100644 --- a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.spec.ts +++ b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.spec.ts @@ -7,6 +7,8 @@ import { Item } from '../../../core/shared/item.model'; import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; import { TranslateModule } from '@ngx-translate/core'; +import { VarDirective } from '../../../shared/utils/var.directive'; +import { of as observableOf } from 'rxjs'; const itemType = 'Person'; const metadataField = 'dc.contributor.author'; @@ -64,7 +66,7 @@ describe('MetadataRepresentationListComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], - declarations: [MetadataRepresentationListComponent], + declarations: [MetadataRepresentationListComponent, VarDirective], providers: [ { provide: RelationshipService, useValue: relationshipService } ], @@ -88,33 +90,29 @@ describe('MetadataRepresentationListComponent', () => { expect(fields.length).toBe(2); }); - it('should initialize the original limit', () => { - expect(comp.originalLimit).toEqual(comp.limit); + it('should contain one page of items', () => { + expect(comp.objects.length).toEqual(1); }); - describe('when viewMore is called', () => { + describe('when increase is called', () => { beforeEach(() => { - comp.viewMore(); + comp.increase(); }); - it('should set the limit to a high number in order to retrieve all metadata representations', () => { - expect(comp.limit).toBeGreaterThanOrEqual(999); + it('should add a new page to the list', () => { + expect(comp.objects.length).toEqual(2); }); }); - describe('when viewLess is called', () => { - let originalLimit; - + describe('when decrease is called', () => { beforeEach(() => { - // Store the original value of limit - originalLimit = comp.limit; - // Set limit to a random number - comp.limit = 458; - comp.viewLess(); + // Add a second page + comp.objects.push(observableOf(undefined)); + comp.decrease(); }); - it('should reset the limit to the original value', () => { - expect(comp.limit).toEqual(originalLimit); + it('should decrease the list of pages', () => { + expect(comp.objects.length).toEqual(1); }); }); diff --git a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts index 1fa623f6c9088c3af8081a133ae5522b7d28db98..23484f22e06a73d5e7f45da07ea85b0d3eb04406 100644 --- a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts +++ b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts @@ -1,16 +1,16 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model'; import { combineLatest as observableCombineLatest, Observable, of as observableOf, zip as observableZip } from 'rxjs'; import { RelationshipService } from '../../../core/data/relationship.service'; import { MetadataValue } from '../../../core/shared/metadata.models'; import { getSucceededRemoteData } from '../../../core/shared/operators'; -import { switchMap } from 'rxjs/operators'; +import { filter, map, switchMap } from 'rxjs/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; import { Item } from '../../../core/shared/item.model'; import { MetadatumRepresentation } from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model'; import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model'; -import { map, filter } from 'rxjs/operators'; +import { AbstractIncrementalListComponent } from '../abstract-incremental-list/abstract-incremental-list.component'; @Component({ selector: 'ds-metadata-representation-list', @@ -22,7 +22,7 @@ import { map, filter } from 'rxjs/operators'; * It expects an itemType to resolve the metadata to a an item * It expects a label to put on top of the list */ -export class MetadataRepresentationListComponent implements OnInit { +export class MetadataRepresentationListComponent extends AbstractIncrementalListComponent<Observable<MetadataRepresentation[]>> { /** * The parent of the list of related items to display */ @@ -44,22 +44,11 @@ export class MetadataRepresentationListComponent implements OnInit { @Input() label: string; /** - * The max amount of representations to display + * The amount to increment the list by when clicking "view more" * Defaults to 10 * The default can optionally be overridden by providing the limit as input to the component */ - @Input() limit = 10; - - /** - * A list of metadata-representations to display - */ - representations$: Observable<MetadataRepresentation[]>; - - /** - * The originally provided limit - * Used for resetting the limit to the original value when collapsing the list - */ - originalLimit: number; + @Input() incrementBy = 10; /** * The total amount of metadata values available @@ -67,30 +56,28 @@ export class MetadataRepresentationListComponent implements OnInit { total: number; constructor(public relationshipService: RelationshipService) { - } - - ngOnInit(): void { - this.originalLimit = this.limit; - this.setRepresentations(); + super(); } /** - * Initialize the metadata representations + * Get a specific page + * @param page The page to fetch */ - setRepresentations() { + getPage(page: number): Observable<MetadataRepresentation[]> { const metadata = this.parentItem.findMetadataSortedByPlace(this.metadataField); this.total = metadata.length; - this.representations$ = this.resolveMetadataRepresentations(metadata); + return this.resolveMetadataRepresentations(metadata, page); } /** * Resolve a list of metadata values to a list of metadata representations - * @param metadata + * @param metadata The list of all metadata values + * @param page The page to return representations for */ - resolveMetadataRepresentations(metadata: MetadataValue[]): Observable<MetadataRepresentation[]> { + resolveMetadataRepresentations(metadata: MetadataValue[], page: number): Observable<MetadataRepresentation[]> { return observableZip( ...metadata - .slice(0, this.limit) + .slice((this.objects.length * this.incrementBy), (this.objects.length * this.incrementBy) + this.incrementBy) .map((metadatum: any) => Object.assign(new MetadataValue(), metadatum)) .map((metadatum: MetadataValue) => { if (metadatum.isVirtual) { @@ -115,20 +102,4 @@ export class MetadataRepresentationListComponent implements OnInit { }) ); } - - /** - * Expand the list to display all metadata representations - */ - viewMore() { - this.limit = 9999; - this.setRepresentations(); - } - - /** - * Collapse the list to display the originally displayed metadata representations - */ - viewLess() { - this.limit = this.originalLimit; - this.setRepresentations(); - } } diff --git a/src/app/+item-page/simple/related-items/related-items-component.ts b/src/app/+item-page/simple/related-items/related-items-component.ts index 5b4e1b985aaa8acb49f75f4a49554b722299412e..0446e53be57ab668895dcbe069837c30ac70d135 100644 --- a/src/app/+item-page/simple/related-items/related-items-component.ts +++ b/src/app/+item-page/simple/related-items/related-items-component.ts @@ -1,4 +1,4 @@ -import { Component, HostBinding, Input, OnDestroy, OnInit } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { Item } from '../../../core/shared/item.model'; import { Observable } from 'rxjs/internal/Observable'; import { RemoteData } from '../../../core/data/remote-data'; @@ -6,7 +6,7 @@ import { PaginatedList } from '../../../core/data/paginated-list'; import { FindListOptions } from '../../../core/data/request.models'; import { ViewMode } from '../../../core/shared/view-mode.model'; import { RelationshipService } from '../../../core/data/relationship.service'; -import { Subscription } from 'rxjs'; +import { AbstractIncrementalListComponent } from '../abstract-incremental-list/abstract-incremental-list.component'; @Component({ selector: 'ds-related-items', @@ -17,7 +17,7 @@ import { Subscription } from 'rxjs'; * This component is used for displaying relations between items * It expects a parent item and relationship type, as well as a label to display on top */ -export class RelatedItemsComponent implements OnInit, OnDestroy { +export class RelatedItemsComponent extends AbstractIncrementalListComponent<Observable<RemoteData<PaginatedList<Item>>>> { /** * The parent of the list of related items to display */ @@ -30,30 +30,22 @@ export class RelatedItemsComponent implements OnInit, OnDestroy { @Input() relationType: string; /** - * Default options to start a search request with - * Optional input, should you wish a different page size (or other options) - */ - @Input() options = Object.assign(new FindListOptions(), { elementsPerPage: 5 }); - - /** - * An i18n label to use as a title for the list (usually describes the relation) - */ - @Input() label: string; - - /** - * Completely hide the component until there's at least one item visible + * The amount to increment the list by when clicking "view more" + * Defaults to 5 + * The default can optionally be overridden by providing the limit as input to the component */ - @HostBinding('class.d-none') hidden = true; + @Input() incrementBy = 5; /** - * The list of related items + * Default options to start a search request with + * Optional input */ - items$: Observable<RemoteData<PaginatedList<Item>>>; + @Input() options = new FindListOptions(); /** - * Search options for displaying all elements in a list + * An i18n label to use as a title for the list (usually describes the relation) */ - allOptions = Object.assign(new FindListOptions(), { elementsPerPage: 9999 }); + @Input() label: string; /** * The view-mode we're currently on @@ -61,48 +53,15 @@ export class RelatedItemsComponent implements OnInit, OnDestroy { */ viewMode = ViewMode.ListElement; - /** - * Whether or not the list is currently expanded to show all related items - */ - showingAll = false; - - /** - * Subscription on items used to update the "hidden" property of this component - */ - itemSub: Subscription; - constructor(public relationshipService: RelationshipService) { - } - - ngOnInit(): void { - this.items$ = this.relationshipService.getRelatedItemsByLabel(this.parentItem, this.relationType, this.options); - this.itemSub = this.items$.subscribe((itemsRD: RemoteData<PaginatedList<Item>>) => { - this.hidden = !(itemsRD.hasSucceeded && itemsRD.payload && itemsRD.payload.page.length > 0); - }); - } - - /** - * Expand the list to display all related items - */ - viewMore() { - this.items$ = this.relationshipService.getRelatedItemsByLabel(this.parentItem, this.relationType, this.allOptions); - this.showingAll = true; - } - - /** - * Collapse the list to display the originally displayed items - */ - viewLess() { - this.items$ = this.relationshipService.getRelatedItemsByLabel(this.parentItem, this.relationType, this.options); - this.showingAll = false; + super(); } /** - * Unsubscribe from the item subscription + * Get a specific page + * @param page The page to fetch */ - ngOnDestroy(): void { - if (this.itemSub) { - this.itemSub.unsubscribe(); - } + getPage(page: number): Observable<RemoteData<PaginatedList<Item>>> { + return this.relationshipService.getRelatedItemsByLabel(this.parentItem, this.relationType, Object.assign(this.options, { elementsPerPage: this.incrementBy, currentPage: page })); } } diff --git a/src/app/+item-page/simple/related-items/related-items.component.html b/src/app/+item-page/simple/related-items/related-items.component.html index dab85ee0e57d906880d600bca45297663175d284..11cedc40409f7ad8e0efacb5d6aa1d401a312ed7 100644 --- a/src/app/+item-page/simple/related-items/related-items.component.html +++ b/src/app/+item-page/simple/related-items/related-items.component.html @@ -1,11 +1,20 @@ -<ds-metadata-field-wrapper *ngIf="(items$ | async)?.payload?.page?.length > 0" [label]="label"> - <ds-listable-object-component-loader *ngFor="let item of (items$ | async)?.payload?.page" - [object]="item" [viewMode]="viewMode"> - </ds-listable-object-component-loader> - <div *ngIf="(items$ | async)?.payload?.page?.length < (items$ | async)?.payload?.totalElements" class="mt-2" id="view-more"> - <a [routerLink]="" (click)="viewMore()">{{'item.page.related-items.view-more' | translate}}</a> - </div> - <div *ngIf="showingAll" class="mt-2" id="view-less"> - <a [routerLink]="" (click)="viewLess()">{{'item.page.related-items.view-less' | translate}}</a> - </div> +<ds-metadata-field-wrapper [label]="label"> + <ng-container *ngFor="let objectPage of objects; let i = index"> + <ng-container *ngVar="(objectPage | async) as itemsRD"> + <ds-listable-object-component-loader *ngFor="let item of itemsRD?.payload?.page" + [object]="item" [viewMode]="viewMode"> + </ds-listable-object-component-loader> + <ds-loading *ngIf="(i + 1) === objects.length && (itemsRD || i > 0) && !(itemsRD?.hasSucceeded && itemsRD?.payload && itemsRD?.payload?.page?.length > 0)" message="{{'loading.default' | translate}}"></ds-loading> + <div class="d-inline-block w-100 mt-2" *ngIf="(i + 1) === objects.length && itemsRD?.payload?.page?.length > 0"> + <div *ngIf="itemsRD?.payload?.totalPages > objects.length" class="float-left" id="view-more"> + <a [routerLink]="" (click)="increase()">{{'item.page.related-items.view-more' | + translate:{ amount: (itemsRD?.payload?.totalElements - (incrementBy * objects.length) < incrementBy) ? itemsRD?.payload?.totalElements - (incrementBy * objects.length) : incrementBy } }}</a> + </div> + <div *ngIf="objects.length > 1" class="float-right" id="view-less"> + <a [routerLink]="" (click)="decrease()">{{'item.page.related-items.view-less' | + translate:{ amount: itemsRD?.payload?.page?.length } }}</a> + </div> + </div> + </ng-container> + </ng-container> </ds-metadata-field-wrapper> diff --git a/src/app/+item-page/simple/related-items/related-items.component.spec.ts b/src/app/+item-page/simple/related-items/related-items.component.spec.ts index 4a751a31b89798430690047584ee407699190929..5b1f33c64df0d9fe2b1eff520e069410a232fd54 100644 --- a/src/app/+item-page/simple/related-items/related-items.component.spec.ts +++ b/src/app/+item-page/simple/related-items/related-items.component.spec.ts @@ -9,6 +9,8 @@ import { createRelationshipsObservable } from '../item-types/shared/item.compone import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; import { RelationshipService } from '../../../core/data/relationship.service'; import { TranslateModule } from '@ngx-translate/core'; +import { VarDirective } from '../../../shared/utils/var.directive'; +import { of as observableOf } from 'rxjs'; const parentItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), @@ -42,7 +44,7 @@ describe('RelatedItemsComponent', () => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], - declarations: [RelatedItemsComponent], + declarations: [RelatedItemsComponent, VarDirective], providers: [ { provide: RelationshipService, useValue: relationshipService } ], @@ -65,31 +67,33 @@ describe('RelatedItemsComponent', () => { expect(fields.length).toBe(mockItems.length); }); - describe('when viewMore is called', () => { + it('should contain one page of items', () => { + expect(comp.objects.length).toEqual(1); + }); + + describe('when increase is called', () => { beforeEach(() => { - comp.viewMore(); + comp.increase(); }); - it('should call relationship-service\'s getRelatedItemsByLabel with the correct arguments', () => { - expect(relationshipService.getRelatedItemsByLabel).toHaveBeenCalledWith(parentItem, relationType, comp.allOptions); + it('should add a new page to the list', () => { + expect(comp.objects.length).toEqual(2); }); - it('should set showingAll to true', () => { - expect(comp.showingAll).toEqual(true); + it('should call relationship-service\'s getRelatedItemsByLabel with the correct arguments (second page)', () => { + expect(relationshipService.getRelatedItemsByLabel).toHaveBeenCalledWith(parentItem, relationType, Object.assign(comp.options, { elementsPerPage: comp.incrementBy, currentPage: 2 })); }); }); - describe('when viewLess is called', () => { + describe('when decrease is called', () => { beforeEach(() => { - comp.viewLess(); - }); - - it('should call relationship-service\'s getRelatedItemsByLabel with the correct arguments', () => { - expect(relationshipService.getRelatedItemsByLabel).toHaveBeenCalledWith(parentItem, relationType, comp.options); + // Add a second page + comp.objects.push(observableOf(undefined)); + comp.decrease(); }); - it('should set showingAll to false', () => { - expect(comp.showingAll).toEqual(false); + it('should decrease the list of pages', () => { + expect(comp.objects.length).toEqual(1); }); });