diff --git a/.travis.yml b/.travis.yml index 901dee8186..c42923886d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ sudo: required -dist: trusty +dist: bionic env: # Install the latest docker-compose version for ci testing. @@ -12,6 +12,9 @@ env: DSPACE_REST_NAMESPACE: '/server/api' DSPACE_REST_SSL: false +services: + - xvfb + before_install: # Docker Compose Install - curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose @@ -33,14 +36,6 @@ before_script: after_script: - docker-compose -f ./docker/docker-compose-travis.yml down -addons: - apt: - sources: - - google-chrome - packages: - - dpkg - - google-chrome-stable - language: node_js node_js: @@ -53,8 +48,6 @@ cache: bundler_args: --retry 5 script: - # Use Chromium instead of Chrome. - - export CHROME_BIN=chromium-browser - yarn run build - yarn run ci - cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index fc4c0aee57..478f183da4 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -1566,6 +1566,8 @@ "search.results.no-results-link": "quotes around it", + "search.results.empty": "Your search returned no results.", + "search.sidebar.close": "Back to results", @@ -1639,13 +1641,21 @@ "submission.sections.describe.relationship-lookup.selected": "Selected {{ size }} items", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Author": "Search for Authors", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Author": "Local Authors ({{ count }})", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Search for Journals", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Local Journals ({{ count }})", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Issue": "Search for Journal Issues", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Issue": "Local Journal Issues ({{ count }})", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Volume": "Search for Journal Volumes", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Volume": "Local Journal Volumes ({{ count }})", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaJournal": "Sherpa Journals ({{ count }})", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Publishers ({{ count }})", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})", "submission.sections.describe.relationship-lookup.search-tab.tab-title.Funding Agency": "Search for Funding Agencies", @@ -1679,6 +1689,14 @@ "submission.sections.describe.relationship-lookup.selection-tab.title.Journal Issue": "Selected Issue", + "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaJournal": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Search Results", + "submission.sections.describe.relationship-lookup.name-variant.notification.content": "Would you like to save \"{{ value }}\" as a name variant for this person so you and others can reuse it for future submissions? If you don\'t you can still use it for this submission.", "submission.sections.describe.relationship-lookup.name-variant.notification.confirm": "Save a new name variant", diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html index 9156a99b18..bf6ce7fd57 100644 --- a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html @@ -1,14 +1,13 @@

{{'community.sub-collection-list.head' | translate}}

- + +
diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts new file mode 100644 index 0000000000..09332dda16 --- /dev/null +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts @@ -0,0 +1,182 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { CommunityPageSubCollectionListComponent } from './community-page-sub-collection-list.component'; +import { Community } from '../../core/shared/community.model'; +import { SharedModule } from '../../shared/shared.module'; +import { CollectionDataService } from '../../core/data/collection-data.service'; +import { FindListOptions } from '../../core/data/request.models'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { HostWindowService } from '../../shared/host-window.service'; +import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub'; +import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; + +describe('CommunityPageSubCollectionList Component', () => { + let comp: CommunityPageSubCollectionListComponent; + let fixture: ComponentFixture; + let collectionDataServiceStub: any; + let subCollList = []; + + const collections = [Object.assign(new Community(), { + id: '123456789-1', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 1' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-2', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 2' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-3', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 3' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-4', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 4' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-5', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 5' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-6', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 6' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-7', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 7' } + ] + } + }) + ]; + + const mockCommunity = Object.assign(new Community(), { + id: '123456789', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Test title' } + ] + } + }); + + collectionDataServiceStub = { + findByParent(parentUUID: string, options: FindListOptions = {}) { + let currentPage = options.currentPage; + let elementsPerPage = options.elementsPerPage; + if (currentPage === undefined) { + currentPage = 1 + } + elementsPerPage = 5; + const startPageIndex = (currentPage - 1) * elementsPerPage; + let endPageIndex = (currentPage * elementsPerPage); + if (endPageIndex > subCollList.length) { + endPageIndex = subCollList.length; + } + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), subCollList.slice(startPageIndex, endPageIndex))); + + } + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + SharedModule, + RouterTestingModule.withRoutes([]), + NgbModule.forRoot(), + NoopAnimationsModule + ], + declarations: [CommunityPageSubCollectionListComponent], + providers: [ + { provide: CollectionDataService, useValue: collectionDataServiceStub }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + { provide: SelectableListService, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CommunityPageSubCollectionListComponent); + comp = fixture.componentInstance; + comp.community = mockCommunity; + }); + + it('should display a list of collections', () => { + subCollList = collections; + fixture.detectChanges(); + + const collList = fixture.debugElement.queryAll(By.css('li')); + expect(collList.length).toEqual(5); + expect(collList[0].nativeElement.textContent).toContain('Collection 1'); + expect(collList[1].nativeElement.textContent).toContain('Collection 2'); + expect(collList[2].nativeElement.textContent).toContain('Collection 3'); + expect(collList[3].nativeElement.textContent).toContain('Collection 4'); + expect(collList[4].nativeElement.textContent).toContain('Collection 5'); + }); + + it('should not display the header when list of collections is empty', () => { + subCollList = []; + fixture.detectChanges(); + + const subComHead = fixture.debugElement.queryAll(By.css('h2')); + expect(subComHead.length).toEqual(0); + }); + + it('should update list of collections on pagination change', () => { + subCollList = collections; + fixture.detectChanges(); + + const pagination = Object.create({ + pagination:{ + id: comp.pageId, + currentPage: 2, + pageSize: 5 + }, + sort: { + field: 'dc.title', + direction: 'ASC' + } + }); + comp.onPaginationChange(pagination); + fixture.detectChanges(); + + const collList = fixture.debugElement.queryAll(By.css('li')); + expect(collList.length).toEqual(2); + expect(collList[0].nativeElement.textContent).toContain('Collection 6'); + expect(collList[1].nativeElement.textContent).toContain('Collection 7'); + }); +}); diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts index b8a5d60002..64c274444e 100644 --- a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts @@ -1,12 +1,16 @@ import { Component, Input, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; + +import { BehaviorSubject } from 'rxjs'; +import { take } from 'rxjs/operators'; import { RemoteData } from '../../core/data/remote-data'; import { Collection } from '../../core/shared/collection.model'; import { Community } from '../../core/shared/community.model'; - import { fadeIn } from '../../shared/animations/fade'; import { PaginatedList } from '../../core/data/paginated-list'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { CollectionDataService } from '../../core/data/collection-data.service'; @Component({ selector: 'ds-community-page-sub-collection-list', @@ -16,9 +20,60 @@ import { PaginatedList } from '../../core/data/paginated-list'; }) export class CommunityPageSubCollectionListComponent implements OnInit { @Input() community: Community; - subCollectionsRDObs: Observable>>; + + /** + * The pagination configuration + */ + config: PaginationComponentOptions; + + /** + * The pagination id + */ + pageId = 'community-collections-pagination'; + + /** + * The sorting configuration + */ + sortConfig: SortOptions; + + /** + * A list of remote data objects of communities' collections + */ + subCollectionsRDObs: BehaviorSubject>> = new BehaviorSubject>>({} as any); + + constructor(private cds: CollectionDataService) {} ngOnInit(): void { - this.subCollectionsRDObs = this.community.collections; + this.config = new PaginationComponentOptions(); + this.config.id = this.pageId; + this.config.pageSize = 5; + this.config.currentPage = 1; + this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); + this.updatePage(); + } + + /** + * Called when one of the pagination settings is changed + * @param event The new pagination data + */ + onPaginationChange(event) { + this.config.currentPage = event.pagination.currentPage; + this.config.pageSize = event.pagination.pageSize; + this.sortConfig.field = event.sort.field; + this.sortConfig.direction = event.sort.direction; + this.updatePage(); + } + + /** + * Update the list of collections + */ + updatePage() { + this.cds.findByParent(this.community.id,{ + currentPage: this.config.currentPage, + elementsPerPage: this.config.pageSize, + sort: { field: this.sortConfig.field, direction: this.sortConfig.direction } + }).pipe(take(1)).subscribe((results) => { + this.subCollectionsRDObs.next(results); + }); } } diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.html b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.html index 6cd62ba48d..880ea9cc8e 100644 --- a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.html +++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.html @@ -1,14 +1,13 @@

{{'community.sub-community-list.head' | translate}}

- + +
diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts index 2feaa3afa6..41502e7bd4 100644 --- a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts +++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts @@ -1,21 +1,29 @@ -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import {TranslateModule} from '@ngx-translate/core'; -import {NO_ERRORS_SCHEMA} from '@angular/core'; -import {CommunityPageSubCommunityListComponent} from './community-page-sub-community-list.component'; -import {Community} from '../../core/shared/community.model'; -import {RemoteData} from '../../core/data/remote-data'; -import {PaginatedList} from '../../core/data/paginated-list'; -import {PageInfo} from '../../core/shared/page-info.model'; -import {SharedModule} from '../../shared/shared.module'; -import {RouterTestingModule} from '@angular/router/testing'; -import {NoopAnimationsModule} from '@angular/platform-browser/animations'; -import {By} from '@angular/platform-browser'; -import {of as observableOf, Observable } from 'rxjs'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { By } from '@angular/platform-browser'; -describe('SubCommunityList Component', () => { +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { CommunityPageSubCommunityListComponent } from './community-page-sub-community-list.component'; +import { Community } from '../../core/shared/community.model'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { SharedModule } from '../../shared/shared.module'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { FindListOptions } from '../../core/data/request.models'; +import { HostWindowService } from '../../shared/host-window.service'; +import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; + +describe('CommunityPageSubCommunityListComponent Component', () => { let comp: CommunityPageSubCommunityListComponent; let fixture: ComponentFixture; + let communityDataServiceStub: any; + let subCommList = []; const subcommunities = [Object.assign(new Community(), { id: '123456789-1', @@ -32,34 +40,92 @@ describe('SubCommunityList Component', () => { { language: 'en_US', value: 'SubCommunity 2' } ] } + }), + Object.assign(new Community(), { + id: '123456789-3', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 3' } + ] + } + }), + Object.assign(new Community(), { + id: '12345678942', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 4' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-5', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 5' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-6', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 6' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-7', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 7' } + ] + } }) ]; - const emptySubCommunitiesCommunity = Object.assign(new Community(), { + const mockCommunity = Object.assign(new Community(), { + id: '123456789', metadata: { 'dc.title': [ { language: 'en_US', value: 'Test title' } ] - }, - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])) + } }); - const mockCommunity = Object.assign(new Community(), { - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'Test title' } - ] - }, - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), subcommunities)) - }) - ; + communityDataServiceStub = { + findByParent(parentUUID: string, options: FindListOptions = {}) { + let currentPage = options.currentPage; + let elementsPerPage = options.elementsPerPage; + if (currentPage === undefined) { + currentPage = 1 + } + elementsPerPage = 5; + + const startPageIndex = (currentPage - 1) * elementsPerPage; + let endPageIndex = (currentPage * elementsPerPage); + if (endPageIndex > subCommList.length) { + endPageIndex = subCommList.length; + } + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), subCommList.slice(startPageIndex, endPageIndex))); + + } + }; beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, + imports: [ + TranslateModule.forRoot(), + SharedModule, RouterTestingModule.withRoutes([]), - NoopAnimationsModule], + NgbModule.forRoot(), + NoopAnimationsModule + ], declarations: [CommunityPageSubCommunityListComponent], + providers: [ + { provide: CommunityDataService, useValue: communityDataServiceStub }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + { provide: SelectableListService, useValue: {} }, + ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); })); @@ -67,23 +133,52 @@ describe('SubCommunityList Component', () => { beforeEach(() => { fixture = TestBed.createComponent(CommunityPageSubCommunityListComponent); comp = fixture.componentInstance; + comp.community = mockCommunity; + }); - it('should display a list of subCommunities', () => { - comp.community = mockCommunity; + it('should display a list of sub-communities', () => { + subCommList = subcommunities; fixture.detectChanges(); const subComList = fixture.debugElement.queryAll(By.css('li')); - expect(subComList.length).toEqual(2); + expect(subComList.length).toEqual(5); expect(subComList[0].nativeElement.textContent).toContain('SubCommunity 1'); expect(subComList[1].nativeElement.textContent).toContain('SubCommunity 2'); + expect(subComList[2].nativeElement.textContent).toContain('SubCommunity 3'); + expect(subComList[3].nativeElement.textContent).toContain('SubCommunity 4'); + expect(subComList[4].nativeElement.textContent).toContain('SubCommunity 5'); }); - it('should not display the header when subCommunities are empty', () => { - comp.community = emptySubCommunitiesCommunity; + it('should not display the header when list of sub-communities is empty', () => { + subCommList = []; fixture.detectChanges(); const subComHead = fixture.debugElement.queryAll(By.css('h2')); expect(subComHead.length).toEqual(0); }); + + it('should update list of sub-communities on pagination change', () => { + subCommList = subcommunities; + fixture.detectChanges(); + + const pagination = Object.create({ + pagination:{ + id: comp.pageId, + currentPage: 2, + pageSize: 5 + }, + sort: { + field: 'dc.title', + direction: 'ASC' + } + }); + comp.onPaginationChange(pagination); + fixture.detectChanges(); + + const collList = fixture.debugElement.queryAll(By.css('li')); + expect(collList.length).toEqual(2); + expect(collList[0].nativeElement.textContent).toContain('SubCommunity 6'); + expect(collList[1].nativeElement.textContent).toContain('SubCommunity 7'); + }); }); diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts index 91f6d7bac1..1bd664021e 100644 --- a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts +++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts @@ -1,26 +1,82 @@ import { Component, Input, OnInit } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { take } from 'rxjs/operators'; + import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; - import { fadeIn } from '../../shared/animations/fade'; import { PaginatedList } from '../../core/data/paginated-list'; -import {Observable} from 'rxjs'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { CommunityDataService } from '../../core/data/community-data.service'; @Component({ selector: 'ds-community-page-sub-community-list', styleUrls: ['./community-page-sub-community-list.component.scss'], templateUrl: './community-page-sub-community-list.component.html', - animations:[fadeIn] + animations: [fadeIn] }) /** * Component to render the sub-communities of a Community */ export class CommunityPageSubCommunityListComponent implements OnInit { @Input() community: Community; - subCommunitiesRDObs: Observable>>; + + /** + * The pagination configuration + */ + config: PaginationComponentOptions; + + /** + * The pagination id + */ + pageId = 'community-subCommunities-pagination'; + + /** + * The sorting configuration + */ + sortConfig: SortOptions; + + /** + * A list of remote data objects of communities' collections + */ + subCommunitiesRDObs: BehaviorSubject>> = new BehaviorSubject>>({} as any); + + constructor(private cds: CommunityDataService) { + } ngOnInit(): void { - this.subCommunitiesRDObs = this.community.subcommunities; + this.config = new PaginationComponentOptions(); + this.config.id = this.pageId; + this.config.pageSize = 5; + this.config.currentPage = 1; + this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); + this.updatePage(); + } + + /** + * Called when one of the pagination settings is changed + * @param event The new pagination data + */ + onPaginationChange(event) { + this.config.currentPage = event.pagination.currentPage; + this.config.pageSize = event.pagination.pageSize; + this.sortConfig.field = event.sort.field; + this.sortConfig.direction = event.sort.direction; + this.updatePage(); + } + + /** + * Update the list of sub-communities + */ + updatePage() { + this.cds.findByParent(this.community.id, { + currentPage: this.config.currentPage, + elementsPerPage: this.config.pageSize, + sort: { field: this.sortConfig.field, direction: this.sortConfig.direction } + }).pipe(take(1)).subscribe((results) => { + this.subCommunitiesRDObs.next(results); + }); } } diff --git a/src/app/+home-page/home-page.component.ts b/src/app/+home-page/home-page.component.ts index 1b915ae683..65caa01430 100644 --- a/src/app/+home-page/home-page.component.ts +++ b/src/app/+home-page/home-page.component.ts @@ -11,14 +11,14 @@ import { Site } from '../core/shared/site.model'; }) export class HomePageComponent implements OnInit { - site$:Observable; + site$: Observable; constructor( - private route:ActivatedRoute, + private route: ActivatedRoute, ) { } - ngOnInit():void { + ngOnInit(): void { this.site$ = this.route.data.pipe( map((data) => data.site as Site), ); diff --git a/src/app/+home-page/home-page.resolver.ts b/src/app/+home-page/home-page.resolver.ts index 1145d1d013..6b63a4e782 100644 --- a/src/app/+home-page/home-page.resolver.ts +++ b/src/app/+home-page/home-page.resolver.ts @@ -10,7 +10,7 @@ import { take } from 'rxjs/operators'; */ @Injectable() export class HomePageResolver implements Resolve { - constructor(private siteService:SiteDataService) { + constructor(private siteService: SiteDataService) { } /** @@ -19,7 +19,7 @@ export class HomePageResolver implements Resolve { * @param {RouterStateSnapshot} state The current RouterStateSnapshot * @returns Observable Emits the found Site object, or an error if something went wrong */ - resolve(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):Observable | Promise | Site { + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | Site { return this.siteService.find().pipe(take(1)); } } diff --git a/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts b/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts new file mode 100644 index 0000000000..fa164fe624 --- /dev/null +++ b/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts @@ -0,0 +1,161 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { By } from '@angular/platform-browser'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { TopLevelCommunityListComponent } from './top-level-community-list.component'; +import { Community } from '../../core/shared/community.model'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { SharedModule } from '../../shared/shared.module'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { FindListOptions } from '../../core/data/request.models'; +import { HostWindowService } from '../../shared/host-window.service'; +import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; + +describe('TopLevelCommunityList Component', () => { + let comp: TopLevelCommunityListComponent; + let fixture: ComponentFixture; + let communityDataServiceStub: any; + + const topCommList = [Object.assign(new Community(), { + id: '123456789-1', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 1' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-2', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 2' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-3', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 3' } + ] + } + }), + Object.assign(new Community(), { + id: '12345678942', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 4' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-5', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 5' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-6', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 6' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-7', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 7' } + ] + } + }) + ]; + + communityDataServiceStub = { + findTop(options: FindListOptions = {}) { + let currentPage = options.currentPage; + let elementsPerPage = options.elementsPerPage; + if (currentPage === undefined) { + currentPage = 1 + } + elementsPerPage = 5; + + const startPageIndex = (currentPage - 1) * elementsPerPage; + let endPageIndex = (currentPage * elementsPerPage); + if (endPageIndex > topCommList.length) { + endPageIndex = topCommList.length; + } + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), topCommList.slice(startPageIndex, endPageIndex))); + + } + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + SharedModule, + RouterTestingModule.withRoutes([]), + NgbModule.forRoot(), + NoopAnimationsModule + ], + declarations: [TopLevelCommunityListComponent], + providers: [ + { provide: CommunityDataService, useValue: communityDataServiceStub }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + { provide: SelectableListService, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TopLevelCommunityListComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + + }); + + it('should display a list of top-communities', () => { + const subComList = fixture.debugElement.queryAll(By.css('li')); + + expect(subComList.length).toEqual(5); + expect(subComList[0].nativeElement.textContent).toContain('TopCommunity 1'); + expect(subComList[1].nativeElement.textContent).toContain('TopCommunity 2'); + expect(subComList[2].nativeElement.textContent).toContain('TopCommunity 3'); + expect(subComList[3].nativeElement.textContent).toContain('TopCommunity 4'); + expect(subComList[4].nativeElement.textContent).toContain('TopCommunity 5'); + }); + + it('should update list of top-communities on pagination change', () => { + const pagination = Object.create({ + pagination:{ + id: comp.pageId, + currentPage: 2, + pageSize: 5 + }, + sort: { + field: 'dc.title', + direction: 'ASC' + } + }); + comp.onPaginationChange(pagination); + fixture.detectChanges(); + + const collList = fixture.debugElement.queryAll(By.css('li')); + expect(collList.length).toEqual(2); + expect(collList[0].nativeElement.textContent).toContain('TopCommunity 6'); + expect(collList[1].nativeElement.textContent).toContain('TopCommunity 7'); + }); +}); diff --git a/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts b/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts index 1115d785a3..02c3cb54a0 100644 --- a/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts +++ b/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts @@ -1,15 +1,15 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { BehaviorSubject, Observable } from 'rxjs'; + +import { BehaviorSubject } from 'rxjs'; +import { take } from 'rxjs/operators'; + import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { CommunityDataService } from '../../core/data/community-data.service'; import { PaginatedList } from '../../core/data/paginated-list'; - import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; - import { fadeInOut } from '../../shared/animations/fade'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; -import { take } from 'rxjs/operators'; /** * this component renders the Top-Level Community list @@ -33,6 +33,11 @@ export class TopLevelCommunityListComponent implements OnInit { */ config: PaginationComponentOptions; + /** + * The pagination id + */ + pageId = 'top-level-pagination'; + /** * The sorting configuration */ @@ -40,7 +45,7 @@ export class TopLevelCommunityListComponent implements OnInit { constructor(private cds: CommunityDataService) { this.config = new PaginationComponentOptions(); - this.config.id = 'top-level-pagination'; + this.config.id = this.pageId; this.config.pageSize = 5; this.config.currentPage = 1; this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); @@ -55,10 +60,10 @@ export class TopLevelCommunityListComponent implements OnInit { * @param event The new pagination data */ onPaginationChange(event) { - this.config.currentPage = event.page; - this.config.pageSize = event.pageSize; - this.sortConfig.field = event.sortField; - this.sortConfig.direction = event.sortDirection; + this.config.currentPage = event.pagination.currentPage; + this.config.pageSize = event.pagination.pageSize; + this.sortConfig.field = event.sort.field; + this.sortConfig.direction = event.sort.direction; this.updatePage(); } diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts index 302ebf68a7..ee9d2cda27 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts @@ -70,5 +70,4 @@ export class EditRelationshipComponent implements OnChanges { canUndo(): boolean { return this.fieldUpdate.changeType >= 0; } - } diff --git a/src/app/+lookup-by-id/lookup-guard.ts b/src/app/+lookup-by-id/lookup-guard.ts index c89e329241..a7ddffcd4e 100644 --- a/src/app/+lookup-by-id/lookup-guard.ts +++ b/src/app/+lookup-by-id/lookup-guard.ts @@ -18,7 +18,7 @@ export class LookupGuard implements CanActivate { constructor(private dsoService: DsoRedirectDataService) { } - canActivate(route: ActivatedRouteSnapshot, state:RouterStateSnapshot): Observable { + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { const params = this.getLookupParams(route); return this.dsoService.findById(params.id, params.type).pipe( map((response: RemoteData) => response.hasFailed) diff --git a/src/app/+search-page/configuration-search-page.component.ts b/src/app/+search-page/configuration-search-page.component.ts index 33d99a9cd2..2cde216c05 100644 --- a/src/app/+search-page/configuration-search-page.component.ts +++ b/src/app/+search-page/configuration-search-page.component.ts @@ -5,10 +5,10 @@ import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angu import { pushInOut } from '../shared/animations/push'; import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; -import { Router } from '@angular/router'; import { hasValue } from '../shared/empty.util'; import { RouteService } from '../core/services/route.service'; import { SearchService } from '../core/shared/search/search.service'; +import { Router } from '@angular/router'; /** * This component renders a search page using a configuration as input. @@ -61,5 +61,8 @@ export class ConfigurationSearchPageComponent extends SearchComponent implements if (hasValue(this.configuration)) { this.routeService.setParameter('configuration', this.configuration); } + if (hasValue(this.fixedFilterQuery)) { + this.routeService.setParameter('fixedFilter', this.fixedFilterQuery); + } } } diff --git a/src/app/+search-page/search-page.module.ts b/src/app/+search-page/search-page.module.ts index e5ce670013..456446781c 100644 --- a/src/app/+search-page/search-page.module.ts +++ b/src/app/+search-page/search-page.module.ts @@ -3,12 +3,17 @@ import { CommonModule } from '@angular/common'; import { CoreModule } from '../core/core.module'; import { SharedModule } from '../shared/shared.module'; import { SearchPageRoutingModule } from './search-page-routing.module'; -import { SearchPageComponent } from './search-page.component'; +import { SearchComponent } from './search.component'; +import { SidebarService } from '../shared/sidebar/sidebar.service'; +import { EffectsModule } from '@ngrx/effects'; import { ConfigurationSearchPageComponent } from './configuration-search-page.component'; import { ConfigurationSearchPageGuard } from './configuration-search-page.guard'; import { SearchTrackerComponent } from './search-tracker.component'; import { StatisticsModule } from '../statistics/statistics.module'; -import { SearchComponent } from './search.component'; +import { SearchPageComponent } from './search-page.component'; +import { SidebarFilterService } from '../shared/sidebar/filter/sidebar-filter.service'; +import { SearchFilterService } from '../core/shared/search/search-filter.service'; +import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; const components = [ SearchPageComponent, @@ -25,8 +30,14 @@ const components = [ CoreModule.forRoot(), StatisticsModule.forRoot(), ], - providers: [ConfigurationSearchPageGuard], declarations: components, + providers: [ + SidebarService, + SidebarFilterService, + SearchFilterService, + ConfigurationSearchPageGuard, + SearchConfigurationService + ], exports: components }) diff --git a/src/app/+search-page/search-tracker.component.ts b/src/app/+search-page/search-tracker.component.ts index 58867e3f03..7e5aa49165 100644 --- a/src/app/+search-page/search-tracker.component.ts +++ b/src/app/+search-page/search-tracker.component.ts @@ -9,10 +9,10 @@ import { RouteService } from '../core/services/route.service'; import { hasValue } from '../shared/empty.util'; import { SearchSuccessResponse } from '../core/cache/response.models'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; -import { Router } from '@angular/router'; import { SearchService } from '../core/shared/search/search.service'; import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model'; import { SearchQueryResponse } from '../shared/search/search-query-response.model'; +import { Router } from '@angular/router'; /** * This component triggers a page view statistic @@ -42,7 +42,7 @@ export class SearchTrackerComponent extends SearchComponent implements OnInit { super(service, sidebarService, windowService, searchConfigService, routeService, router); } - ngOnInit():void { + ngOnInit(): void { // super.ngOnInit(); this.getSearchOptions().pipe( switchMap((options) => this.service.searchEntries(options) @@ -62,7 +62,7 @@ export class SearchTrackerComponent extends SearchComponent implements OnInit { .subscribe((entry) => { const config: PaginatedSearchOptions = entry.searchOptions; const searchQueryResponse: SearchQueryResponse = entry.response; - const filters:Array<{ filter: string, operator: string, value: string, label: string; }> = []; + const filters: Array<{ filter: string, operator: string, value: string, label: string; }> = []; const appliedFilters = searchQueryResponse.appliedFilters || []; for (let i = 0, filtersLength = appliedFilters.length; i < filtersLength; i++) { const appliedFilter = appliedFilters[i]; diff --git a/src/app/+search-page/search.component.html b/src/app/+search-page/search.component.html index f3731607db..36879f33d4 100644 --- a/src/app/+search-page/search.component.html +++ b/src/app/+search-page/search.component.html @@ -46,9 +46,9 @@ [scopes]="(scopeListRD$ | async)" [inPlaceSearch]="inPlaceSearch"> -
-
- -
+
+
+
+
diff --git a/src/app/+search-page/search.component.ts b/src/app/+search-page/search.component.ts index bfb99755d8..b27ebf625f 100644 --- a/src/app/+search-page/search.component.ts +++ b/src/app/+search-page/search.component.ts @@ -11,9 +11,9 @@ import { hasValue, isNotEmpty } from '../shared/empty.util'; import { getSucceededRemoteData } from '../core/shared/operators'; import { RouteService } from '../core/services/route.service'; import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component'; -import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; -import { SearchResult } from '../shared/search/search-result.model'; import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model'; +import { SearchResult } from '../shared/search/search-result.model'; +import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; import { SearchService } from '../core/shared/search/search.service'; import { currentPath } from '../shared/utils/route.utils'; import { Router } from '@angular/router'; diff --git a/src/app/core/auth/auth.interceptor.ts b/src/app/core/auth/auth.interceptor.ts index da760b8faa..08e892bbd9 100644 --- a/src/app/core/auth/auth.interceptor.ts +++ b/src/app/core/auth/auth.interceptor.ts @@ -56,7 +56,7 @@ export class AuthInterceptor implements HttpInterceptor { return http.url && http.url.endsWith('/authn/logout'); } - private makeAuthStatusObject(authenticated:boolean, accessToken?: string, error?: string): AuthStatus { + private makeAuthStatusObject(authenticated: boolean, accessToken?: string, error?: string): AuthStatus { const authStatus = new AuthStatus(); authStatus.id = null; authStatus.okay = true; diff --git a/src/app/core/cache/models/normalized-external-source-entry.model.ts b/src/app/core/cache/models/normalized-external-source-entry.model.ts new file mode 100644 index 0000000000..e8e3c695c3 --- /dev/null +++ b/src/app/core/cache/models/normalized-external-source-entry.model.ts @@ -0,0 +1,36 @@ +import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; +import { NormalizedObject } from './normalized-object.model'; +import { ExternalSourceEntry } from '../../shared/external-source-entry.model'; +import { mapsTo } from '../builders/build-decorators'; +import { MetadataMap, MetadataMapSerializer } from '../../shared/metadata.models'; + +/** + * Normalized model class for an external source entry + */ +@mapsTo(ExternalSourceEntry) +@inheritSerialization(NormalizedObject) +export class NormalizedExternalSourceEntry extends NormalizedObject { + /** + * Unique identifier + */ + @autoserialize + id: string; + + /** + * The value to display + */ + @autoserialize + display: string; + + /** + * The value to store the entry with + */ + @autoserialize + value: string; + + /** + * Metadata of the entry + */ + @autoserializeAs(MetadataMapSerializer) + metadata: MetadataMap; +} diff --git a/src/app/core/cache/models/normalized-external-source.model.ts b/src/app/core/cache/models/normalized-external-source.model.ts new file mode 100644 index 0000000000..fd9a42fb72 --- /dev/null +++ b/src/app/core/cache/models/normalized-external-source.model.ts @@ -0,0 +1,29 @@ +import { autoserialize, inheritSerialization } from 'cerialize'; +import { NormalizedObject } from './normalized-object.model'; +import { ExternalSource } from '../../shared/external-source.model'; +import { mapsTo } from '../builders/build-decorators'; + +/** + * Normalized model class for an external source + */ +@mapsTo(ExternalSource) +@inheritSerialization(NormalizedObject) +export class NormalizedExternalSource extends NormalizedObject { + /** + * Unique identifier + */ + @autoserialize + id: string; + + /** + * The name of this external source + */ + @autoserialize + name: string; + + /** + * Is the source hierarchical? + */ + @autoserialize + hierarchical: boolean; +} diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 4fdef02357..efd83d33d5 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -136,6 +136,10 @@ import { SearchConfigurationService } from './shared/search/search-configuration import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service'; import { RelationshipTypeService } from './data/relationship-type.service'; import { SidebarService } from '../shared/sidebar/sidebar.service'; +import { NormalizedExternalSource } from './cache/models/normalized-external-source.model'; +import { NormalizedExternalSourceEntry } from './cache/models/normalized-external-source-entry.model'; +import { ExternalSourceService } from './data/external-source.service'; +import { LookupRelationService } from './data/lookup-relation.service'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -247,6 +251,8 @@ const PROVIDERS = [ SearchConfigurationService, SelectableListService, RelationshipTypeService, + ExternalSourceService, + LookupRelationService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, @@ -292,7 +298,9 @@ export const normalizedModels = NormalizedPoolTask, NormalizedRelationship, NormalizedRelationshipType, - NormalizedItemType + NormalizedItemType, + NormalizedExternalSource, + NormalizedExternalSourceEntry ]; @NgModule({ diff --git a/src/app/core/data/change-analyzer.ts b/src/app/core/data/change-analyzer.ts index 6b5a69259b..c45c9e55b7 100644 --- a/src/app/core/data/change-analyzer.ts +++ b/src/app/core/data/change-analyzer.ts @@ -17,5 +17,5 @@ export interface ChangeAnalyzer { * @param {NormalizedObject} object2 * The second object to compare */ - diff(object1: T | NormalizedObject, object2: T | NormalizedObject): Operation[]; + diff(object1: T | NormalizedObject, object2: T | NormalizedObject): Operation[]; } diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index 1ea2652813..ca5f2cc12e 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -17,6 +17,7 @@ import { NormalizedObjectBuildService } from '../cache/builders/normalized-objec import { NotificationsService } from '../../shared/notifications/notifications.service'; import { Item } from '../shared/item.model'; import * as uuidv4 from 'uuid/v4'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; const endpoint = 'https://rest.api/core'; @@ -191,8 +192,7 @@ describe('DataService', () => { dso2.self = selfLink; dso2.metadata = [{ key: 'dc.title', value: name2 }]; - spyOn(service, 'findByHref').and.returnValues(observableOf(dso)); - spyOn(objectCache, 'getObjectBySelfLink').and.returnValues(observableOf(dso)); + spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(dso)); spyOn(objectCache, 'addPatch'); }); diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index f1d76b47fd..d55b7353eb 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -37,7 +37,7 @@ import { Operation } from 'fast-json-patch'; import { ObjectCacheService } from '../cache/object-cache.service'; import { DSpaceObject } from '../shared/dspace-object.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { configureRequest, getResponseFromEntry } from '../shared/operators'; +import { configureRequest, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators'; import { ErrorResponse, RestResponse } from '../cache/response.models'; import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; @@ -248,8 +248,11 @@ export abstract class DataService { * @param {DSpaceObject} object The given object */ update(object: T): Observable> { - const oldVersion$ = this.objectCache.getObjectBySelfLink(object.self); - return oldVersion$.pipe(take(1), mergeMap((oldVersion: NormalizedObject) => { + const oldVersion$ = this.findByHref(object.self); + return oldVersion$.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + mergeMap((oldVersion: T) => { const operations = this.comparator.diff(oldVersion, object); if (isNotEmpty(operations)) { this.objectCache.addPatch(object.self, operations); @@ -257,7 +260,6 @@ export abstract class DataService { return this.findByHref(object.self); } )); - } /** diff --git a/src/app/core/data/external-source.service.spec.ts b/src/app/core/data/external-source.service.spec.ts new file mode 100644 index 0000000000..77a2a85dfd --- /dev/null +++ b/src/app/core/data/external-source.service.spec.ts @@ -0,0 +1,76 @@ +import { ExternalSourceService } from './external-source.service'; +import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { ExternalSourceEntry } from '../shared/external-source-entry.model'; +import { of as observableOf } from 'rxjs'; +import { GetRequest } from './request.models'; + +describe('ExternalSourceService', () => { + let service: ExternalSourceService; + + let requestService; + let rdbService; + let halService; + + const entries = [ + Object.assign(new ExternalSourceEntry(), { + id: '0001-0001-0001-0001', + display: 'John Doe', + value: 'John, Doe', + metadata: { + 'dc.identifier.uri': [ + { + value: 'https://orcid.org/0001-0001-0001-0001' + } + ] + } + }), + Object.assign(new ExternalSourceEntry(), { + id: '0001-0001-0001-0002', + display: 'Sampson Megan', + value: 'Sampson, Megan', + metadata: { + 'dc.identifier.uri': [ + { + value: 'https://orcid.org/0001-0001-0001-0002' + } + ] + } + }) + ]; + + function init() { + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: 'request-uuid', + configure: {} + }); + rdbService = jasmine.createSpyObj('rdbService', { + buildList: createSuccessfulRemoteDataObject$(createPaginatedList(entries)) + }); + halService = jasmine.createSpyObj('halService', { + getEndpoint: observableOf('external-sources-REST-endpoint') + }); + service = new ExternalSourceService(requestService, rdbService, undefined, undefined, undefined, halService, undefined, undefined, undefined); + } + + beforeEach(() => { + init(); + }); + + describe('getExternalSourceEntries', () => { + let result; + + beforeEach(() => { + result = service.getExternalSourceEntries('test'); + }); + + it('should configure a GetRequest', () => { + expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest)); + }); + + it('should return the entries', () => { + result.subscribe((resultRD) => { + expect(resultRD.payload.page).toBe(entries); + }); + }); + }); +}); diff --git a/src/app/core/data/external-source.service.ts b/src/app/core/data/external-source.service.ts new file mode 100644 index 0000000000..c32c13a20f --- /dev/null +++ b/src/app/core/data/external-source.service.ts @@ -0,0 +1,85 @@ +import { Injectable } from '@angular/core'; +import { DataService } from './data.service'; +import { ExternalSource } from '../shared/external-source.model'; +import { RequestService } from './request.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { FindListOptions, GetRequest } from './request.models'; +import { Observable } from 'rxjs/internal/Observable'; +import { distinctUntilChanged, map, switchMap } from 'rxjs/operators'; +import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { hasValue, isNotEmptyOperator } from '../../shared/empty.util'; +import { configureRequest } from '../shared/operators'; +import { RemoteData } from './remote-data'; +import { PaginatedList } from './paginated-list'; +import { ExternalSourceEntry } from '../shared/external-source-entry.model'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; + +/** + * A service handling all external source requests + */ +@Injectable() +export class ExternalSourceService extends DataService { + protected linkPath = 'externalsources'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + super(); + } + + /** + * Get the endpoint to browse external sources + * @param options + * @param linkPath + */ + getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable { + return this.halService.getEndpoint(linkPath); + } + + /** + * Get the endpoint for an external source's entries + * @param externalSourceId The id of the external source to fetch entries for + */ + getEntriesEndpoint(externalSourceId: string): Observable { + return this.getBrowseEndpoint().pipe( + map((href) => this.getIDHref(href, externalSourceId)), + switchMap((href) => this.halService.getEndpoint('entries', href)) + ); + } + + /** + * Get the entries for an external source + * @param externalSourceId The id of the external source to fetch entries for + * @param searchOptions The search options to limit results to + */ + getExternalSourceEntries(externalSourceId: string, searchOptions?: PaginatedSearchOptions): Observable>> { + const requestUuid = this.requestService.generateRequestId(); + + const href$ = this.getEntriesEndpoint(externalSourceId).pipe( + isNotEmptyOperator(), + distinctUntilChanged(), + map((endpoint: string) => hasValue(searchOptions) ? searchOptions.toRestUrl(endpoint) : endpoint) + ); + + href$.pipe( + map((endpoint: string) => new GetRequest(requestUuid, endpoint)), + configureRequest(this.requestService) + ).subscribe(); + + return this.rdbService.buildList(href$); + } +} diff --git a/src/app/core/data/lookup-relation.service.spec.ts b/src/app/core/data/lookup-relation.service.spec.ts new file mode 100644 index 0000000000..321fd8d218 --- /dev/null +++ b/src/app/core/data/lookup-relation.service.spec.ts @@ -0,0 +1,116 @@ +import { LookupRelationService } from './lookup-relation.service'; +import { ExternalSourceService } from './external-source.service'; +import { SearchService } from '../shared/search/search.service'; +import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { PaginatedList } from './paginated-list'; +import { PageInfo } from '../shared/page-info.model'; +import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model'; +import { SearchResult } from '../../shared/search/search-result.model'; +import { Item } from '../shared/item.model'; +import { skip, take } from 'rxjs/operators'; +import { ExternalSource } from '../shared/external-source.model'; + +describe('LookupRelationService', () => { + let service: LookupRelationService; + let externalSourceService: ExternalSourceService; + let searchService: SearchService; + + const totalExternal = 8; + const optionsWithQuery = new PaginatedSearchOptions({ query: 'test-query' }); + const relationship = Object.assign(new RelationshipOptions(), { + filter: 'test-filter', + configuration: 'test-configuration' + }); + const localResults = [ + Object.assign(new SearchResult(), { + indexableObject: Object.assign(new Item(), { + uuid: 'test-item-uuid', + handle: 'test-item-handle' + }) + }) + ]; + const externalSource = Object.assign(new ExternalSource(), { + id: 'orcidV2', + name: 'orcidV2', + hierarchical: false + }); + + function init() { + externalSourceService = jasmine.createSpyObj('externalSourceService', { + getExternalSourceEntries: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: totalExternal, totalPages: totalExternal, currentPage: 1 }), [{}])) + }); + searchService = jasmine.createSpyObj('searchService', { + search: createSuccessfulRemoteDataObject$(createPaginatedList(localResults)) + }); + service = new LookupRelationService(externalSourceService, searchService); + } + + beforeEach(() => { + init(); + }); + + describe('getLocalResults', () => { + let result; + + beforeEach(() => { + result = service.getLocalResults(relationship, optionsWithQuery); + }); + + it('should return the local results', () => { + result.subscribe((resultsRD) => { + expect(resultsRD.payload.page).toBe(localResults); + }); + }); + + it('should set the searchConfig to contain a fixedFilter and configuration', () => { + expect(service.searchConfig).toEqual(Object.assign(new PaginatedSearchOptions({}), optionsWithQuery, + { fixedFilter: relationship.filter, configuration: relationship.searchConfiguration } + )); + }); + }); + + describe('getTotalLocalResults', () => { + let result; + + beforeEach(() => { + result = service.getTotalLocalResults(relationship, optionsWithQuery); + }); + + it('should start with 0', () => { + result.pipe(take(1)).subscribe((amount) => { + expect(amount).toEqual(0) + }); + }); + + it('should return the correct total amount', () => { + result.pipe(skip(1)).subscribe((amount) => { + expect(amount).toEqual(localResults.length) + }); + }); + + it('should not set searchConfig', () => { + expect(service.searchConfig).toBeUndefined(); + }); + }); + + describe('getTotalExternalResults', () => { + let result; + + beforeEach(() => { + result = service.getTotalExternalResults(externalSource, optionsWithQuery); + }); + + it('should start with 0', () => { + result.pipe(take(1)).subscribe((amount) => { + expect(amount).toEqual(0) + }); + }); + + it('should return the correct total amount', () => { + result.pipe(skip(1)).subscribe((amount) => { + expect(amount).toEqual(totalExternal) + }); + }); + }); +}); diff --git a/src/app/core/data/lookup-relation.service.ts b/src/app/core/data/lookup-relation.service.ts new file mode 100644 index 0000000000..ad977e42dc --- /dev/null +++ b/src/app/core/data/lookup-relation.service.ts @@ -0,0 +1,94 @@ +import { ExternalSourceService } from './external-source.service'; +import { SearchService } from '../shared/search/search.service'; +import { concat, map, multicast, startWith, take, takeWhile } from 'rxjs/operators'; +import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { ReplaySubject } from 'rxjs/internal/ReplaySubject'; +import { RemoteData } from './remote-data'; +import { PaginatedList } from './paginated-list'; +import { SearchResult } from '../../shared/search/search-result.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model'; +import { Observable } from 'rxjs/internal/Observable'; +import { Item } from '../shared/item.model'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { getAllSucceededRemoteData, getRemoteDataPayload } from '../shared/operators'; +import { Injectable } from '@angular/core'; +import { ExternalSource } from '../shared/external-source.model'; +import { ExternalSourceEntry } from '../shared/external-source-entry.model'; + +/** + * A service for retrieving local and external entries information during a relation lookup + */ +@Injectable() +export class LookupRelationService { + /** + * The search config last used for retrieving local results + */ + public searchConfig: PaginatedSearchOptions; + + /** + * Pagination options for retrieving exactly one result + */ + private singleResultOptions = Object.assign(new PaginationComponentOptions(), { + id: 'single-result-options', + pageSize: 1 + }); + + constructor(protected externalSourceService: ExternalSourceService, + protected searchService: SearchService) { + } + + /** + * Retrieve the available local entries for a relationship + * @param relationship Relationship options + * @param searchOptions Search options to filter results + * @param setSearchConfig Optionally choose if we should store the used search config in a local variable (defaults to true) + */ + getLocalResults(relationship: RelationshipOptions, searchOptions: PaginatedSearchOptions, setSearchConfig = true): Observable>>> { + const newConfig = Object.assign(new PaginatedSearchOptions({}), searchOptions, + { fixedFilter: relationship.filter, configuration: relationship.searchConfiguration } + ); + if (setSearchConfig) { + this.searchConfig = newConfig; + } + return this.searchService.search(newConfig).pipe( + /* Make sure to only listen to the first x results, until loading is finished */ + /* TODO: in Rxjs 6.4.0 and up, we can replace this with takeWhile(predicate, true) - see https://stackoverflow.com/a/44644237 */ + multicast( + () => new ReplaySubject(1), + (subject) => subject.pipe( + takeWhile((rd: RemoteData>>) => rd.isLoading), + concat(subject.pipe(take(1))) + ) + ) as any + ) as Observable>>>; + } + + /** + * Calculate the total local entries available for the given relationship + * @param relationship Relationship options + * @param searchOptions Search options to filter results + */ + getTotalLocalResults(relationship: RelationshipOptions, searchOptions: PaginatedSearchOptions): Observable { + return this.getLocalResults(relationship, Object.assign(new PaginatedSearchOptions({}), searchOptions, { pagination: this.singleResultOptions }), false).pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload(), + map((results: PaginatedList>) => results.totalElements), + startWith(0) + ); + } + + /** + * Calculate the total external entries available for a given external source + * @param externalSource External Source + * @param searchOptions Search options to filter results + */ + getTotalExternalResults(externalSource: ExternalSource, searchOptions: PaginatedSearchOptions): Observable { + return this.externalSourceService.getExternalSourceEntries(externalSource.id, Object.assign(new PaginatedSearchOptions({}), searchOptions, { pagination: this.singleResultOptions })).pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload(), + map((results: PaginatedList) => results.totalElements), + startWith(0) + ); + } +} diff --git a/src/app/core/data/relationship.service.spec.ts b/src/app/core/data/relationship.service.spec.ts index b33db80fbe..9287935f59 100644 --- a/src/app/core/data/relationship.service.spec.ts +++ b/src/app/core/data/relationship.service.spec.ts @@ -123,8 +123,8 @@ describe('RelationshipService', () => { it('should clear the related items their cache', () => { expect(objectCache.remove).toHaveBeenCalledWith(relatedItem1.self); expect(objectCache.remove).toHaveBeenCalledWith(item.self); - expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.self); - expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.self); + expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.uuid); + expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.uuid); }); }); diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index b73657ce2c..d6993ebcee 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -1,46 +1,35 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { RequestService } from './request.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; -import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators'; -import { - configureRequest, - getRemoteDataPayload, - getResponseFromEntry, - getSucceededRemoteData -} from '../shared/operators'; -import { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './request.models'; -import { Observable } from 'rxjs/internal/Observable'; -import { RestResponse } from '../cache/response.models'; -import { Item } from '../shared/item.model'; -import { Relationship } from '../shared/item-relationships/relationship.model'; -import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; -import { RemoteData } from './remote-data'; +import { MemoizedSelector, select, Store } from '@ngrx/store'; import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs'; +import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators'; +import { compareArraysUsingIds, paginatedRelationsToItems, relationsToItems } from '../../+item-page/simple/item-types/shared/item-relationships-utils'; +import { AppState, keySelector } from '../../app.reducer'; +import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; +import { ReorderableRelationship } from '../../shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component'; +import { RemoveNameVariantAction, SetNameVariantAction } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions'; +import { NameVariantListState } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { configureRequest, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators'; +import { SearchParam } from '../cache/models/search-param.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './request.models'; +import { RestResponse } from '../cache/response.models'; +import { CoreState } from '../core.reducers'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; +import { RemoteData, RemoteDataState } from './remote-data'; import { PaginatedList } from './paginated-list'; import { ItemDataService } from './item-data.service'; -import { - compareArraysUsingIds, - paginatedRelationsToItems, - relationsToItems -} from '../../+item-page/simple/item-types/shared/item-relationships-utils'; -import { ObjectCacheService } from '../cache/object-cache.service'; +import { Relationship } from '../shared/item-relationships/relationship.model'; +import { Item } from '../shared/item.model'; import { DataService } from './data.service'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; -import { MemoizedSelector, select, Store } from '@ngrx/store'; -import { CoreState } from '../core.reducers'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { HttpClient, HttpHeaders } from '@angular/common/http'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; -import { SearchParam } from '../cache/models/search-param.model'; -import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; -import { AppState, keySelector } from '../../app.reducer'; -import { NameVariantListState } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer'; -import { - RemoveNameVariantAction, - SetNameVariantAction -} from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions'; +import { RequestService } from './request.service'; +import { Observable } from 'rxjs/internal/Observable'; const relationshipListsStateSelector = (state: AppState) => state.relationshipLists; @@ -140,9 +129,9 @@ export class RelationshipService extends DataService { this.findById(relationshipId).pipe( getSucceededRemoteData(), getRemoteDataPayload(), - switchMap((relationship: Relationship) => combineLatest( - relationship.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()), - relationship.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()) + switchMap((rel: Relationship) => combineLatest( + rel.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()), + rel.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()) ) ), take(1) @@ -158,10 +147,10 @@ export class RelationshipService extends DataService { */ private removeRelationshipItemsFromCache(item) { this.objectCache.remove(item.self); - this.requestService.removeByHrefSubstring(item.self); + this.requestService.removeByHrefSubstring(item.uuid); combineLatest( this.objectCache.hasBySelfLinkObservable(item.self), - this.requestService.hasByHrefObservable(item.self) + this.requestService.hasByHrefObservable(item.uuid) ).pipe( filter(([existsInOC, existsInRC]) => !existsInOC && !existsInRC), take(1), @@ -367,7 +356,7 @@ export class RelationshipService extends DataService { * @param nameVariant The name variant to set for the matching relationship */ public updateNameVariant(item1: Item, item2: Item, relationshipLabel: string, nameVariant: string): Observable> { - return this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel) + const update$: Observable> = this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel) .pipe( switchMap((relation: Relationship) => relation.relationshipType.pipe( @@ -388,14 +377,44 @@ export class RelationshipService extends DataService { } return this.update(updatedRelationship); }), - // skipWhile((relationshipRD: RemoteData) => !relationshipRD.isSuccessful) - tap((relationshipRD: RemoteData) => { - if (relationshipRD.hasSucceeded) { - this.removeRelationshipItemsFromCache(item1); - this.removeRelationshipItemsFromCache(item2); - } - }), - ) + ); + + update$.pipe( + filter((relationshipRD: RemoteData) => relationshipRD.state === RemoteDataState.RequestPending), + take(1), + ).subscribe(() => { + this.removeRelationshipItemsFromCache(item1); + this.removeRelationshipItemsFromCache(item2); + }); + + return update$ + } + + /** + * Method to update the the right or left place of a relationship + * The useLeftItem field in the reorderable relationship determines which place should be updated + * @param reoRel + */ + public updatePlace(reoRel: ReorderableRelationship): Observable> { + let updatedRelationship; + if (reoRel.useLeftItem) { + updatedRelationship = Object.assign(new Relationship(), reoRel.relationship, { rightPlace: reoRel.newIndex }); + } else { + updatedRelationship = Object.assign(new Relationship(), reoRel.relationship, { leftPlace: reoRel.newIndex }); + } + + const update$ = this.update(updatedRelationship); + + update$.pipe( + filter((relationshipRD: RemoteData) => relationshipRD.state === RemoteDataState.ResponsePending), + take(1), + ).subscribe((relationshipRD: RemoteData) => { + if (relationshipRD.state === RemoteDataState.ResponsePending) { + this.removeRelationshipItemsFromCacheByRelationship(reoRel.relationship.id); + } + }); + + return update$; } } diff --git a/src/app/core/data/site-data.service.spec.ts b/src/app/core/data/site-data.service.spec.ts index 09fa7fb457..6148135f50 100644 --- a/src/app/core/data/site-data.service.spec.ts +++ b/src/app/core/data/site-data.service.spec.ts @@ -19,12 +19,12 @@ import { PaginatedList } from './paginated-list'; import { RemoteData } from './remote-data'; describe('SiteDataService', () => { - let scheduler:TestScheduler; - let service:SiteDataService; - let halService:HALEndpointService; - let requestService:RequestService; - let rdbService:RemoteDataBuildService; - let objectCache:ObjectCacheService; + let scheduler: TestScheduler; + let service: SiteDataService; + let halService: HALEndpointService; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; const testObject = Object.assign(new Site(), { uuid: '9b4f22f4-164a-49db-8817-3316b6ee5746', @@ -33,7 +33,7 @@ describe('SiteDataService', () => { const requestUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2'; const options = Object.assign(new FindListOptions(), {}); - const getRequestEntry$ = (successful:boolean, statusCode:number, statusText:string) => { + const getRequestEntry$ = (successful: boolean, statusCode: number, statusText: string) => { return observableOf({ response: new RestResponse(successful, statusCode, statusText) } as RequestEntry); diff --git a/src/app/core/data/site-data.service.ts b/src/app/core/data/site-data.service.ts index 7550594cda..c1a1b2069b 100644 --- a/src/app/core/data/site-data.service.ts +++ b/src/app/core/data/site-data.service.ts @@ -22,47 +22,41 @@ import { getSucceededRemoteData } from '../shared/operators'; * Service responsible for handling requests related to the Site object */ @Injectable() -export class SiteDataService extends DataService { -​ +export class SiteDataService extends DataService {​ protected linkPath = 'sites'; protected forceBypassCache = false; -​ constructor( - protected requestService:RequestService, - protected rdbService:RemoteDataBuildService, - protected dataBuildService:NormalizedObjectBuildService, - protected store:Store, - protected objectCache:ObjectCacheService, - protected halService:HALEndpointService, - protected notificationsService:NotificationsService, - protected http:HttpClient, - protected comparator:DSOChangeAnalyzer, + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer, ) { super(); } -​ - /** * Get the endpoint for browsing the site object * @param {FindListOptions} options * @param {Observable} linkPath */ - getBrowseEndpoint(options:FindListOptions, linkPath?:string):Observable { + getBrowseEndpoint(options: FindListOptions, linkPath?: string): Observable { return this.halService.getEndpoint(this.linkPath); } -​ - /** * Retrieve the Site Object */ - find():Observable { + find(): Observable { return this.findAll().pipe( getSucceededRemoteData(), - map((remoteData:RemoteData>) => remoteData.payload), - map((list:PaginatedList) => list.page[0]) + map((remoteData: RemoteData>) => remoteData.payload), + map((list: PaginatedList) => list.page[0]) ); } } diff --git a/src/app/core/services/route.service.ts b/src/app/core/services/route.service.ts index 2a0df6d16a..661f4acf94 100644 --- a/src/app/core/services/route.service.ts +++ b/src/app/core/services/route.service.ts @@ -176,10 +176,20 @@ export class RouteService { ); } + /** + * Add a parameter to the current route + * @param key The parameter name + * @param value The parameter value + */ public addParameter(key, value) { this.store.dispatch(new AddParameterAction(key, value)); } + /** + * Set a parameter in the current route (overriding the previous value) + * @param key The parameter name + * @param value The parameter value + */ public setParameter(key, value) { this.store.dispatch(new SetParameterAction(key, value)); } diff --git a/src/app/core/shared/external-source-entry.model.ts b/src/app/core/shared/external-source-entry.model.ts new file mode 100644 index 0000000000..be52f96b07 --- /dev/null +++ b/src/app/core/shared/external-source-entry.model.ts @@ -0,0 +1,43 @@ +import { MetadataMap } from './metadata.models'; +import { ResourceType } from './resource-type'; +import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; +import { GenericConstructor } from './generic-constructor'; + +/** + * Model class for a single entry from an external source + */ +export class ExternalSourceEntry extends ListableObject { + static type = new ResourceType('externalSourceEntry'); + + /** + * Unique identifier + */ + id: string; + + /** + * The value to display + */ + display: string; + + /** + * The value to store the entry with + */ + value: string; + + /** + * Metadata of the entry + */ + metadata: MetadataMap; + + /** + * The link to the rest endpoint where this External Source Entry can be found + */ + self: string; + + /** + * Method that returns as which type of object this object should be rendered + */ + getRenderTypes(): Array> { + return [this.constructor as GenericConstructor]; + } +} diff --git a/src/app/core/shared/external-source.model.ts b/src/app/core/shared/external-source.model.ts new file mode 100644 index 0000000000..a158f18f5d --- /dev/null +++ b/src/app/core/shared/external-source.model.ts @@ -0,0 +1,29 @@ +import { ResourceType } from './resource-type'; +import { CacheableObject } from '../cache/object-cache.reducer'; + +/** + * Model class for an external source + */ +export class ExternalSource extends CacheableObject { + static type = new ResourceType('externalsource'); + + /** + * Unique identifier + */ + id: string; + + /** + * The name of this external source + */ + name: string; + + /** + * Is the source hierarchical? + */ + hierarchical: boolean; + + /** + * The link to the rest endpoint where this External Source can be found + */ + self: string; +} diff --git a/src/app/core/shared/search/search.service.ts b/src/app/core/shared/search/search.service.ts index f6886c268e..141f261990 100644 --- a/src/app/core/shared/search/search.service.ts +++ b/src/app/core/shared/search/search.service.ts @@ -89,9 +89,9 @@ export class SearchService implements OnDestroy { } } - getEndpoint(searchOptions?:PaginatedSearchOptions):Observable { + getEndpoint(searchOptions?: PaginatedSearchOptions): Observable { return this.halService.getEndpoint(this.searchLinkPath).pipe( - map((url:string) => { + map((url: string) => { if (hasValue(searchOptions)) { return (searchOptions as PaginatedSearchOptions).toRestUrl(url); } else { @@ -117,16 +117,15 @@ export class SearchService implements OnDestroy { * @param responseMsToLive The amount of milliseconds for the response to live in cache * @returns {Observable} Emits an observable with the request entries */ - searchEntries(searchOptions?: PaginatedSearchOptions, responseMsToLive?:number) - :Observable<{searchOptions: PaginatedSearchOptions, requestEntry: RequestEntry}> { + searchEntries(searchOptions?: PaginatedSearchOptions, responseMsToLive?: number): Observable<{searchOptions: PaginatedSearchOptions, requestEntry: RequestEntry}> { const hrefObs = this.getEndpoint(searchOptions); const requestObs = hrefObs.pipe( - map((url:string) => { + map((url: string) => { const request = new this.request(this.requestService.generateRequestId(), url); - const getResponseParserFn:() => GenericConstructor = () => { + const getResponseParserFn: () => GenericConstructor = () => { return this.parser; }; @@ -139,8 +138,8 @@ export class SearchService implements OnDestroy { configureRequest(this.requestService), ); return requestObs.pipe( - switchMap((request:RestRequest) => this.requestService.getByHref(request.href)), - map(((requestEntry:RequestEntry) => ({ + switchMap((request: RestRequest) => this.requestService.getByHref(request.href)), + map(((requestEntry: RequestEntry) => ({ searchOptions: searchOptions, requestEntry: requestEntry }))) @@ -152,16 +151,15 @@ export class SearchService implements OnDestroy { * @param searchEntries: The request entries from the search method * @returns {Observable>>>} Emits a paginated list with all search results found */ - getPaginatedResults(searchEntries:Observable<{ searchOptions:PaginatedSearchOptions, requestEntry:RequestEntry }>) - :Observable>>> { - const requestEntryObs:Observable = searchEntries.pipe( + getPaginatedResults(searchEntries: Observable<{ searchOptions: PaginatedSearchOptions, requestEntry: RequestEntry }>): Observable>>> { + const requestEntryObs: Observable = searchEntries.pipe( map((entry) => entry.requestEntry), ); // get search results from response cache - const sqrObs:Observable = requestEntryObs.pipe( + const sqrObs: Observable = requestEntryObs.pipe( filterSuccessfulResponses(), - map((response:SearchSuccessResponse) => response.results), + map((response: SearchSuccessResponse) => response.results), ); // turn dspace href from search results to effective list of DSpaceObjects diff --git a/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.ts b/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.ts index f3d0a28fda..867b5890eb 100644 --- a/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.ts +++ b/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { metadataRepresentationComponent } from '../../../../shared/metadata-representation/metadata-representation.decorator'; import { MetadataRepresentationType } from '../../../../core/shared/metadata-representation/metadata-representation.model'; import { ItemMetadataRepresentationListElementComponent } from '../../../../shared/object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component'; diff --git a/src/app/entity-groups/research-entities/research-entities.module.ts b/src/app/entity-groups/research-entities/research-entities.module.ts index 86c2a375da..cef3b4539b 100644 --- a/src/app/entity-groups/research-entities/research-entities.module.ts +++ b/src/app/entity-groups/research-entities/research-entities.module.ts @@ -25,6 +25,7 @@ import { PersonInputSuggestionsComponent } from './submission/item-list-elements import { NameVariantModalComponent } from './submission/name-variant-modal/name-variant-modal.component'; import { OrgUnitInputSuggestionsComponent } from './submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component'; import { OrgUnitSearchResultListSubmissionElementComponent } from './submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component'; +import { ExternalSourceEntryListSubmissionElementComponent } from './submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component'; const ENTRY_COMPONENTS = [ OrgUnitComponent, @@ -48,7 +49,8 @@ const ENTRY_COMPONENTS = [ PersonInputSuggestionsComponent, NameVariantModalComponent, OrgUnitSearchResultListSubmissionElementComponent, - OrgUnitInputSuggestionsComponent + OrgUnitInputSuggestionsComponent, + ExternalSourceEntryListSubmissionElementComponent ]; @NgModule({ diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html new file mode 100644 index 0000000000..55b8f38a5e --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html @@ -0,0 +1,2 @@ +
{{object.display}}
+ diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.scss b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.spec.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.spec.ts new file mode 100644 index 0000000000..fa153b8c5e --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.spec.ts @@ -0,0 +1,47 @@ +import { ExternalSourceEntryListSubmissionElementComponent } from './external-source-entry-list-submission-element.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ExternalSourceEntry } from '../../../../../core/shared/external-source-entry.model'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +describe('ExternalSourceEntryListSubmissionElementComponent', () => { + let component: ExternalSourceEntryListSubmissionElementComponent; + let fixture: ComponentFixture; + + const uri = 'https://orcid.org/0001-0001-0001-0001'; + const entry = Object.assign(new ExternalSourceEntry(), { + id: '0001-0001-0001-0001', + display: 'John Doe', + value: 'John, Doe', + metadata: { + 'dc.identifier.uri': [ + { + value: uri + } + ] + } + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ExternalSourceEntryListSubmissionElementComponent], + imports: [TranslateModule.forRoot()], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ExternalSourceEntryListSubmissionElementComponent); + component = fixture.componentInstance; + component.object = entry; + fixture.detectChanges(); + }); + + it('should display the entry\'s display value', () => { + expect(fixture.nativeElement.textContent).toContain(entry.display); + }); + + it('should display the entry\'s uri', () => { + expect(fixture.nativeElement.textContent).toContain(uri); + }); +}); diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts new file mode 100644 index 0000000000..c0512b4995 --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts @@ -0,0 +1,28 @@ +import { AbstractListableElementComponent } from '../../../../../shared/object-collection/shared/object-collection-element/abstract-listable-element.component'; +import { ExternalSourceEntry } from '../../../../../core/shared/external-source-entry.model'; +import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { Context } from '../../../../../core/shared/context.model'; +import { Component, OnInit } from '@angular/core'; +import { Metadata } from '../../../../../core/shared/metadata.utils'; +import { MetadataValue } from '../../../../../core/shared/metadata.models'; + +@listableObjectComponent(ExternalSourceEntry, ViewMode.ListElement, Context.SubmissionModal) +@Component({ + selector: 'ds-external-source-entry-list-submission-element', + styleUrls: ['./external-source-entry-list-submission-element.component.scss'], + templateUrl: './external-source-entry-list-submission-element.component.html' +}) +/** + * The component for displaying a list element of an external source entry + */ +export class ExternalSourceEntryListSubmissionElementComponent extends AbstractListableElementComponent implements OnInit { + /** + * The metadata value for the object's uri + */ + uri: MetadataValue; + + ngOnInit(): void { + this.uri = Metadata.first(this.object.metadata, 'dc.identifier.uri'); + } +} diff --git a/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.ts b/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.ts index 75817d786a..eb6f7d01ac 100644 --- a/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.ts +++ b/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.ts @@ -10,7 +10,13 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; templateUrl: './name-variant-modal.component.html', styleUrls: ['./name-variant-modal.component.scss'] }) +/** + * The component for the modal to add a name variant to an item + */ export class NameVariantModalComponent { + /** + * The name variant + */ @Input() value: string; constructor(public modal: NgbActiveModal) { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html index 144848b478..a31171d7ef 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html @@ -53,16 +53,15 @@
-
    -
  • - - - - -
  • +
      + +
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index 637da20790..22376502e7 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -1,12 +1,13 @@ import { - ChangeDetectionStrategy, + ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentFactoryResolver, ContentChildren, EventEmitter, Input, NgZone, - OnChanges, OnDestroy, + OnChanges, + OnDestroy, OnInit, Output, QueryList, @@ -49,7 +50,10 @@ import { DynamicNGBootstrapTimePickerComponent } from '@ng-dynamic-forms/ui-ng-bootstrap'; import { TranslateService } from '@ngx-translate/core'; -import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model'; +import { + Reorderable, + ReorderableRelationship +} from './existing-metadata-list-element/existing-metadata-list-element.component'; import { DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD } from './models/typeahead/dynamic-typeahead.model'; import { DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; @@ -71,9 +75,8 @@ import { DsDynamicFormArrayComponent } from './models/array-group/dynamic-form-a import { DsDynamicRelationGroupComponent } from './models/relation-group/dynamic-relation-group.components'; import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './models/relation-group/dynamic-relation-group.model'; import { DsDatePickerInlineComponent } from './models/date-picker-inline/dynamic-date-picker-inline.component'; -import { map, switchMap, take, tap } from 'rxjs/operators'; -import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs'; -import { SelectableListState } from '../../../object-list/selectable-list/selectable-list.reducer'; +import { map, startWith, switchMap, find } from 'rxjs/operators'; +import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; import { SearchResult } from '../../../search/search-result.model'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; @@ -82,23 +85,18 @@ import { SelectableListService } from '../../../object-list/selectable-list/sele import { DsDynamicDisabledComponent } from './models/disabled/dynamic-disabled.component'; import { DYNAMIC_FORM_CONTROL_TYPE_DISABLED } from './models/disabled/dynamic-disabled.model'; import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/dynamic-lookup-relation-modal.component'; -import { - getAllSucceededRemoteData, - getRemoteDataPayload, - getSucceededRemoteData -} from '../../../../core/shared/operators'; +import { getAllSucceededRemoteData, getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; import { RemoteData } from '../../../../core/data/remote-data'; import { Item } from '../../../../core/shared/item.model'; import { ItemDataService } from '../../../../core/data/item-data.service'; -import { RemoveRelationshipAction } from './relation-lookup-modal/relationship.actions'; import { Store } from '@ngrx/store'; import { AppState } from '../../../../app.reducer'; import { SubmissionObjectDataService } from '../../../../core/submission/submission-object-data.service'; import { SubmissionObject } from '../../../../core/submission/models/submission-object.model'; import { PaginatedList } from '../../../../core/data/paginated-list'; import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; -import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model'; -import { MetadataValue } from '../../../../core/shared/metadata.models'; +import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type | null { switch (model.type) { @@ -182,16 +180,14 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo @Input() hasErrorMessaging = false; @Input() layout = null as DynamicFormLayout; @Input() model: any; - relationships$: Observable>>; + reorderables$: Observable; + reorderables: ReorderableRelationship[]; hasRelationLookup: boolean; modalRef: NgbModalRef; item: Item; listId: string; searchConfig: string; - selectedValues$: Observable, - mdRep: MetadataRepresentation - }>>; + /** * List of subscriptions to unsubscribe from */ @@ -224,7 +220,8 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo private relationshipService: RelationshipService, private zone: NgZone, private store: Store, - private submissionObjectService: SubmissionObjectDataService + private submissionObjectService: SubmissionObjectDataService, + private ref: ChangeDetectorRef ) { super(componentFactoryResolver, layoutService, validationService, dynamicFormInstanceService); @@ -235,44 +232,59 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo */ ngOnInit(): void { this.hasRelationLookup = hasValue(this.model.relationship); + this.reorderables = []; if (this.hasRelationLookup) { + this.listId = 'list-' + this.model.relationship.relationshipType; const item$ = this.submissionObjectService .findById(this.model.submissionId).pipe( getAllSucceededRemoteData(), getRemoteDataPayload(), - switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()))); + switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable>) + .pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload() + ) + ) + ); this.subs.push(item$.subscribe((item) => this.item = item)); + this.reorderables$ = item$.pipe( + switchMap((item) => this.relationService.getItemRelationshipsByLabel(item, this.model.relationship.relationshipType) + .pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload(), + map((relationshipList: PaginatedList) => relationshipList.page), + startWith([]), + switchMap((relationships: Relationship[]) => + observableCombineLatest( + relationships.map((relationship: Relationship) => + relationship.leftItem.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((leftItem: Item) => { + return new ReorderableRelationship(relationship, leftItem.uuid !== this.item.uuid) + }), + ) + ))), + map((relationships: ReorderableRelationship[]) => + relationships + .sort((a: Reorderable, b: Reorderable) => { + return Math.sign(a.getPlace() - b.getPlace()); + }) + ) + ) + ) + ); + + this.subs.push(this.reorderables$.subscribe((rs) => { + this.reorderables = rs; + this.ref.detectChanges(); + })); this.relationService.getRelatedItemsByLabel(this.item, this.model.relationship.relationshipType).pipe( map((items: RemoteData>) => items.payload.page.map((item) => Object.assign(new ItemSearchResult(), { indexableObject: item }))), ).subscribe((relatedItems: Array>) => this.selectableListService.select(this.listId, relatedItems)); - - this.relationships$ = this.selectableListService.getSelectableList(this.listId).pipe( - map((listState: SelectableListState) => hasValue(listState) && hasValue(listState.selection) ? listState.selection : []), - ) as Observable>>; - this.selectedValues$ = - observableCombineLatest(item$, this.relationships$).pipe( - map(([item, relatedItems]: [Item, Array>]) => { - return relatedItems - .map((element: SearchResult) => { - const relationMD: MetadataValue = item.firstMetadata(this.model.relationship.metadataField, { value: element.indexableObject.uuid }); - if (hasValue(relationMD)) { - const metadataRepresentationMD: MetadataValue = item.firstMetadata(this.model.metadataFields, { authority: relationMD.authority }); - return { - selectedResult: element, - mdRep: Object.assign( - new ItemMetadataRepresentation(metadataRepresentationMD), - element.indexableObject - ) - }; - } - }).filter(hasValue) - } - ) - ); - } } @@ -334,12 +346,29 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo } /** - * Method to remove a selected relationship from the item - * @param object The second item in the relationship, the submitted item being the first + * Method to move a relationship inside the list of relationships + * This will update the view and update the right or left place field of the relationships in the list + * @param event */ - removeSelection(object: SearchResult) { - this.selectableListService.deselectSingle(this.listId, object); - this.store.dispatch(new RemoveRelationshipAction(this.item, object.indexableObject, this.model.relationship.relationshipType)) + moveSelection(event: CdkDragDrop) { + this.zone.runOutsideAngular(() => { + moveItemInArray(this.reorderables, event.previousIndex, event.currentIndex); + const reorderables: Reorderable[] = this.reorderables.map((reo: Reorderable, index: number) => { + reo.oldIndex = reo.getPlace(); + reo.newIndex = index; + return reo; + } + ); + observableCombineLatest( + reorderables.map((rel: ReorderableRelationship) => { + if (rel.oldIndex !== rel.newIndex) { + return this.relationshipService.updatePlace(rel); + } else { + return observableOf(undefined) as Observable>; + } + }) + ).subscribe(); + }) } /** @@ -350,4 +379,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo .filter((sub) => hasValue(sub)) .forEach((sub) => sub.unsubscribe()); } + + /** + * Prevent unnecessary rerendering so fields don't lose focus + */ + trackReorderable(index, reorderable: Reorderable) { + return hasValue(reorderable) ? reorderable.getId() : undefined; + } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html new file mode 100644 index 0000000000..960dd78767 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html @@ -0,0 +1,11 @@ +
  • + + + + + +
  • diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts new file mode 100644 index 0000000000..fa13febcd1 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.spec.ts @@ -0,0 +1,92 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ExistingMetadataListElementComponent, Reorderable, ReorderableRelationship } from './existing-metadata-list-element.component'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service'; +import { select, Store } from '@ngrx/store'; +import { Item } from '../../../../../core/shared/item.model'; +import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model'; +import { RelationshipOptions } from '../../models/relationship-options.model'; +import { createSuccessfulRemoteDataObject$ } from '../../../../testing/utils'; +import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions'; +import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; + +describe('ExistingMetadataListElementComponent', () => { + let component: ExistingMetadataListElementComponent; + let fixture: ComponentFixture; + let selectionService; + let store; + let listID; + let submissionItem; + let relationship; + let reoRel; + let metadataFields; + let relationshipOptions; + let uuid1; + let uuid2; + let relatedItem; + let leftItemRD$; + let rightItemRD$; + let relatedSearchResult; + + function init() { + uuid1 = '91ce578d-2e63-4093-8c73-3faafd716000'; + uuid2 = '0e9dba1c-e1c3-4e05-a539-446f08ef57a7'; + selectionService = jasmine.createSpyObj('selectionService', ['deselectSingle']); + store = jasmine.createSpyObj('store', ['dispatch']); + listID = '1234-listID'; + submissionItem = Object.assign(new Item(), { uuid: uuid1 }); + metadataFields = ['dc.contributor.author']; + relationshipOptions = Object.assign(new RelationshipOptions(), { relationshipType: 'isPublicationOfAuthor', filter: 'test.filter', searchConfiguration: 'personConfiguration', nameVariants: true }) + relatedItem = Object.assign(new Item(), { uuid: uuid2 }); + leftItemRD$ = createSuccessfulRemoteDataObject$(relatedItem); + rightItemRD$ = createSuccessfulRemoteDataObject$(submissionItem); + relatedSearchResult = Object.assign(new ItemSearchResult(), { indexableObject: relatedItem }); + + relationship = Object.assign(new Relationship(), { leftItem: leftItemRD$, rightItem: rightItemRD$ }); + reoRel = new ReorderableRelationship(relationship, true); + } + + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + declarations: [ExistingMetadataListElementComponent], + providers: [ + { provide: SelectableListService, useValue: selectionService }, + { provide: Store, useValue: store }, + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ExistingMetadataListElementComponent); + component = fixture.componentInstance; + component.listId = listID; + component.submissionItem = submissionItem; + component.reoRel = reoRel; + component.metadataFields = metadataFields; + component.relationshipOptions = relationshipOptions; + fixture.detectChanges(); + component.ngOnChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('removeSelection', () => { + it('should deselect the object in the selectable list service', () => { + component.removeSelection(); + expect(selectionService.deselectSingle).toHaveBeenCalledWith(listID, relatedSearchResult); + }); + + it('should dispatch a RemoveRelationshipAction', () => { + component.removeSelection(); + const action = new RemoveRelationshipAction(submissionItem, relatedItem, relationshipOptions.relationshipType); + expect(store.dispatch).toHaveBeenCalledWith(action); + + }); + }) +}); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts new file mode 100644 index 0000000000..09aaa253c6 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.ts @@ -0,0 +1,123 @@ +import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core'; +import { Item } from '../../../../../core/shared/item.model'; +import { MetadataRepresentation } from '../../../../../core/shared/metadata-representation/metadata-representation.model'; +import { getAllSucceededRemoteData, getRemoteDataPayload } from '../../../../../core/shared/operators'; +import { hasValue, isNotEmpty } from '../../../../empty.util'; +import { Subscription } from 'rxjs'; +import { filter } from 'rxjs/operators'; +import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model'; +import { MetadataValue } from '../../../../../core/shared/metadata.models'; +import { ItemMetadataRepresentation } from '../../../../../core/shared/metadata-representation/item/item-metadata-representation.model'; +import { RelationshipOptions } from '../../models/relationship-options.model'; +import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions'; +import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '../../../../../app.reducer'; +import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; + +// tslint:disable:max-classes-per-file +/** + * Abstract class that defines objects that can be reordered + */ +export abstract class Reorderable { + constructor(public oldIndex?: number, public newIndex?: number) { + } + + abstract getId(): string; + + abstract getPlace(): number; +} + +/** + * Represents a single relationship that can be reordered in a list of multiple relationships + */ +export class ReorderableRelationship extends Reorderable { + relationship: Relationship; + useLeftItem: boolean; + + constructor(relationship: Relationship, useLeftItem: boolean, oldIndex?: number, newIndex?: number) { + super(oldIndex, newIndex); + this.relationship = relationship; + this.useLeftItem = useLeftItem; + } + + getId(): string { + return this.relationship.id; + } + + getPlace(): number { + if (this.useLeftItem) { + return this.relationship.rightPlace + } else { + return this.relationship.leftPlace + } + } +} + +/** + * Represents a single existing relationship value as metadata in submission + */ +@Component({ + selector: 'ds-existing-metadata-list-element', + templateUrl: './existing-metadata-list-element.component.html', + styleUrls: ['./existing-metadata-list-element.component.scss'] +}) +export class ExistingMetadataListElementComponent implements OnChanges, OnDestroy { + @Input() listId: string; + @Input() submissionItem: Item; + @Input() reoRel: ReorderableRelationship; + @Input() metadataFields: string[]; + @Input() relationshipOptions: RelationshipOptions; + metadataRepresentation: MetadataRepresentation; + relatedItem: Item; + + /** + * List of subscriptions to unsubscribe from + */ + private subs: Subscription[] = []; + + constructor( + private selectableListService: SelectableListService, + private store: Store + ) { + } + + ngOnChanges() { + const item$ = this.reoRel.useLeftItem ? + this.reoRel.relationship.leftItem : this.reoRel.relationship.rightItem; + this.subs.push(item$.pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload(), + filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid)) + ).subscribe((item: Item) => { + this.relatedItem = item; + const relationMD: MetadataValue = this.submissionItem.firstMetadata(this.relationshipOptions.metadataField, { value: this.relatedItem.uuid }); + if (hasValue(relationMD)) { + const metadataRepresentationMD: MetadataValue = this.submissionItem.firstMetadata(this.metadataFields, { authority: relationMD.authority }); + this.metadataRepresentation = Object.assign( + new ItemMetadataRepresentation(metadataRepresentationMD), + this.relatedItem + ) + } + })); + } + + /** + * Removes the selected relationship from the list + */ + removeSelection() { + this.selectableListService.deselectSingle(this.listId, Object.assign(new ItemSearchResult(), { indexableObject: this.relatedItem })); + this.store.dispatch(new RemoveRelationshipAction(this.submissionItem, this.relatedItem, this.relationshipOptions.relationshipType)) + } + + /** + * Unsubscribe from all subscriptions + */ + ngOnDestroy(): void { + this.subs + .filter((sub) => hasValue(sub)) + .forEach((sub) => sub.unsubscribe()); + } + +} +// tslint:enable:max-classes-per-file diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.ts index 490be050ef..9032cb48cb 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.ts @@ -11,6 +11,9 @@ import { DynamicDisabledModel } from './dynamic-disabled.model'; selector: 'ds-dynamic-disabled', templateUrl: './dynamic-disabled.component.html' }) +/** + * Component for displaying a form input with a disabled property + */ export class DsDynamicDisabledComponent extends DynamicFormControlComponent { @Input() formId: string; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html index 52f983e723..46620aa00b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html @@ -7,7 +7,7 @@ - \ No newline at end of file + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts index a4f77fd364..d1b289bf11 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts @@ -13,6 +13,12 @@ import { Item } from '../../../../../core/shared/item.model'; import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; import { RelationshipOptions } from '../../models/relationship-options.model'; import { AddRelationshipAction, RemoveRelationshipAction } from './relationship.actions'; +import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service'; +import { PaginatedSearchOptions } from '../../../../search/paginated-search-options.model'; +import { ExternalSource } from '../../../../../core/shared/external-source.model'; +import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../../../testing/utils'; +import { ExternalSourceService } from '../../../../../core/data/external-source.service'; +import { LookupRelationService } from '../../../../../core/data/lookup-relation.service'; describe('DsDynamicLookupRelationModalComponent', () => { let component: DsDynamicLookupRelationModalComponent; @@ -28,6 +34,24 @@ describe('DsDynamicLookupRelationModalComponent', () => { let relationship; let nameVariant; let metadataField; + let pSearchOptions; + let externalSourceService; + let lookupRelationService; + + const externalSources = [ + Object.assign(new ExternalSource(), { + id: 'orcidV2', + name: 'orcidV2', + hierarchical: false + }), + Object.assign(new ExternalSource(), { + id: 'sherpaPublisher', + name: 'sherpaPublisher', + hierarchical: false + }) + ]; + const totalLocal = 10; + const totalExternal = 8; function init() { item = Object.assign(new Item(), { uuid: '7680ca97-e2bd-4398-bfa7-139a8673dc42', metadata: {} }); @@ -41,6 +65,14 @@ describe('DsDynamicLookupRelationModalComponent', () => { relationship = { filter: 'filter', relationshipType: 'isAuthorOfPublication', nameVariants: true } as RelationshipOptions; nameVariant = 'Doe, J.'; metadataField = 'dc.contributor.author'; + pSearchOptions = new PaginatedSearchOptions({}); + externalSourceService = jasmine.createSpyObj('externalSourceService', { + findAll: createSuccessfulRemoteDataObject$(createPaginatedList(externalSources)) + }); + lookupRelationService = jasmine.createSpyObj('lookupRelationService', { + getTotalLocalResults: observableOf(totalLocal), + getTotalExternalResults: observableOf(totalExternal) + }); } beforeEach(async(() => { @@ -49,6 +81,13 @@ describe('DsDynamicLookupRelationModalComponent', () => { declarations: [DsDynamicLookupRelationModalComponent], imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NgbModule.forRoot()], providers: [ + { + provide: SearchConfigurationService, useValue: { + paginatedSearchOptions: observableOf(pSearchOptions) + } + }, + { provide: ExternalSourceService, useValue: externalSourceService }, + { provide: LookupRelationService, useValue: lookupRelationService }, { provide: SelectableListService, useValue: selectableListService }, diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts index 7e6a9b3981..bce1f53c4d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts @@ -1,5 +1,5 @@ import { Component, NgZone, OnDestroy, OnInit } from '@angular/core'; -import { combineLatest, Observable, Subscription } from 'rxjs'; +import { combineLatest, Observable, Subscription, zip as observableZip } from 'rxjs'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { hasValue } from '../../../../empty.util'; import { map, skip, switchMap, take } from 'rxjs/operators'; @@ -11,7 +11,11 @@ import { ListableObject } from '../../../../object-collection/shared/listable-ob import { RelationshipOptions } from '../../models/relationship-options.model'; import { SearchResult } from '../../../../search/search-result.model'; import { Item } from '../../../../../core/shared/item.model'; -import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../../core/shared/operators'; +import { + getAllSucceededRemoteData, + getRemoteDataPayload, + getSucceededRemoteData +} from '../../../../../core/shared/operators'; import { AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipAction } from './relationship.actions'; import { RelationshipService } from '../../../../../core/data/relationship.service'; import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service'; @@ -20,6 +24,11 @@ import { AppState } from '../../../../../app.reducer'; import { Context } from '../../../../../core/shared/context.model'; import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model'; import { MetadataValue } from '../../../../../core/shared/metadata.models'; +import { LookupRelationService } from '../../../../../core/data/lookup-relation.service'; +import { RemoteData } from '../../../../../core/data/remote-data'; +import { PaginatedList } from '../../../../../core/data/paginated-list'; +import { ExternalSource } from '../../../../../core/shared/external-source.model'; +import { ExternalSourceService } from '../../../../../core/data/external-source.service'; @Component({ selector: 'ds-dynamic-lookup-relation-modal', @@ -37,23 +46,76 @@ import { MetadataValue } from '../../../../../core/shared/metadata.models'; * Represents a modal where the submitter can select items to be added as a certain relationship type to the object being submitted */ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy { + /** + * The label to use to display i18n messages (describing the type of relationship) + */ label: string; + + /** + * Options for searching related items + */ relationshipOptions: RelationshipOptions; + + /** + * The ID of the list to add/remove selected items to/from + */ listId: string; + + /** + * The item we're adding relationships to + */ item; + + /** + * Is the selection repeatable? + */ repeatable: boolean; + + /** + * The list of selected items + */ selection$: Observable; + + /** + * The context to display lists + */ context: Context; + + /** + * The metadata-fields describing these relationships + */ metadataFields: string; + + /** + * A map of subscriptions within this component + */ subMap: { [uuid: string]: Subscription } = {}; + /** + * A list of the available external sources configured for this relationship + */ + externalSourcesRD$: Observable>>; + + /** + * The total amount of internal items for the current options + */ + totalInternal$: Observable; + + /** + * The total amount of results for each external source using the current options + */ + totalExternal$: Observable; + constructor( public modal: NgbActiveModal, private selectableListService: SelectableListService, private relationshipService: RelationshipService, private relationshipTypeService: RelationshipTypeService, + private externalSourceService: ExternalSourceService, + private lookupRelationService: LookupRelationService, + private searchConfigService: SearchConfigurationService, private zone: NgZone, private store: Store ) { @@ -70,13 +132,19 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy this.context = Context.SubmissionModal; } - // this.setExistingNameVariants(); + this.externalSourcesRD$ = this.externalSourceService.findAll(); + + this.setTotals(); } close() { this.modal.close(); } + /** + * Select (a list of) objects and add them to the store + * @param selectableObjects + */ select(...selectableObjects: Array>) { this.zone.runOutsideAngular( () => { @@ -104,6 +172,10 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy }); } + /** + * Add a subscription updating relationships with name variants + * @param sri The search result to track name variants for + */ private addNameVariantSubscription(sri: SearchResult) { const nameVariant$ = this.relationshipService.getNameVariant(this.listId, sri.indexableObject.uuid); this.subMap[sri.indexableObject.uuid] = nameVariant$.pipe( @@ -111,6 +183,10 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy ).subscribe((nameVariant: string) => this.store.dispatch(new UpdateRelationshipAction(this.item, sri.indexableObject, this.relationshipOptions.relationshipType, nameVariant))) } + /** + * Deselect (a list of) objects and remove them from the store + * @param selectableObjects + */ deselect(...selectableObjects: Array>) { this.zone.runOutsideAngular( () => selectableObjects.forEach((object) => { @@ -120,6 +196,9 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy ); } + /** + * Set existing name variants for items by the item's virtual metadata + */ private setExistingNameVariants() { const virtualMDs: MetadataValue[] = this.item.allMetadata(this.metadataFields).filter((mdValue) => mdValue.isVirtual); @@ -154,6 +233,28 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy ) } + /** + * Calculate and set the total entries available for each tab + */ + setTotals() { + this.totalInternal$ = this.searchConfigService.paginatedSearchOptions.pipe( + switchMap((options) => this.lookupRelationService.getTotalLocalResults(this.relationshipOptions, options)) + ); + + const externalSourcesAndOptions$ = combineLatest( + this.externalSourcesRD$.pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload() + ), + this.searchConfigService.paginatedSearchOptions + ); + + this.totalExternal$ = externalSourcesAndOptions$.pipe( + switchMap(([sources, options]) => + observableZip(...sources.page.map((source: ExternalSource) => this.lookupRelationService.getTotalExternalResults(source, options)))) + ); + } + ngOnDestroy() { Object.values(this.subMap).forEach((subscription) => subscription.unsubscribe()); } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html new file mode 100644 index 0000000000..9536d0a5cb --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.html @@ -0,0 +1,31 @@ +
    +
    +

    {{ 'submission.sections.describe.relationship-lookup.selection-tab.settings' | translate}}

    + +
    +
    + +
    +

    {{ 'submission.sections.describe.relationship-lookup.selection-tab.title.' + externalSource.id | translate}}

    + + + + + +
    + {{ 'search.results.empty' | translate }} +
    +
    +
    +
    +
    diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts new file mode 100644 index 0000000000..62327e236e --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts @@ -0,0 +1,162 @@ +import { DsDynamicLookupRelationExternalSourceTabComponent } from './dynamic-lookup-relation-external-source-tab.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { VarDirective } from '../../../../../utils/var.directive'; +import { TranslateModule } from '@ngx-translate/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { PaginatedSearchOptions } from '../../../../../search/paginated-search-options.model'; +import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { + createFailedRemoteDataObject$, + createPaginatedList, + createPendingRemoteDataObject$, + createSuccessfulRemoteDataObject$ +} from '../../../../../testing/utils'; +import { ExternalSourceService } from '../../../../../../core/data/external-source.service'; +import { ExternalSource } from '../../../../../../core/shared/external-source.model'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { By } from '@angular/platform-browser'; +import { ExternalSourceEntry } from '../../../../../../core/shared/external-source-entry.model'; + +describe('DsDynamicLookupRelationExternalSourceTabComponent', () => { + let component: DsDynamicLookupRelationExternalSourceTabComponent; + let fixture: ComponentFixture; + let pSearchOptions; + let externalSourceService; + + const externalSource = { + id: 'orcidV2', + name: 'orcidV2', + hierarchical: false + } as ExternalSource; + const externalEntries = [ + Object.assign({ + id: '0001-0001-0001-0001', + display: 'John Doe', + value: 'John, Doe', + metadata: { + 'dc.identifier.uri': [ + { + value: 'https://orcid.org/0001-0001-0001-0001' + } + ] + } + }), + Object.assign({ + id: '0001-0001-0001-0002', + display: 'Sampson Megan', + value: 'Sampson, Megan', + metadata: { + 'dc.identifier.uri': [ + { + value: 'https://orcid.org/0001-0001-0001-0002' + } + ] + } + }), + Object.assign({ + id: '0001-0001-0001-0003', + display: 'Edwards Anna', + value: 'Edwards, Anna', + metadata: { + 'dc.identifier.uri': [ + { + value: 'https://orcid.org/0001-0001-0001-0003' + } + ] + } + }) + ] as ExternalSourceEntry[]; + + function init() { + pSearchOptions = new PaginatedSearchOptions({ + query: 'test' + }); + externalSourceService = jasmine.createSpyObj('externalSourceService', { + getExternalSourceEntries: createSuccessfulRemoteDataObject$(createPaginatedList(externalEntries)) + }); + } + + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + declarations: [DsDynamicLookupRelationExternalSourceTabComponent, VarDirective], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), BrowserAnimationsModule], + providers: [ + { + provide: SearchConfigurationService, useValue: { + paginatedSearchOptions: observableOf(pSearchOptions) + } + }, + { provide: ExternalSourceService, useValue: externalSourceService } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DsDynamicLookupRelationExternalSourceTabComponent); + component = fixture.componentInstance; + component.externalSource = externalSource; + fixture.detectChanges(); + }); + + describe('when the external entries finished loading successfully', () => { + it('should display a ds-viewable-collection component', () => { + const collection = fixture.debugElement.query(By.css('ds-viewable-collection')); + expect(collection).toBeDefined(); + }); + }); + + describe('when the external entries are loading', () => { + beforeEach(() => { + component.entriesRD$ = createPendingRemoteDataObject$(undefined); + fixture.detectChanges(); + }); + + it('should not display a ds-viewable-collection component', () => { + const collection = fixture.debugElement.query(By.css('ds-viewable-collection')); + expect(collection).toBeNull(); + }); + + it('should display a ds-loading component', () => { + const loading = fixture.debugElement.query(By.css('ds-loading')); + expect(loading).not.toBeNull(); + }); + }); + + describe('when the external entries failed loading', () => { + beforeEach(() => { + component.entriesRD$ = createFailedRemoteDataObject$(undefined); + fixture.detectChanges(); + }); + + it('should not display a ds-viewable-collection component', () => { + const collection = fixture.debugElement.query(By.css('ds-viewable-collection')); + expect(collection).toBeNull(); + }); + + it('should display a ds-error component', () => { + const error = fixture.debugElement.query(By.css('ds-error')); + expect(error).not.toBeNull(); + }); + }); + + describe('when the external entries return an empty list', () => { + beforeEach(() => { + component.entriesRD$ = createSuccessfulRemoteDataObject$(createPaginatedList([])); + fixture.detectChanges(); + }); + + it('should not display a ds-viewable-collection component', () => { + const collection = fixture.debugElement.query(By.css('ds-viewable-collection')); + expect(collection).toBeNull(); + }); + + it('should display a message the list is empty', () => { + const empty = fixture.debugElement.query(By.css('#empty-external-entry-list')); + expect(empty).not.toBeNull(); + }); + }); +}); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts new file mode 100644 index 0000000000..d1fa538de3 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts @@ -0,0 +1,96 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { SEARCH_CONFIG_SERVICE } from '../../../../../../+my-dspace-page/my-dspace-page.component'; +import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; +import { Router } from '@angular/router'; +import { ExternalSourceService } from '../../../../../../core/data/external-source.service'; +import { Observable } from 'rxjs/internal/Observable'; +import { RemoteData } from '../../../../../../core/data/remote-data'; +import { PaginatedList } from '../../../../../../core/data/paginated-list'; +import { ExternalSourceEntry } from '../../../../../../core/shared/external-source-entry.model'; +import { ExternalSource } from '../../../../../../core/shared/external-source.model'; +import { startWith, switchMap } from 'rxjs/operators'; +import { PaginatedSearchOptions } from '../../../../../search/paginated-search-options.model'; +import { Context } from '../../../../../../core/shared/context.model'; +import { ListableObject } from '../../../../../object-collection/shared/listable-object.model'; +import { fadeIn, fadeInOut } from '../../../../../animations/fade'; +import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model'; + +@Component({ + selector: 'ds-dynamic-lookup-relation-external-source-tab', + styleUrls: ['./dynamic-lookup-relation-external-source-tab.component.scss'], + templateUrl: './dynamic-lookup-relation-external-source-tab.component.html', + providers: [ + { + provide: SEARCH_CONFIG_SERVICE, + useClass: SearchConfigurationService + } + ], + animations: [ + fadeIn, + fadeInOut + ] +}) +/** + * The tab displaying a list of importable entries for an external source + */ +export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit { + /** + * The label to use to display i18n messages (describing the type of relationship) + */ + @Input() label: string; + + /** + * The ID of the list to add/remove selected items to/from + */ + @Input() listId: string; + + /** + * Is the selection repeatable? + */ + @Input() repeatable: boolean; + + /** + * The context to display lists + */ + @Input() context: Context; + + /** + * Send an event to deselect an object from the list + */ + @Output() deselectObject: EventEmitter = new EventEmitter(); + + /** + * Send an event to select an object from the list + */ + @Output() selectObject: EventEmitter = new EventEmitter(); + + /** + * The initial pagination to start with + */ + initialPagination = Object.assign(new PaginationComponentOptions(), { + id: 'submission-external-source-relation-list', + pageSize: 5 + }); + + /** + * The external source we're selecting entries for + */ + @Input() externalSource: ExternalSource; + + /** + * The displayed list of entries + */ + entriesRD$: Observable>>; + + constructor(private router: Router, + public searchConfigService: SearchConfigurationService, + private externalSourceService: ExternalSourceService) { + } + + ngOnInit(): void { + this.entriesRD$ = this.searchConfigService.paginatedSearchOptions.pipe( + switchMap((searchOptions: PaginatedSearchOptions) => + this.externalSourceService.getExternalSourceEntries(this.externalSource.id, searchOptions).pipe(startWith(undefined))) + ) + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts index 9402ef6d19..e26abf94c1 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts @@ -3,6 +3,7 @@ import { Actions, Effect, ofType } from '@ngrx/effects'; import { debounceTime, map, mergeMap, take, tap } from 'rxjs/operators'; import { BehaviorSubject } from 'rxjs'; import { RelationshipService } from '../../../../../core/data/relationship.service'; +import { getSucceededRemoteData } from '../../../../../core/shared/operators'; import { AddRelationshipAction, RelationshipAction, RelationshipActionTypes, RemoveRelationshipAction, UpdateRelationshipAction } from './relationship.actions'; import { Item } from '../../../../../core/shared/item.model'; import { hasNoValue, hasValue, hasValueOperator } from '../../../../empty.util'; @@ -88,7 +89,7 @@ export class RelationshipEffects { this.nameVariantUpdates[identifier] = nameVariant; } else { this.relationshipService.updateNameVariant(item1, item2, relationshipType, nameVariant) - .pipe() + .pipe(getSucceededRemoteData()) .subscribe(); } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html index 4e2da1f12b..36197b33c4 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html @@ -3,7 +3,7 @@ [resultCount]="(resultsRD$ | async)?.payload?.totalElements" [inPlaceSearch]="true" [showViewModes]="false">
    - + @@ -56,8 +56,8 @@
    - \ No newline at end of file + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.spec.ts index bd83a48237..ced6c8b88b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.spec.ts @@ -15,6 +15,8 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../../testing/utils' import { PaginatedList } from '../../../../../../core/data/paginated-list'; import { ItemSearchResult } from '../../../../../object-collection/shared/item-search-result.model'; import { Item } from '../../../../../../core/shared/item.model'; +import { ActivatedRoute } from '@angular/router'; +import { LookupRelationService } from '../../../../../../core/data/lookup-relation.service'; describe('DsDynamicLookupRelationSearchTabComponent', () => { let component: DsDynamicLookupRelationSearchTabComponent; @@ -34,6 +36,7 @@ describe('DsDynamicLookupRelationSearchTabComponent', () => { let results; let selectableListService; + let lookupRelationService; function init() { relationship = { filter: 'filter', relationshipType: 'isAuthorOfPublication', nameVariants: true } as RelationshipOptions; @@ -51,6 +54,10 @@ describe('DsDynamicLookupRelationSearchTabComponent', () => { results = new PaginatedList(undefined, [searchResult1, searchResult2, searchResult3]); selectableListService = jasmine.createSpyObj('selectableListService', ['deselect', 'select', 'deselectAll']); + lookupRelationService = jasmine.createSpyObj('lookupRelationService', { + getLocalResults: createSuccessfulRemoteDataObject$(results) + }); + lookupRelationService.searchConfig = {}; } beforeEach(async(() => { @@ -75,6 +82,8 @@ describe('DsDynamicLookupRelationSearchTabComponent', () => { } } }, + { provide: ActivatedRoute, useValue: { snapshot: { queryParams: {} } } }, + { provide: LookupRelationService, useValue: lookupRelationService } ], schemas: [NO_ERRORS_SCHEMA] }) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts index 254c4ac4ff..9484631610 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts @@ -11,14 +11,16 @@ import { RelationshipOptions } from '../../../models/relationship-options.model' import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model'; import { ListableObject } from '../../../../../object-collection/shared/listable-object.model'; import { SearchService } from '../../../../../../core/shared/search/search.service'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service'; import { hasValue, isNotEmpty } from '../../../../../empty.util'; import { concat, map, multicast, switchMap, take, takeWhile, tap } from 'rxjs/operators'; +import { DSpaceObject } from '../../../../../../core/shared/dspace-object.model'; import { getSucceededRemoteData } from '../../../../../../core/shared/operators'; import { RouteService } from '../../../../../../core/services/route.service'; import { CollectionElementLinkType } from '../../../../../object-collection/collection-element-link.type'; import { Context } from '../../../../../../core/shared/context.model'; +import { LookupRelationService } from '../../../../../../core/data/lookup-relation.service'; @Component({ selector: 'ds-dynamic-lookup-relation-search-tab', @@ -36,32 +38,87 @@ import { Context } from '../../../../../../core/shared/context.model'; * Tab for inside the lookup model that represents the items that can be used as a relationship in this submission */ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDestroy { + /** + * Options for searching related items + */ @Input() relationship: RelationshipOptions; + + /** + * The ID of the list to add/remove selected items to/from + */ @Input() listId: string; + + /** + * Is the selection repeatable? + */ @Input() repeatable: boolean; + + /** + * The list of selected items + */ @Input() selection$: Observable; + + /** + * The context to display lists + */ @Input() context: Context; + /** + * Send an event to deselect an object from the list + */ @Output() deselectObject: EventEmitter = new EventEmitter(); + + /** + * Send an event to select an object from the list + */ @Output() selectObject: EventEmitter = new EventEmitter(); + + /** + * Search results + */ resultsRD$: Observable>>>; - searchConfig: PaginatedSearchOptions; + + /** + * Are all results selected? + */ allSelected: boolean; + + /** + * Are some results selected? + */ someSelected$: Observable; + + /** + * Is it currently loading to select all results? + */ selectAllLoading: boolean; + + /** + * Subscription to unsubscribe from + */ subscription; + + /** + * The initial pagination to use + */ initialPagination = Object.assign(new PaginationComponentOptions(), { id: 'submission-relation-list', pageSize: 5 }); + + /** + * The type of links to display + */ linkTypes = CollectionElementLinkType; constructor( private searchService: SearchService, private router: Router, + private route: ActivatedRoute, private selectableListService: SelectableListService, - private searchConfigService: SearchConfigurationService, + public searchConfigService: SearchConfigurationService, private routeService: RouteService, + public lookupRelationService: LookupRelationService ) { } @@ -75,24 +132,8 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest this.someSelected$ = this.selection$.pipe(map((selection) => isNotEmpty(selection))); this.resultsRD$ = this.searchConfigService.paginatedSearchOptions.pipe( - map((options) => { - return Object.assign(new PaginatedSearchOptions({}), options, { fixedFilter: this.relationship.filter, configuration: this.relationship.searchConfiguration }) - }), - switchMap((options) => { - this.searchConfig = options; - return this.searchService.search(options).pipe( - /* Make sure to only listen to the first x results, until loading is finished */ - /* TODO: in Rxjs 6.4.0 and up, we can replace this with takeWhile(predicate, true) - see https://stackoverflow.com/a/44644237 */ - multicast( - () => new ReplaySubject(1), - (subject) => subject.pipe( - takeWhile((rd: RemoteData>>) => rd.isLoading), - concat(subject.pipe(take(1))) - ) - ) as any - ) - }) - ) as Observable>>>; + switchMap((options) => this.lookupRelationService.getLocalResults(this.relationship, options)) + ); } /** @@ -100,7 +141,7 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest */ resetRoute() { this.router.navigate([], { - queryParams: Object.assign({}, { page: 1, pageSize: this.initialPagination.pageSize }), + queryParams: Object.assign({}, { pageSize: this.initialPagination.pageSize }, this.route.snapshot.queryParams, { page: 1 }) }); } @@ -143,7 +184,7 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest currentPage: 1, pageSize: 9999 }); - const fullSearchConfig = Object.assign(this.searchConfig, { pagination: fullPagination }); + const fullSearchConfig = Object.assign(this.lookupRelationService.searchConfig, { pagination: fullPagination }); const results$ = this.searchService.search(fullSearchConfig) as Observable>>>; results$.pipe( getSucceededRemoteData(), diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.spec.ts index 203a4df0b0..18e5d3c3ab 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.spec.ts @@ -73,11 +73,6 @@ describe('DsDynamicLookupRelationSelectionTabComponent', () => { expect(component).toBeTruthy(); }); - it('should call navigate on the router when is called resetRoute', () => { - component.resetRoute(); - expect(router.navigate).toHaveBeenCalled(); - }); - it('should call navigate on the router when is called resetRoute', () => { component.selectionRD$ = createSelection([]); fixture.detectChanges(); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts index 8aa3dc3828..f4746853f6 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts @@ -29,15 +29,49 @@ import { Context } from '../../../../../../core/shared/context.model'; * Tab for inside the lookup model that represents the currently selected relationships */ export class DsDynamicLookupRelationSelectionTabComponent { + /** + * The label to use to display i18n messages (describing the type of relationship) + */ @Input() label: string; + + /** + * The ID of the list to add/remove selected items to/from + */ @Input() listId: string; + + /** + * Is the selection repeatable? + */ @Input() repeatable: boolean; + + /** + * The list of selected items + */ @Input() selection$: Observable; + + /** + * The paginated list of selected items + */ @Input() selectionRD$: Observable>>; + + /** + * The context to display lists + */ @Input() context: Context; + + /** + * Send an event to deselect an object from the list + */ @Output() deselectObject: EventEmitter = new EventEmitter(); + + /** + * Send an event to select an object from the list + */ @Output() selectObject: EventEmitter = new EventEmitter(); + /** + * The initial pagination to use + */ initialPagination = Object.assign(new PaginationComponentOptions(), { id: 'submission-relation-list', pageSize: 5 @@ -51,7 +85,6 @@ export class DsDynamicLookupRelationSelectionTabComponent { * Set up the selection and pagination on load */ ngOnInit() { - this.resetRoute(); this.selectionRD$ = this.searchConfigService.paginatedSearchOptions .pipe( map((options: PaginatedSearchOptions) => options.pagination), @@ -75,13 +108,4 @@ export class DsDynamicLookupRelationSelectionTabComponent { }) ) } - - /** - * Method to reset the route when the window is opened to make sure no strange pagination issues appears - */ - resetRoute() { - this.router.navigate([], { - queryParams: Object.assign({}, { page: 1, pageSize: this.initialPagination.pageSize }), - }); - } } diff --git a/src/app/shared/lang-switch/lang-switch.component.spec.ts b/src/app/shared/lang-switch/lang-switch.component.spec.ts index 5b10578f77..3d7aca46b6 100644 --- a/src/app/shared/lang-switch/lang-switch.component.spec.ts +++ b/src/app/shared/lang-switch/lang-switch.component.spec.ts @@ -78,7 +78,7 @@ describe('LangSwitchComponent', () => { }).compileComponents() .then(() => { translate = TestBed.get(TranslateService); - translate.addLangs(mockConfig.languages.filter((langConfig:LangConfig) => langConfig.active === true).map((a) => a.code)); + translate.addLangs(mockConfig.languages.filter((langConfig: LangConfig) => langConfig.active === true).map((a) => a.code)); translate.setDefaultLang('en'); translate.use('en'); http = TestBed.get(HttpTestingController); diff --git a/src/app/shared/mocks/mock-angulartics.service.ts b/src/app/shared/mocks/mock-angulartics.service.ts index 5581e183d1..a7516eb44a 100644 --- a/src/app/shared/mocks/mock-angulartics.service.ts +++ b/src/app/shared/mocks/mock-angulartics.service.ts @@ -1,5 +1,5 @@ /* tslint:disable:no-empty */ export class AngularticsMock { public eventTrack(action, properties) { } - public startTracking():void {} + public startTracking(): void {} } diff --git a/src/app/shared/pagination/pagination.component.ts b/src/app/shared/pagination/pagination.component.ts index 014f01f152..9c378d1aff 100644 --- a/src/app/shared/pagination/pagination.component.ts +++ b/src/app/shared/pagination/pagination.component.ts @@ -225,10 +225,14 @@ export class PaginationComponent implements OnDestroy, OnInit { } /** + * @param cdRef + * ChangeDetectorRef is a singleton service provided by Angular. * @param route * Route is a singleton service provided by Angular. * @param router * Router is a singleton service provided by Angular. + * @param hostWindowService + * the HostWindowService singleton. */ constructor(private cdRef: ChangeDetectorRef, private route: ActivatedRoute, @@ -243,7 +247,7 @@ export class PaginationComponent implements OnDestroy, OnInit { * The page being navigated to. */ public doPageChange(page: number) { - this.updateRoute({ page: page.toString() }); + this.updateRoute({ pageId: this.id, page: page.toString() }); } /** @@ -253,7 +257,7 @@ export class PaginationComponent implements OnDestroy, OnInit { * The page size being navigated to. */ public doPageSizeChange(pageSize: number) { - this.updateRoute({ page: 1, pageSize: pageSize }); + this.updateRoute({ pageId: this.id, page: 1, pageSize: pageSize }); } /** @@ -263,7 +267,7 @@ export class PaginationComponent implements OnDestroy, OnInit { * The sort direction being navigated to. */ public doSortDirectionChange(sortDirection: SortDirection) { - this.updateRoute({ page: 1, sortDirection: sortDirection }); + this.updateRoute({ pageId: this.id, page: 1, sortDirection: sortDirection }); } /** @@ -273,7 +277,7 @@ export class PaginationComponent implements OnDestroy, OnInit { * The sort field being navigated to. */ public doSortFieldChange(field: string) { - this.updateRoute({ page: 1, sortField: field }); + this.updateRoute({ pageId: this.id, page: 1, sortField: field }); } /** @@ -413,27 +417,30 @@ export class PaginationComponent implements OnDestroy, OnInit { * Method to update all pagination variables to the current query parameters */ private setFields() { - // (+) converts string to a number - const page = this.currentQueryParams.page; - if (this.currentPage !== +page) { - this.setPage(+page); - } + // set fields only when page id is the one configured for this pagination instance + if (this.currentQueryParams.pageId === this.id) { + // (+) converts string to a number + const page = this.currentQueryParams.page; + if (this.currentPage !== +page) { + this.setPage(+page); + } - const pageSize = this.currentQueryParams.pageSize; - if (this.pageSize !== +pageSize) { - this.setPageSize(+pageSize); - } + const pageSize = this.currentQueryParams.pageSize; + if (this.pageSize !== +pageSize) { + this.setPageSize(+pageSize); + } - const sortDirection = this.currentQueryParams.sortDirection; - if (this.sortDirection !== sortDirection) { - this.setSortDirection(sortDirection); - } + const sortDirection = this.currentQueryParams.sortDirection; + if (this.sortDirection !== sortDirection) { + this.setSortDirection(sortDirection); + } - const sortField = this.currentQueryParams.sortField; - if (this.sortField !== sortField) { - this.setSortField(sortField); + const sortField = this.currentQueryParams.sortField; + if (this.sortField !== sortField) { + this.setSortField(sortField); + } + this.cdRef.detectChanges(); } - this.cdRef.detectChanges(); } /** diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts index 3bed8e7397..1d6a85b95b 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts @@ -24,6 +24,7 @@ import { getSucceededRemoteData } from '../../../../../core/shared/operators'; import { InputSuggestion } from '../../../../input-suggestions/input-suggestions.model'; import { SearchOptions } from '../../../search-options.model'; import { SEARCH_CONFIG_SERVICE } from '../../../../../+my-dspace-page/my-dspace-page.component'; +import { currentPath } from '../../../../utils/route.utils'; @Component({ selector: 'ds-search-facet-filter', @@ -185,7 +186,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy { */ public getSearchLink(): string { if (this.inPlaceSearch) { - return ''; + return currentPath(this.router); } return this.searchService.getSearchLink(); } diff --git a/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts b/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts index 8c6860c2d3..5de87be3bc 100644 --- a/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts +++ b/src/app/shared/search/search-labels/search-label/search-label.component.spec.ts @@ -40,7 +40,7 @@ describe('SearchLabelComponent', () => { providers: [ { provide: SearchService, useValue: new SearchServiceStub(searchLink) }, { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }, - { provide: Router, useValue: {} } + { provide: Router, useValue: {}} // { provide: SearchConfigurationService, useValue: {getCurrentFrontendFilters : () => observableOf({})} } ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index eb73514d76..89157aeee1 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -48,10 +48,7 @@ import { LogOutComponent } from './log-out/log-out.component'; import { FormComponent } from './form/form.component'; import { DsDynamicTypeaheadComponent } from './form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component'; import { DsDynamicScrollableDropdownComponent } from './form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component'; -import { - DsDynamicFormControlContainerComponent, - dsDynamicFormControlMapFn -} from './form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component'; +import { DsDynamicFormControlContainerComponent, dsDynamicFormControlMapFn } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component'; import { DsDynamicFormComponent } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form.component'; import { DYNAMIC_FORM_CONTROL_MAP_FN, DynamicFormsCoreModule } from '@ng-dynamic-forms/core'; import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap'; @@ -174,8 +171,10 @@ import { PageWithSidebarComponent } from './sidebar/page-with-sidebar.component' import { SidebarDropdownComponent } from './sidebar/sidebar-dropdown.component'; import { SidebarFilterComponent } from './sidebar/filter/sidebar-filter.component'; import { SidebarFilterSelectedOptionComponent } from './sidebar/filter/sidebar-filter-selected-option.component'; -import { MetadataRepresentationListComponent } from '../+item-page/simple/metadata-representation-list/metadata-representation-list.component'; import { SelectableListItemControlComponent } from './object-collection/shared/selectable-list-item-control/selectable-list-item-control.component'; +import { DsDynamicLookupRelationExternalSourceTabComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component'; +import { DragDropModule } from '@angular/cdk/drag-drop'; +import { ExistingMetadataListElementComponent } from './form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -198,6 +197,7 @@ const MODULES = [ MomentModule, TextMaskModule, MenuModule, + DragDropModule ]; const ROOT_MODULES = [ @@ -334,7 +334,8 @@ const COMPONENTS = [ ItemSelectComponent, CollectionSelectComponent, MetadataRepresentationLoaderComponent, - SelectableListItemControlComponent + SelectableListItemControlComponent, + ExistingMetadataListElementComponent ]; const ENTRY_COMPONENTS = [ @@ -395,7 +396,8 @@ const ENTRY_COMPONENTS = [ SearchFacetRangeOptionComponent, SearchAuthorityFilterComponent, DsDynamicLookupRelationSearchTabComponent, - DsDynamicLookupRelationSelectionTabComponent + DsDynamicLookupRelationSelectionTabComponent, + DsDynamicLookupRelationExternalSourceTabComponent ]; const SHARED_ITEM_PAGE_COMPONENTS = [ @@ -437,7 +439,8 @@ const DIRECTIVES = [ ...DIRECTIVES, ...ENTRY_COMPONENTS, ...SHARED_ITEM_PAGE_COMPONENTS, - PublicationSearchResultListElementComponent + PublicationSearchResultListElementComponent, + ExistingMetadataListElementComponent ], providers: [ ...PROVIDERS diff --git a/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.ts b/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.ts index 5c80a9cd87..4f1d2415ae 100644 --- a/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.ts +++ b/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.ts @@ -10,6 +10,6 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; * Represents a single selected option in a sidebar filter */ export class SidebarFilterSelectedOptionComponent { - @Input() label:string; - @Output() click:EventEmitter = new EventEmitter(); + @Input() label: string; + @Output() click: EventEmitter = new EventEmitter(); } diff --git a/src/app/shared/sidebar/filter/sidebar-filter.actions.ts b/src/app/shared/sidebar/filter/sidebar-filter.actions.ts index 2391274489..644bebd949 100644 --- a/src/app/shared/sidebar/filter/sidebar-filter.actions.ts +++ b/src/app/shared/sidebar/filter/sidebar-filter.actions.ts @@ -45,7 +45,7 @@ export class FilterInitializeAction extends SidebarFilterAction { type = SidebarFilterActionTypes.INITIALIZE; initiallyExpanded; - constructor(name:string, initiallyExpanded:boolean) { + constructor(name: string, initiallyExpanded: boolean) { super(name); this.initiallyExpanded = initiallyExpanded; } diff --git a/src/app/shared/sidebar/filter/sidebar-filter.component.ts b/src/app/shared/sidebar/filter/sidebar-filter.component.ts index 2a98565639..5a019d41df 100644 --- a/src/app/shared/sidebar/filter/sidebar-filter.component.ts +++ b/src/app/shared/sidebar/filter/sidebar-filter.component.ts @@ -15,13 +15,13 @@ import { slide } from '../../animations/slide'; */ export class SidebarFilterComponent implements OnInit { - @Input() name:string; - @Input() type:string; - @Input() label:string; + @Input() name: string; + @Input() type: string; + @Input() label: string; @Input() expanded = true; @Input() singleValue = false; - @Input() selectedValues:Observable; - @Output() removeValue:EventEmitter = new EventEmitter(); + @Input() selectedValues: Observable; + @Output() removeValue: EventEmitter = new EventEmitter(); /** * True when the filter is 100% collapsed in the UI @@ -31,10 +31,10 @@ export class SidebarFilterComponent implements OnInit { /** * Emits true when the filter is currently collapsed in the store */ - collapsed$:Observable; + collapsed$: Observable; constructor( - protected filterService:SidebarFilterService, + protected filterService: SidebarFilterService, ) { } @@ -49,7 +49,7 @@ export class SidebarFilterComponent implements OnInit { * Method to change this.collapsed to false when the slide animation ends and is sliding open * @param event The animation event */ - finishSlide(event:any):void { + finishSlide(event: any): void { if (event.fromState === 'collapsed') { this.closed = false; } @@ -59,13 +59,13 @@ export class SidebarFilterComponent implements OnInit { * Method to change this.collapsed to true when the slide animation starts and is sliding closed * @param event The animation event */ - startSlide(event:any):void { + startSlide(event: any): void { if (event.toState === 'collapsed') { this.closed = true; } } - ngOnInit():void { + ngOnInit(): void { this.closed = !this.expanded; this.initializeFilter(); this.collapsed$ = this.isCollapsed(); @@ -82,7 +82,7 @@ export class SidebarFilterComponent implements OnInit { * Checks if the filter is currently collapsed * @returns {Observable} Emits true when the current state of the filter is collapsed, false when it's expanded */ - private isCollapsed():Observable { + private isCollapsed(): Observable { return this.filterService.isCollapsed(this.name); } diff --git a/src/app/shared/sidebar/filter/sidebar-filter.reducer.ts b/src/app/shared/sidebar/filter/sidebar-filter.reducer.ts index d25737eaa9..672a7a2a2d 100644 --- a/src/app/shared/sidebar/filter/sidebar-filter.reducer.ts +++ b/src/app/shared/sidebar/filter/sidebar-filter.reducer.ts @@ -8,17 +8,17 @@ import { * Interface that represents the state for a single filters */ export interface SidebarFilterState { - filterCollapsed:boolean, + filterCollapsed: boolean, } /** * Interface that represents the state for all available filters */ export interface SidebarFiltersState { - [name:string]:SidebarFilterState + [name: string]: SidebarFilterState } -const initialState:SidebarFiltersState = Object.create(null); +const initialState: SidebarFiltersState = Object.create(null); /** * Performs a filter action on the current state @@ -26,7 +26,7 @@ const initialState:SidebarFiltersState = Object.create(null); * @param {SidebarFilterAction} action The action that should be performed * @returns {SidebarFiltersState} The state after the action is performed */ -export function sidebarFilterReducer(state = initialState, action:SidebarFilterAction):SidebarFiltersState { +export function sidebarFilterReducer(state = initialState, action: SidebarFilterAction): SidebarFiltersState { switch (action.type) { diff --git a/src/app/shared/sidebar/filter/sidebar-filter.service.ts b/src/app/shared/sidebar/filter/sidebar-filter.service.ts index 6dab18c5d7..b67de24f9e 100644 --- a/src/app/shared/sidebar/filter/sidebar-filter.service.ts +++ b/src/app/shared/sidebar/filter/sidebar-filter.service.ts @@ -16,7 +16,7 @@ import { hasValue } from '../../empty.util'; @Injectable() export class SidebarFilterService { - constructor(private store:Store) { + constructor(private store: Store) { } /** @@ -24,7 +24,7 @@ export class SidebarFilterService { * @param {string} filter The filter for which the action is dispatched * @param {boolean} expanded If the filter should be open from the start */ - public initializeFilter(filter:string, expanded:boolean):void { + public initializeFilter(filter: string, expanded: boolean): void { this.store.dispatch(new FilterInitializeAction(filter, expanded)); } @@ -32,7 +32,7 @@ export class SidebarFilterService { * Dispatches a collapse action to the store for a given filter * @param {string} filterName The filter for which the action is dispatched */ - public collapse(filterName:string):void { + public collapse(filterName: string): void { this.store.dispatch(new FilterCollapseAction(filterName)); } @@ -40,7 +40,7 @@ export class SidebarFilterService { * Dispatches an expand action to the store for a given filter * @param {string} filterName The filter for which the action is dispatched */ - public expand(filterName:string):void { + public expand(filterName: string): void { this.store.dispatch(new FilterExpandAction(filterName)); } @@ -48,7 +48,7 @@ export class SidebarFilterService { * Dispatches a toggle action to the store for a given filter * @param {string} filterName The filter for which the action is dispatched */ - public toggle(filterName:string):void { + public toggle(filterName: string): void { this.store.dispatch(new FilterToggleAction(filterName)); } @@ -57,10 +57,10 @@ export class SidebarFilterService { * @param {string} filterName The filtername for which the collapsed state is checked * @returns {Observable} Emits the current collapsed state of the given filter, if it's unavailable, return false */ - isCollapsed(filterName:string):Observable { + isCollapsed(filterName: string): Observable { return this.store.pipe( select(filterByNameSelector(filterName)), - map((object:SidebarFilterState) => { + map((object: SidebarFilterState) => { if (object) { return object.filterCollapsed; } else { @@ -73,14 +73,14 @@ export class SidebarFilterService { } -const filterStateSelector = (state:SidebarFiltersState) => state.sidebarFilter; +const filterStateSelector = (state: SidebarFiltersState) => state.sidebarFilter; -function filterByNameSelector(name:string):MemoizedSelector { +function filterByNameSelector(name: string): MemoizedSelector { return keySelector(name); } -export function keySelector(key:string):MemoizedSelector { - return createSelector(filterStateSelector, (state:SidebarFilterState) => { +export function keySelector(key: string): MemoizedSelector { + return createSelector(filterStateSelector, (state: SidebarFilterState) => { if (hasValue(state)) { return state[key]; } else { diff --git a/src/app/shared/sidebar/page-with-sidebar.component.spec.ts b/src/app/shared/sidebar/page-with-sidebar.component.spec.ts index 77f59090ab..e9211797a9 100644 --- a/src/app/shared/sidebar/page-with-sidebar.component.spec.ts +++ b/src/app/shared/sidebar/page-with-sidebar.component.spec.ts @@ -7,8 +7,8 @@ import { HostWindowService } from '../host-window.service'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; describe('PageWithSidebarComponent', () => { - let comp:PageWithSidebarComponent; - let fixture:ComponentFixture; + let comp: PageWithSidebarComponent; + let fixture: ComponentFixture; const sidebarService = { isCollapsed: observableOf(true), @@ -42,7 +42,7 @@ describe('PageWithSidebarComponent', () => { }); describe('when sidebarCollapsed is true in mobile view', () => { - let menu:HTMLElement; + let menu: HTMLElement; beforeEach(() => { menu = fixture.debugElement.query(By.css('#mock-id-sidebar-content')).nativeElement; @@ -58,7 +58,7 @@ describe('PageWithSidebarComponent', () => { }); describe('when sidebarCollapsed is false in mobile view', () => { - let menu:HTMLElement; + let menu: HTMLElement; beforeEach(() => { menu = fixture.debugElement.query(By.css('#mock-id-sidebar-content')).nativeElement; @@ -70,6 +70,5 @@ describe('PageWithSidebarComponent', () => { it('should open the menu', () => { expect(menu.classList).toContain('active'); }); - }); }); diff --git a/src/app/shared/sidebar/page-with-sidebar.component.ts b/src/app/shared/sidebar/page-with-sidebar.component.ts index 8b7f987a37..44fa238d3b 100644 --- a/src/app/shared/sidebar/page-with-sidebar.component.ts +++ b/src/app/shared/sidebar/page-with-sidebar.component.ts @@ -18,13 +18,13 @@ import { map } from 'rxjs/operators'; * the template outlet (inside the page-width-sidebar tags). */ export class PageWithSidebarComponent implements OnInit { - @Input() id:string; - @Input() sidebarContent:TemplateRef; + @Input() id: string; + @Input() sidebarContent: TemplateRef; /** * Emits true if were on a small screen */ - isXsOrSm$:Observable; + isXsOrSm$: Observable; /** * The width of the sidebar (bootstrap columns) @@ -35,16 +35,16 @@ export class PageWithSidebarComponent implements OnInit { /** * Observable for whether or not the sidebar is currently collapsed */ - isSidebarCollapsed$:Observable; + isSidebarCollapsed$: Observable; - sidebarClasses:Observable; + sidebarClasses: Observable; - constructor(protected sidebarService:SidebarService, - protected windowService:HostWindowService, + constructor(protected sidebarService: SidebarService, + protected windowService: HostWindowService, ) { } - ngOnInit():void { + ngOnInit(): void { this.isXsOrSm$ = this.windowService.isXsOrSm(); this.isSidebarCollapsed$ = this.isSidebarCollapsed(); this.sidebarClasses = this.isSidebarCollapsed$.pipe( @@ -56,21 +56,21 @@ export class PageWithSidebarComponent implements OnInit { * Check if the sidebar is collapsed * @returns {Observable} emits true if the sidebar is currently collapsed, false if it is expanded */ - private isSidebarCollapsed():Observable { + private isSidebarCollapsed(): Observable { return this.sidebarService.isCollapsed; } /** * Set the sidebar to a collapsed state */ - public closeSidebar():void { + public closeSidebar(): void { this.sidebarService.collapse() } /** * Set the sidebar to an expanded state */ - public openSidebar():void { + public openSidebar(): void { this.sidebarService.expand(); } diff --git a/src/app/shared/sidebar/sidebar-dropdown.component.ts b/src/app/shared/sidebar/sidebar-dropdown.component.ts index 313538eded..471d357e25 100644 --- a/src/app/shared/sidebar/sidebar-dropdown.component.ts +++ b/src/app/shared/sidebar/sidebar-dropdown.component.ts @@ -10,7 +10,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; * The options should still be provided in the content. */ export class SidebarDropdownComponent { - @Input() id:string; - @Input() label:string; - @Output() change:EventEmitter = new EventEmitter(); + @Input() id: string; + @Input() label: string; + @Output() change: EventEmitter = new EventEmitter(); } diff --git a/src/app/shared/utils/object-keys-pipe.ts b/src/app/shared/utils/object-keys-pipe.ts index fd3d018b88..1320dbc4bf 100644 --- a/src/app/shared/utils/object-keys-pipe.ts +++ b/src/app/shared/utils/object-keys-pipe.ts @@ -10,7 +10,7 @@ export class ObjectKeysPipe implements PipeTransform { * @param value An object * @returns {any} Array with all keys the input object */ - transform(value, args:string[]): any { + transform(value, args: string[]): any { const keys = []; Object.keys(value).forEach((k) => keys.push(k)); return keys; diff --git a/src/app/shared/utils/object-values-pipe.ts b/src/app/shared/utils/object-values-pipe.ts index 79efd1cb76..bb511b4e5c 100644 --- a/src/app/shared/utils/object-values-pipe.ts +++ b/src/app/shared/utils/object-values-pipe.ts @@ -10,7 +10,7 @@ export class ObjectValuesPipe implements PipeTransform { * @param value An object * @returns {any} Array with all values of the input object */ - transform(value, args:string[]): any { + transform(value, args: string[]): any { const values = []; Object.values(value).forEach((v) => values.push(v)); return values; diff --git a/src/app/statistics/angulartics/dspace-provider.spec.ts b/src/app/statistics/angulartics/dspace-provider.spec.ts index 922a3152fd..8491d8e80c 100644 --- a/src/app/statistics/angulartics/dspace-provider.spec.ts +++ b/src/app/statistics/angulartics/dspace-provider.spec.ts @@ -5,9 +5,9 @@ import { filter } from 'rxjs/operators'; import { of as observableOf } from 'rxjs'; describe('Angulartics2DSpace', () => { - let provider:Angulartics2DSpace; - let angulartics2:Angulartics2; - let statisticsService:jasmine.SpyObj; + let provider: Angulartics2DSpace; + let angulartics2: Angulartics2; + let statisticsService: jasmine.SpyObj; beforeEach(() => { angulartics2 = { diff --git a/src/app/statistics/angulartics/dspace-provider.ts b/src/app/statistics/angulartics/dspace-provider.ts index 9ab01f6023..cd1aab94bd 100644 --- a/src/app/statistics/angulartics/dspace-provider.ts +++ b/src/app/statistics/angulartics/dspace-provider.ts @@ -9,15 +9,15 @@ import { StatisticsService } from '../statistics.service'; export class Angulartics2DSpace { constructor( - private angulartics2:Angulartics2, - private statisticsService:StatisticsService, + private angulartics2: Angulartics2, + private statisticsService: StatisticsService, ) { } /** * Activates this plugin */ - startTracking():void { + startTracking(): void { this.angulartics2.eventTrack .pipe(this.angulartics2.filterDeveloperMode()) .subscribe((event) => this.eventTrack(event)); diff --git a/src/app/statistics/angulartics/dspace/view-tracker.component.ts b/src/app/statistics/angulartics/dspace/view-tracker.component.ts index 1151287ea8..85588aeb97 100644 --- a/src/app/statistics/angulartics/dspace/view-tracker.component.ts +++ b/src/app/statistics/angulartics/dspace/view-tracker.component.ts @@ -11,14 +11,14 @@ import { DSpaceObject } from '../../../core/shared/dspace-object.model'; templateUrl: './view-tracker.component.html', }) export class ViewTrackerComponent implements OnInit { - @Input() object:DSpaceObject; + @Input() object: DSpaceObject; constructor( - public angulartics2:Angulartics2 + public angulartics2: Angulartics2 ) { } - ngOnInit():void { + ngOnInit(): void { this.angulartics2.eventTrack.next({ action: 'pageView', properties: {object: this.object}, diff --git a/src/app/statistics/statistics.module.ts b/src/app/statistics/statistics.module.ts index a67ff7613c..58ac1f07ab 100644 --- a/src/app/statistics/statistics.module.ts +++ b/src/app/statistics/statistics.module.ts @@ -25,7 +25,7 @@ import { StatisticsService } from './statistics.service'; * This module handles the statistics */ export class StatisticsModule { - static forRoot():ModuleWithProviders { + static forRoot(): ModuleWithProviders { return { ngModule: StatisticsModule, providers: [ diff --git a/src/app/statistics/statistics.service.spec.ts b/src/app/statistics/statistics.service.spec.ts index c6cc4c10b5..1d659aac2b 100644 --- a/src/app/statistics/statistics.service.spec.ts +++ b/src/app/statistics/statistics.service.spec.ts @@ -8,10 +8,10 @@ import { DSpaceObjectType } from '../core/shared/dspace-object-type.model'; import { SearchOptions } from '../shared/search/search-options.model'; describe('StatisticsService', () => { - let service:StatisticsService; - let requestService:jasmine.SpyObj; + let service: StatisticsService; + let requestService: jasmine.SpyObj; const restURL = 'https://rest.api'; - const halService:any = new HALEndpointServiceStub(restURL); + const halService: any = new HALEndpointServiceStub(restURL); function initTestService() { return new StatisticsService( @@ -25,9 +25,9 @@ describe('StatisticsService', () => { service = initTestService(); it('should send a request to track an item view ', () => { - const mockItem:any = {uuid: 'mock-item-uuid', type: 'item'}; + const mockItem: any = {uuid: 'mock-item-uuid', type: 'item'}; service.trackViewEvent(mockItem); - const request:TrackRequest = requestService.configure.calls.mostRecent().args[0]; + const request: TrackRequest = requestService.configure.calls.mostRecent().args[0]; expect(request.body).toBeDefined('request.body'); const body = JSON.parse(request.body); expect(body.targetId).toBe('mock-item-uuid'); @@ -39,7 +39,7 @@ describe('StatisticsService', () => { requestService = getMockRequestService(); service = initTestService(); - const mockSearch:any = new SearchOptions({ + const mockSearch: any = new SearchOptions({ query: 'mock-query', }); @@ -51,7 +51,7 @@ describe('StatisticsService', () => { }; const sort = {by: 'search-field', order: 'ASC'}; service.trackSearchEvent(mockSearch, page, sort); - const request:TrackRequest = requestService.configure.calls.mostRecent().args[0]; + const request: TrackRequest = requestService.configure.calls.mostRecent().args[0]; const body = JSON.parse(request.body); it('should specify the right query', () => { @@ -79,7 +79,7 @@ describe('StatisticsService', () => { requestService = getMockRequestService(); service = initTestService(); - const mockSearch:any = new SearchOptions({ + const mockSearch: any = new SearchOptions({ query: 'mock-query', configuration: 'mock-configuration', dsoType: DSpaceObjectType.ITEM, @@ -108,7 +108,7 @@ describe('StatisticsService', () => { } ]; service.trackSearchEvent(mockSearch, page, sort, filters); - const request:TrackRequest = requestService.configure.calls.mostRecent().args[0]; + const request: TrackRequest = requestService.configure.calls.mostRecent().args[0]; const body = JSON.parse(request.body); it('should specify the dsoType', () => { diff --git a/src/app/submission/form/collection/submission-form-collection.component.ts b/src/app/submission/form/collection/submission-form-collection.component.ts index d318bfe687..f84764d6a4 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.ts +++ b/src/app/submission/form/collection/submission-form-collection.component.ts @@ -1,8 +1,28 @@ -import { ChangeDetectorRef, Component, EventEmitter, HostListener, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; +import { + ChangeDetectorRef, + Component, + EventEmitter, + HostListener, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges +} from '@angular/core'; import { FormControl } from '@angular/forms'; import { BehaviorSubject, combineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; -import { debounceTime, distinctUntilChanged, filter, map, mergeMap, reduce, startWith, flatMap, find } from 'rxjs/operators'; +import { + debounceTime, + distinctUntilChanged, + filter, + find, + flatMap, + map, + mergeMap, + reduce, + startWith +} from 'rxjs/operators'; import { Collection } from '../../../core/shared/collection.model'; import { CommunityDataService } from '../../../core/data/community-data.service'; @@ -227,8 +247,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { } else { return listCollection.filter((v) => v.collection.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1).slice(0, 5); } - }) - ); + })); } } } diff --git a/tslint.json b/tslint.json index 92bb66cee1..51edf5e1d7 100644 --- a/tslint.json +++ b/tslint.json @@ -113,6 +113,13 @@ "parameter": "nospace", "property-declaration": "nospace", "variable-declaration": "nospace" + }, + { + "call-signature": "onespace", + "index-signature": "onespace", + "parameter": "onespace", + "property-declaration": "onespace", + "variable-declaration": "onespace" } ], "unified-signatures": true,