Merge remote-tracking branch 'remotes/origin/master' into fix_store_selectors

This commit is contained in:
Giuseppe Digilio
2020-01-16 16:14:19 +01:00
88 changed files with 2435 additions and 422 deletions

View File

@@ -1,5 +1,5 @@
sudo: required sudo: required
dist: trusty dist: bionic
env: env:
# Install the latest docker-compose version for ci testing. # Install the latest docker-compose version for ci testing.
@@ -12,6 +12,9 @@ env:
DSPACE_REST_NAMESPACE: '/server/api' DSPACE_REST_NAMESPACE: '/server/api'
DSPACE_REST_SSL: false DSPACE_REST_SSL: false
services:
- xvfb
before_install: before_install:
# Docker Compose Install # Docker Compose Install
- curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose - 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: after_script:
- docker-compose -f ./docker/docker-compose-travis.yml down - docker-compose -f ./docker/docker-compose-travis.yml down
addons:
apt:
sources:
- google-chrome
packages:
- dpkg
- google-chrome-stable
language: node_js language: node_js
node_js: node_js:
@@ -53,8 +48,6 @@ cache:
bundler_args: --retry 5 bundler_args: --retry 5
script: script:
# Use Chromium instead of Chrome.
- export CHROME_BIN=chromium-browser
- yarn run build - yarn run build
- yarn run ci - yarn run ci
- cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js - cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js

View File

@@ -1566,6 +1566,8 @@
"search.results.no-results-link": "quotes around it", "search.results.no-results-link": "quotes around it",
"search.results.empty": "Your search returned no results.",
"search.sidebar.close": "Back to 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.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", "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.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.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", "submission.sections.describe.relationship-lookup.name-variant.notification.confirm": "Save a new name variant",

View File

@@ -1,14 +1,13 @@
<ng-container *ngVar="(subCollectionsRDObs | async) as subCollectionsRD"> <ng-container *ngVar="(subCollectionsRDObs | async) as subCollectionsRD">
<div *ngIf="subCollectionsRD?.hasSucceeded && subCollectionsRD?.payload.totalElements > 0" @fadeIn> <div *ngIf="subCollectionsRD?.hasSucceeded && subCollectionsRD?.payload.totalElements > 0" @fadeIn>
<h2>{{'community.sub-collection-list.head' | translate}}</h2> <h2>{{'community.sub-collection-list.head' | translate}}</h2>
<ul> <ds-viewable-collection
<li *ngFor="let collection of subCollectionsRD?.payload.page"> [config]="config"
<p> [sortConfig]="sortConfig"
<span class="lead"><a [routerLink]="['/collections', collection.id]">{{collection.name}}</a></span><br> [objects]="subCollectionsRD"
<span class="text-muted">{{collection.shortDescription}}</span> [hideGear]="false"
</p> (paginationChange)="onPaginationChange($event)">
</li> </ds-viewable-collection>
</ul>
</div> </div>
<ds-error *ngIf="subCollectionsRD?.hasFailed" message="{{'error.sub-collections' | translate}}"></ds-error> <ds-error *ngIf="subCollectionsRD?.hasFailed" message="{{'error.sub-collections' | translate}}"></ds-error>
<ds-loading *ngIf="subCollectionsRD?.isLoading" message="{{'loading.sub-collections' | translate}}"></ds-loading> <ds-loading *ngIf="subCollectionsRD?.isLoading" message="{{'loading.sub-collections' | translate}}"></ds-loading>

View File

@@ -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<CommunityPageSubCollectionListComponent>;
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');
});
});

View File

@@ -1,12 +1,16 @@
import { Component, Input, OnInit } from '@angular/core'; 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 { RemoteData } from '../../core/data/remote-data';
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
import { fadeIn } from '../../shared/animations/fade'; import { fadeIn } from '../../shared/animations/fade';
import { PaginatedList } from '../../core/data/paginated-list'; 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({ @Component({
selector: 'ds-community-page-sub-collection-list', selector: 'ds-community-page-sub-collection-list',
@@ -16,9 +20,60 @@ import { PaginatedList } from '../../core/data/paginated-list';
}) })
export class CommunityPageSubCollectionListComponent implements OnInit { export class CommunityPageSubCollectionListComponent implements OnInit {
@Input() community: Community; @Input() community: Community;
subCollectionsRDObs: Observable<RemoteData<PaginatedList<Collection>>>;
/**
* 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<RemoteData<PaginatedList<Collection>>> = new BehaviorSubject<RemoteData<PaginatedList<Collection>>>({} as any);
constructor(private cds: CollectionDataService) {}
ngOnInit(): void { 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);
});
} }
} }

View File

@@ -1,14 +1,13 @@
<ng-container *ngVar="(subCommunitiesRDObs | async) as subCommunitiesRD"> <ng-container *ngVar="(subCommunitiesRDObs | async) as subCommunitiesRD">
<div *ngIf="subCommunitiesRD?.hasSucceeded && subCommunitiesRD?.payload.totalElements > 0" @fadeIn> <div *ngIf="subCommunitiesRD?.hasSucceeded && subCommunitiesRD?.payload.totalElements > 0" @fadeIn>
<h2>{{'community.sub-community-list.head' | translate}}</h2> <h2>{{'community.sub-community-list.head' | translate}}</h2>
<ul> <ds-viewable-collection
<li *ngFor="let community of subCommunitiesRD?.payload.page"> [config]="config"
<p> [sortConfig]="sortConfig"
<span class="lead"><a [routerLink]="['/communities', community.id]">{{community.name}}</a></span><br> [objects]="subCommunitiesRD"
<span class="text-muted">{{community.shortDescription}}</span> [hideGear]="false"
</p> (paginationChange)="onPaginationChange($event)">
</li> </ds-viewable-collection>
</ul>
</div> </div>
<ds-error *ngIf="subCommunitiesRD?.hasFailed" message="{{'error.sub-communities' | translate}}"></ds-error> <ds-error *ngIf="subCommunitiesRD?.hasFailed" message="{{'error.sub-communities' | translate}}"></ds-error>
<ds-loading *ngIf="subCommunitiesRD?.isLoading" message="{{'loading.sub-communities' | translate}}"></ds-loading> <ds-loading *ngIf="subCommunitiesRD?.isLoading" message="{{'loading.sub-communities' | translate}}"></ds-loading>

View File

@@ -1,21 +1,29 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { NO_ERRORS_SCHEMA } from '@angular/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 { RouterTestingModule } from '@angular/router/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import {of as observableOf, Observable } from 'rxjs';
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
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 comp: CommunityPageSubCommunityListComponent;
let fixture: ComponentFixture<CommunityPageSubCommunityListComponent>; let fixture: ComponentFixture<CommunityPageSubCommunityListComponent>;
let communityDataServiceStub: any;
let subCommList = [];
const subcommunities = [Object.assign(new Community(), { const subcommunities = [Object.assign(new Community(), {
id: '123456789-1', id: '123456789-1',
@@ -32,34 +40,92 @@ describe('SubCommunityList Component', () => {
{ language: 'en_US', value: 'SubCommunity 2' } { 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: { metadata: {
'dc.title': [ 'dc.title': [
{ language: 'en_US', value: 'Test title' } { language: 'en_US', value: 'Test title' }
] ]
}, }
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), []))
}); });
const mockCommunity = Object.assign(new Community(), { communityDataServiceStub = {
metadata: { findByParent(parentUUID: string, options: FindListOptions = {}) {
'dc.title': [ let currentPage = options.currentPage;
{ language: 'en_US', value: 'Test title' } let elementsPerPage = options.elementsPerPage;
] if (currentPage === undefined) {
}, currentPage = 1
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), subcommunities)) }
}) 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(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), SharedModule, imports: [
TranslateModule.forRoot(),
SharedModule,
RouterTestingModule.withRoutes([]), RouterTestingModule.withRoutes([]),
NoopAnimationsModule], NgbModule.forRoot(),
NoopAnimationsModule
],
declarations: [CommunityPageSubCommunityListComponent], declarations: [CommunityPageSubCommunityListComponent],
providers: [
{ provide: CommunityDataService, useValue: communityDataServiceStub },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: SelectableListService, useValue: {} },
],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
})); }));
@@ -67,23 +133,52 @@ describe('SubCommunityList Component', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(CommunityPageSubCommunityListComponent); fixture = TestBed.createComponent(CommunityPageSubCommunityListComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
comp.community = mockCommunity;
}); });
it('should display a list of subCommunities', () => { it('should display a list of sub-communities', () => {
comp.community = mockCommunity; subCommList = subcommunities;
fixture.detectChanges(); fixture.detectChanges();
const subComList = fixture.debugElement.queryAll(By.css('li')); 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[0].nativeElement.textContent).toContain('SubCommunity 1');
expect(subComList[1].nativeElement.textContent).toContain('SubCommunity 2'); 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', () => { it('should not display the header when list of sub-communities is empty', () => {
comp.community = emptySubCommunitiesCommunity; subCommList = [];
fixture.detectChanges(); fixture.detectChanges();
const subComHead = fixture.debugElement.queryAll(By.css('h2')); const subComHead = fixture.debugElement.queryAll(By.css('h2'));
expect(subComHead.length).toEqual(0); 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');
});
}); });

View File

@@ -1,11 +1,15 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { take } from 'rxjs/operators';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
import { fadeIn } from '../../shared/animations/fade'; import { fadeIn } from '../../shared/animations/fade';
import { PaginatedList } from '../../core/data/paginated-list'; 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({ @Component({
selector: 'ds-community-page-sub-community-list', selector: 'ds-community-page-sub-community-list',
@@ -18,9 +22,61 @@ import {Observable} from 'rxjs';
*/ */
export class CommunityPageSubCommunityListComponent implements OnInit { export class CommunityPageSubCommunityListComponent implements OnInit {
@Input() community: Community; @Input() community: Community;
subCommunitiesRDObs: Observable<RemoteData<PaginatedList<Community>>>;
/**
* 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<RemoteData<PaginatedList<Community>>> = new BehaviorSubject<RemoteData<PaginatedList<Community>>>({} as any);
constructor(private cds: CommunityDataService) {
}
ngOnInit(): void { 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);
});
} }
} }

View File

@@ -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<TopLevelCommunityListComponent>;
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');
});
});

View File

@@ -1,15 +1,15 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 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 { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { CommunityDataService } from '../../core/data/community-data.service'; import { CommunityDataService } from '../../core/data/community-data.service';
import { PaginatedList } from '../../core/data/paginated-list'; import { PaginatedList } from '../../core/data/paginated-list';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
import { fadeInOut } from '../../shared/animations/fade'; import { fadeInOut } from '../../shared/animations/fade';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { take } from 'rxjs/operators';
/** /**
* this component renders the Top-Level Community list * this component renders the Top-Level Community list
@@ -33,6 +33,11 @@ export class TopLevelCommunityListComponent implements OnInit {
*/ */
config: PaginationComponentOptions; config: PaginationComponentOptions;
/**
* The pagination id
*/
pageId = 'top-level-pagination';
/** /**
* The sorting configuration * The sorting configuration
*/ */
@@ -40,7 +45,7 @@ export class TopLevelCommunityListComponent implements OnInit {
constructor(private cds: CommunityDataService) { constructor(private cds: CommunityDataService) {
this.config = new PaginationComponentOptions(); this.config = new PaginationComponentOptions();
this.config.id = 'top-level-pagination'; this.config.id = this.pageId;
this.config.pageSize = 5; this.config.pageSize = 5;
this.config.currentPage = 1; this.config.currentPage = 1;
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
@@ -55,10 +60,10 @@ export class TopLevelCommunityListComponent implements OnInit {
* @param event The new pagination data * @param event The new pagination data
*/ */
onPaginationChange(event) { onPaginationChange(event) {
this.config.currentPage = event.page; this.config.currentPage = event.pagination.currentPage;
this.config.pageSize = event.pageSize; this.config.pageSize = event.pagination.pageSize;
this.sortConfig.field = event.sortField; this.sortConfig.field = event.sort.field;
this.sortConfig.direction = event.sortDirection; this.sortConfig.direction = event.sort.direction;
this.updatePage(); this.updatePage();
} }

View File

@@ -70,5 +70,4 @@ export class EditRelationshipComponent implements OnChanges {
canUndo(): boolean { canUndo(): boolean {
return this.fieldUpdate.changeType >= 0; return this.fieldUpdate.changeType >= 0;
} }
} }

View File

@@ -5,10 +5,10 @@ import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angu
import { pushInOut } from '../shared/animations/push'; import { pushInOut } from '../shared/animations/push';
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component'; import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
import { Router } from '@angular/router';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
import { RouteService } from '../core/services/route.service'; import { RouteService } from '../core/services/route.service';
import { SearchService } from '../core/shared/search/search.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. * 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)) { if (hasValue(this.configuration)) {
this.routeService.setParameter('configuration', this.configuration); this.routeService.setParameter('configuration', this.configuration);
} }
if (hasValue(this.fixedFilterQuery)) {
this.routeService.setParameter('fixedFilter', this.fixedFilterQuery);
}
} }
} }

View File

@@ -3,12 +3,17 @@ import { CommonModule } from '@angular/common';
import { CoreModule } from '../core/core.module'; import { CoreModule } from '../core/core.module';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { SearchPageRoutingModule } from './search-page-routing.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 { ConfigurationSearchPageComponent } from './configuration-search-page.component';
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard'; import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
import { SearchTrackerComponent } from './search-tracker.component'; import { SearchTrackerComponent } from './search-tracker.component';
import { StatisticsModule } from '../statistics/statistics.module'; 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 = [ const components = [
SearchPageComponent, SearchPageComponent,
@@ -25,8 +30,14 @@ const components = [
CoreModule.forRoot(), CoreModule.forRoot(),
StatisticsModule.forRoot(), StatisticsModule.forRoot(),
], ],
providers: [ConfigurationSearchPageGuard],
declarations: components, declarations: components,
providers: [
SidebarService,
SidebarFilterService,
SearchFilterService,
ConfigurationSearchPageGuard,
SearchConfigurationService
],
exports: components exports: components
}) })

View File

@@ -9,10 +9,10 @@ import { RouteService } from '../core/services/route.service';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
import { SearchSuccessResponse } from '../core/cache/response.models'; import { SearchSuccessResponse } from '../core/cache/response.models';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
import { Router } from '@angular/router';
import { SearchService } from '../core/shared/search/search.service'; import { SearchService } from '../core/shared/search/search.service';
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model'; import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
import { SearchQueryResponse } from '../shared/search/search-query-response.model'; import { SearchQueryResponse } from '../shared/search/search-query-response.model';
import { Router } from '@angular/router';
/** /**
* This component triggers a page view statistic * This component triggers a page view statistic

View File

@@ -11,9 +11,9 @@ import { hasValue, isNotEmpty } from '../shared/empty.util';
import { getSucceededRemoteData } from '../core/shared/operators'; import { getSucceededRemoteData } from '../core/shared/operators';
import { RouteService } from '../core/services/route.service'; import { RouteService } from '../core/services/route.service';
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component'; 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 { 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 { SearchService } from '../core/shared/search/search.service';
import { currentPath } from '../shared/utils/route.utils'; import { currentPath } from '../shared/utils/route.utils';
import { Router } from '@angular/router'; import { Router } from '@angular/router';

View File

@@ -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<ExternalSourceEntry> {
/**
* 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;
}

View File

@@ -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<ExternalSource> {
/**
* Unique identifier
*/
@autoserialize
id: string;
/**
* The name of this external source
*/
@autoserialize
name: string;
/**
* Is the source hierarchical?
*/
@autoserialize
hierarchical: boolean;
}

View File

@@ -136,6 +136,10 @@ import { SearchConfigurationService } from './shared/search/search-configuration
import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service'; import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service';
import { RelationshipTypeService } from './data/relationship-type.service'; import { RelationshipTypeService } from './data/relationship-type.service';
import { SidebarService } from '../shared/sidebar/sidebar.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 * When not in production, endpoint responses can be mocked for testing purposes
@@ -247,6 +251,8 @@ const PROVIDERS = [
SearchConfigurationService, SearchConfigurationService,
SelectableListService, SelectableListService,
RelationshipTypeService, RelationshipTypeService,
ExternalSourceService,
LookupRelationService,
// register AuthInterceptor as HttpInterceptor // register AuthInterceptor as HttpInterceptor
{ {
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,
@@ -292,7 +298,9 @@ export const normalizedModels =
NormalizedPoolTask, NormalizedPoolTask,
NormalizedRelationship, NormalizedRelationship,
NormalizedRelationshipType, NormalizedRelationshipType,
NormalizedItemType NormalizedItemType,
NormalizedExternalSource,
NormalizedExternalSourceEntry
]; ];
@NgModule({ @NgModule({

View File

@@ -17,6 +17,7 @@ import { NormalizedObjectBuildService } from '../cache/builders/normalized-objec
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { Item } from '../shared/item.model'; import { Item } from '../shared/item.model';
import * as uuidv4 from 'uuid/v4'; import * as uuidv4 from 'uuid/v4';
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
const endpoint = 'https://rest.api/core'; const endpoint = 'https://rest.api/core';
@@ -191,8 +192,7 @@ describe('DataService', () => {
dso2.self = selfLink; dso2.self = selfLink;
dso2.metadata = [{ key: 'dc.title', value: name2 }]; dso2.metadata = [{ key: 'dc.title', value: name2 }];
spyOn(service, 'findByHref').and.returnValues(observableOf(dso)); spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(dso));
spyOn(objectCache, 'getObjectBySelfLink').and.returnValues(observableOf(dso));
spyOn(objectCache, 'addPatch'); spyOn(objectCache, 'addPatch');
}); });

View File

@@ -37,7 +37,7 @@ import { Operation } from 'fast-json-patch';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { DSpaceObject } from '../shared/dspace-object.model'; import { DSpaceObject } from '../shared/dspace-object.model';
import { NotificationsService } from '../../shared/notifications/notifications.service'; 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 { ErrorResponse, RestResponse } from '../cache/response.models';
import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; import { NotificationOptions } from '../../shared/notifications/models/notification-options.model';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
@@ -248,8 +248,11 @@ export abstract class DataService<T extends CacheableObject> {
* @param {DSpaceObject} object The given object * @param {DSpaceObject} object The given object
*/ */
update(object: T): Observable<RemoteData<T>> { update(object: T): Observable<RemoteData<T>> {
const oldVersion$ = this.objectCache.getObjectBySelfLink(object.self); const oldVersion$ = this.findByHref(object.self);
return oldVersion$.pipe(take(1), mergeMap((oldVersion: NormalizedObject<T>) => { return oldVersion$.pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
mergeMap((oldVersion: T) => {
const operations = this.comparator.diff(oldVersion, object); const operations = this.comparator.diff(oldVersion, object);
if (isNotEmpty(operations)) { if (isNotEmpty(operations)) {
this.objectCache.addPatch(object.self, operations); this.objectCache.addPatch(object.self, operations);
@@ -257,7 +260,6 @@ export abstract class DataService<T extends CacheableObject> {
return this.findByHref(object.self); return this.findByHref(object.self);
} }
)); ));
} }
/** /**

View File

@@ -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);
});
});
});
});

View File

@@ -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<ExternalSource> {
protected linkPath = 'externalsources';
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected dataBuildService: NormalizedObjectBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<ExternalSource>) {
super();
}
/**
* Get the endpoint to browse external sources
* @param options
* @param linkPath
*/
getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
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<string> {
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<RemoteData<PaginatedList<ExternalSourceEntry>>> {
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$);
}
}

View File

@@ -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)
});
});
});
});

View File

@@ -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<RemoteData<PaginatedList<SearchResult<Item>>>> {
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<PaginatedList<SearchResult<DSpaceObject>>>) => rd.isLoading),
concat(subject.pipe(take(1)))
)
) as any
) as Observable<RemoteData<PaginatedList<SearchResult<Item>>>>;
}
/**
* 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<number> {
return this.getLocalResults(relationship, Object.assign(new PaginatedSearchOptions({}), searchOptions, { pagination: this.singleResultOptions }), false).pipe(
getAllSucceededRemoteData(),
getRemoteDataPayload(),
map((results: PaginatedList<SearchResult<Item>>) => 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<number> {
return this.externalSourceService.getExternalSourceEntries(externalSource.id, Object.assign(new PaginatedSearchOptions({}), searchOptions, { pagination: this.singleResultOptions })).pipe(
getAllSucceededRemoteData(),
getRemoteDataPayload(),
map((results: PaginatedList<ExternalSourceEntry>) => results.totalElements),
startWith(0)
);
}
}

View File

@@ -123,8 +123,8 @@ describe('RelationshipService', () => {
it('should clear the related items their cache', () => { it('should clear the related items their cache', () => {
expect(objectCache.remove).toHaveBeenCalledWith(relatedItem1.self); expect(objectCache.remove).toHaveBeenCalledWith(relatedItem1.self);
expect(objectCache.remove).toHaveBeenCalledWith(item.self); expect(objectCache.remove).toHaveBeenCalledWith(item.self);
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.self); expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.uuid);
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.self); expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.uuid);
}); });
}); });

View File

@@ -1,46 +1,35 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { RequestService } from './request.service'; import { MemoizedSelector, select, Store } from '@ngrx/store';
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 { combineLatest, combineLatest as observableCombineLatest } from 'rxjs'; 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 { PaginatedList } from './paginated-list';
import { ItemDataService } from './item-data.service'; import { ItemDataService } from './item-data.service';
import { import { Relationship } from '../shared/item-relationships/relationship.model';
compareArraysUsingIds, import { Item } from '../shared/item.model';
paginatedRelationsToItems,
relationsToItems
} from '../../+item-page/simple/item-types/shared/item-relationships-utils';
import { ObjectCacheService } from '../cache/object-cache.service';
import { DataService } from './data.service'; 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 { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { SearchParam } from '../cache/models/search-param.model'; import { RequestService } from './request.service';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { Observable } from 'rxjs/internal/Observable';
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';
const relationshipListsStateSelector = (state: AppState) => state.relationshipLists; const relationshipListsStateSelector = (state: AppState) => state.relationshipLists;
@@ -140,9 +129,9 @@ export class RelationshipService extends DataService<Relationship> {
this.findById(relationshipId).pipe( this.findById(relationshipId).pipe(
getSucceededRemoteData(), getSucceededRemoteData(),
getRemoteDataPayload(), getRemoteDataPayload(),
switchMap((relationship: Relationship) => combineLatest( switchMap((rel: Relationship) => combineLatest(
relationship.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()), rel.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()),
relationship.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()) rel.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload())
) )
), ),
take(1) take(1)
@@ -158,10 +147,10 @@ export class RelationshipService extends DataService<Relationship> {
*/ */
private removeRelationshipItemsFromCache(item) { private removeRelationshipItemsFromCache(item) {
this.objectCache.remove(item.self); this.objectCache.remove(item.self);
this.requestService.removeByHrefSubstring(item.self); this.requestService.removeByHrefSubstring(item.uuid);
combineLatest( combineLatest(
this.objectCache.hasBySelfLinkObservable(item.self), this.objectCache.hasBySelfLinkObservable(item.self),
this.requestService.hasByHrefObservable(item.self) this.requestService.hasByHrefObservable(item.uuid)
).pipe( ).pipe(
filter(([existsInOC, existsInRC]) => !existsInOC && !existsInRC), filter(([existsInOC, existsInRC]) => !existsInOC && !existsInRC),
take(1), take(1),
@@ -367,7 +356,7 @@ export class RelationshipService extends DataService<Relationship> {
* @param nameVariant The name variant to set for the matching relationship * @param nameVariant The name variant to set for the matching relationship
*/ */
public updateNameVariant(item1: Item, item2: Item, relationshipLabel: string, nameVariant: string): Observable<RemoteData<Relationship>> { public updateNameVariant(item1: Item, item2: Item, relationshipLabel: string, nameVariant: string): Observable<RemoteData<Relationship>> {
return this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel) const update$: Observable<RemoteData<Relationship>> = this.getRelationshipByItemsAndLabel(item1, item2, relationshipLabel)
.pipe( .pipe(
switchMap((relation: Relationship) => switchMap((relation: Relationship) =>
relation.relationshipType.pipe( relation.relationshipType.pipe(
@@ -388,14 +377,44 @@ export class RelationshipService extends DataService<Relationship> {
} }
return this.update(updatedRelationship); return this.update(updatedRelationship);
}), }),
// skipWhile((relationshipRD: RemoteData<Relationship>) => !relationshipRD.isSuccessful) );
tap((relationshipRD: RemoteData<Relationship>) => {
if (relationshipRD.hasSucceeded) { update$.pipe(
filter((relationshipRD: RemoteData<Relationship>) => relationshipRD.state === RemoteDataState.RequestPending),
take(1),
).subscribe(() => {
this.removeRelationshipItemsFromCache(item1); this.removeRelationshipItemsFromCache(item1);
this.removeRelationshipItemsFromCache(item2); 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<RemoteData<Relationship>> {
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<Relationship>) => relationshipRD.state === RemoteDataState.ResponsePending),
take(1),
).subscribe((relationshipRD: RemoteData<Relationship>) => {
if (relationshipRD.state === RemoteDataState.ResponsePending) {
this.removeRelationshipItemsFromCacheByRelationship(reoRel.relationship.id);
}
});
return update$;
} }
} }

View File

@@ -22,11 +22,9 @@ import { getSucceededRemoteData } from '../shared/operators';
* Service responsible for handling requests related to the Site object * Service responsible for handling requests related to the Site object
*/ */
@Injectable() @Injectable()
export class SiteDataService extends DataService<Site> { export class SiteDataService extends DataService<Site> {
protected linkPath = 'sites'; protected linkPath = 'sites';
protected forceBypassCache = false; protected forceBypassCache = false;
constructor( constructor(
protected requestService: RequestService, protected requestService: RequestService,
@@ -42,8 +40,6 @@ export class SiteDataService extends DataService<Site> {
super(); super();
} }
/** /**
* Get the endpoint for browsing the site object * Get the endpoint for browsing the site object
* @param {FindListOptions} options * @param {FindListOptions} options
@@ -53,8 +49,6 @@ export class SiteDataService extends DataService<Site> {
return this.halService.getEndpoint(this.linkPath); return this.halService.getEndpoint(this.linkPath);
} }
/** /**
* Retrieve the Site Object * Retrieve the Site Object
*/ */

View File

@@ -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) { public addParameter(key, value) {
this.store.dispatch(new AddParameterAction(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) { public setParameter(key, value) {
this.store.dispatch(new SetParameterAction(key, value)); this.store.dispatch(new SetParameterAction(key, value));
} }

View File

@@ -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<string | GenericConstructor<ListableObject>> {
return [this.constructor as GenericConstructor<ListableObject>];
}
}

View File

@@ -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;
}

View File

@@ -117,8 +117,7 @@ export class SearchService implements OnDestroy {
* @param responseMsToLive The amount of milliseconds for the response to live in cache * @param responseMsToLive The amount of milliseconds for the response to live in cache
* @returns {Observable<RequestEntry>} Emits an observable with the request entries * @returns {Observable<RequestEntry>} Emits an observable with the request entries
*/ */
searchEntries(searchOptions?: PaginatedSearchOptions, responseMsToLive?:number) searchEntries(searchOptions?: PaginatedSearchOptions, responseMsToLive?: number): Observable<{searchOptions: PaginatedSearchOptions, requestEntry: RequestEntry}> {
:Observable<{searchOptions: PaginatedSearchOptions, requestEntry: RequestEntry}> {
const hrefObs = this.getEndpoint(searchOptions); const hrefObs = this.getEndpoint(searchOptions);
@@ -152,8 +151,7 @@ export class SearchService implements OnDestroy {
* @param searchEntries: The request entries from the search method * @param searchEntries: The request entries from the search method
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found * @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
*/ */
getPaginatedResults(searchEntries:Observable<{ searchOptions:PaginatedSearchOptions, requestEntry:RequestEntry }>) getPaginatedResults(searchEntries: Observable<{ searchOptions: PaginatedSearchOptions, requestEntry: RequestEntry }>): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
:Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
const requestEntryObs: Observable<RequestEntry> = searchEntries.pipe( const requestEntryObs: Observable<RequestEntry> = searchEntries.pipe(
map((entry) => entry.requestEntry), map((entry) => entry.requestEntry),
); );

View File

@@ -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 { metadataRepresentationComponent } from '../../../../shared/metadata-representation/metadata-representation.decorator';
import { MetadataRepresentationType } from '../../../../core/shared/metadata-representation/metadata-representation.model'; 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'; import { ItemMetadataRepresentationListElementComponent } from '../../../../shared/object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component';

View File

@@ -25,6 +25,7 @@ import { PersonInputSuggestionsComponent } from './submission/item-list-elements
import { NameVariantModalComponent } from './submission/name-variant-modal/name-variant-modal.component'; 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 { 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 { 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 = [ const ENTRY_COMPONENTS = [
OrgUnitComponent, OrgUnitComponent,
@@ -48,7 +49,8 @@ const ENTRY_COMPONENTS = [
PersonInputSuggestionsComponent, PersonInputSuggestionsComponent,
NameVariantModalComponent, NameVariantModalComponent,
OrgUnitSearchResultListSubmissionElementComponent, OrgUnitSearchResultListSubmissionElementComponent,
OrgUnitInputSuggestionsComponent OrgUnitInputSuggestionsComponent,
ExternalSourceEntryListSubmissionElementComponent
]; ];
@NgModule({ @NgModule({

View File

@@ -0,0 +1,2 @@
<div>{{object.display}}</div>
<div *ngIf="uri"><a target="_blank" [href]="uri.value">{{uri.value}}</a></div>

View File

@@ -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<ExternalSourceEntryListSubmissionElementComponent>;
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);
});
});

View File

@@ -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<ExternalSourceEntry> implements OnInit {
/**
* The metadata value for the object's uri
*/
uri: MetadataValue;
ngOnInit(): void {
this.uri = Metadata.first(this.object.metadata, 'dc.identifier.uri');
}
}

View File

@@ -10,7 +10,13 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
templateUrl: './name-variant-modal.component.html', templateUrl: './name-variant-modal.component.html',
styleUrls: ['./name-variant-modal.component.scss'] styleUrls: ['./name-variant-modal.component.scss']
}) })
/**
* The component for the modal to add a name variant to an item
*/
export class NameVariantModalComponent { export class NameVariantModalComponent {
/**
* The name variant
*/
@Input() value: string; @Input() value: string;
constructor(public modal: NgbActiveModal) { constructor(public modal: NgbActiveModal) {

View File

@@ -53,16 +53,15 @@
<div *ngIf="hasRelationLookup" class="mt-3"> <div *ngIf="hasRelationLookup" class="mt-3">
<ul class="list-unstyled"> <ul class="list-unstyled" cdkDropList (cdkDropListDropped)="moveSelection($event)">
<li *ngFor="let value of ( selectedValues$ | async)"> <ds-existing-metadata-list-element cdkDrag
<button type="button" class="close float-left" aria-label="Close button" *ngFor="let reorderable of reorderables; trackBy: trackReorderable"
(click)="removeSelection(value.selectedResult)"> [reoRel]="reorderable"
<span aria-hidden="true">&times;</span> [submissionItem]="item"
</button> [listId]="listId"
<span class="d-inline-block align-middle ml-1"> [metadataFields]="model.metadataFields"
<ds-metadata-representation-loader [mdRepresentation]="value.mdRep"></ds-metadata-representation-loader> [relationshipOptions]="model.relationship">
</span> </ds-existing-metadata-list-element>
</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -1,12 +1,13 @@
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy, ChangeDetectorRef,
Component, Component,
ComponentFactoryResolver, ComponentFactoryResolver,
ContentChildren, ContentChildren,
EventEmitter, EventEmitter,
Input, Input,
NgZone, NgZone,
OnChanges, OnDestroy, OnChanges,
OnDestroy,
OnInit, OnInit,
Output, Output,
QueryList, QueryList,
@@ -49,7 +50,10 @@ import {
DynamicNGBootstrapTimePickerComponent DynamicNGBootstrapTimePickerComponent
} from '@ng-dynamic-forms/ui-ng-bootstrap'; } from '@ng-dynamic-forms/ui-ng-bootstrap';
import { TranslateService } from '@ngx-translate/core'; 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_TYPEAHEAD } from './models/typeahead/dynamic-typeahead.model';
import { DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.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 { 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 { 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 { DsDatePickerInlineComponent } from './models/date-picker-inline/dynamic-date-picker-inline.component';
import { map, switchMap, take, tap } from 'rxjs/operators'; import { map, startWith, switchMap, find } from 'rxjs/operators';
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
import { SelectableListState } from '../../../object-list/selectable-list/selectable-list.reducer';
import { SearchResult } from '../../../search/search-result.model'; import { SearchResult } from '../../../search/search-result.model';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; 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 { DsDynamicDisabledComponent } from './models/disabled/dynamic-disabled.component';
import { DYNAMIC_FORM_CONTROL_TYPE_DISABLED } from './models/disabled/dynamic-disabled.model'; import { DYNAMIC_FORM_CONTROL_TYPE_DISABLED } from './models/disabled/dynamic-disabled.model';
import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/dynamic-lookup-relation-modal.component'; import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/dynamic-lookup-relation-modal.component';
import { import { getAllSucceededRemoteData, getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
getAllSucceededRemoteData,
getRemoteDataPayload,
getSucceededRemoteData
} from '../../../../core/shared/operators';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { ItemDataService } from '../../../../core/data/item-data.service'; import { ItemDataService } from '../../../../core/data/item-data.service';
import { RemoveRelationshipAction } from './relation-lookup-modal/relationship.actions';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '../../../../app.reducer'; import { AppState } from '../../../../app.reducer';
import { SubmissionObjectDataService } from '../../../../core/submission/submission-object-data.service'; import { SubmissionObjectDataService } from '../../../../core/submission/submission-object-data.service';
import { SubmissionObject } from '../../../../core/submission/models/submission-object.model'; import { SubmissionObject } from '../../../../core/submission/models/submission-object.model';
import { PaginatedList } from '../../../../core/data/paginated-list'; import { PaginatedList } from '../../../../core/data/paginated-list';
import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model';
import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { MetadataValue } from '../../../../core/shared/metadata.models'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type<DynamicFormControl> | null { export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type<DynamicFormControl> | null {
switch (model.type) { switch (model.type) {
@@ -182,16 +180,14 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
@Input() hasErrorMessaging = false; @Input() hasErrorMessaging = false;
@Input() layout = null as DynamicFormLayout; @Input() layout = null as DynamicFormLayout;
@Input() model: any; @Input() model: any;
relationships$: Observable<Array<SearchResult<Item>>>; reorderables$: Observable<ReorderableRelationship[]>;
reorderables: ReorderableRelationship[];
hasRelationLookup: boolean; hasRelationLookup: boolean;
modalRef: NgbModalRef; modalRef: NgbModalRef;
item: Item; item: Item;
listId: string; listId: string;
searchConfig: string; searchConfig: string;
selectedValues$: Observable<Array<{
selectedResult: SearchResult<Item>,
mdRep: MetadataRepresentation
}>>;
/** /**
* List of subscriptions to unsubscribe from * List of subscriptions to unsubscribe from
*/ */
@@ -224,7 +220,8 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
private relationshipService: RelationshipService, private relationshipService: RelationshipService,
private zone: NgZone, private zone: NgZone,
private store: Store<AppState>, private store: Store<AppState>,
private submissionObjectService: SubmissionObjectDataService private submissionObjectService: SubmissionObjectDataService,
private ref: ChangeDetectorRef
) { ) {
super(componentFactoryResolver, layoutService, validationService, dynamicFormInstanceService); super(componentFactoryResolver, layoutService, validationService, dynamicFormInstanceService);
@@ -235,44 +232,59 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
*/ */
ngOnInit(): void { ngOnInit(): void {
this.hasRelationLookup = hasValue(this.model.relationship); this.hasRelationLookup = hasValue(this.model.relationship);
this.reorderables = [];
if (this.hasRelationLookup) { if (this.hasRelationLookup) {
this.listId = 'list-' + this.model.relationship.relationshipType; this.listId = 'list-' + this.model.relationship.relationshipType;
const item$ = this.submissionObjectService const item$ = this.submissionObjectService
.findById(this.model.submissionId).pipe( .findById(this.model.submissionId).pipe(
getAllSucceededRemoteData(), getAllSucceededRemoteData(),
getRemoteDataPayload(), getRemoteDataPayload(),
switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable<RemoteData<Item>>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()))); switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable<RemoteData<Item>>)
.pipe(
getAllSucceededRemoteData(),
getRemoteDataPayload()
)
)
);
this.subs.push(item$.subscribe((item) => this.item = item)); 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<Relationship>) => 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( this.relationService.getRelatedItemsByLabel(this.item, this.model.relationship.relationshipType).pipe(
map((items: RemoteData<PaginatedList<Item>>) => items.payload.page.map((item) => Object.assign(new ItemSearchResult(), { indexableObject: item }))), map((items: RemoteData<PaginatedList<Item>>) => items.payload.page.map((item) => Object.assign(new ItemSearchResult(), { indexableObject: item }))),
).subscribe((relatedItems: Array<SearchResult<Item>>) => this.selectableListService.select(this.listId, relatedItems)); ).subscribe((relatedItems: Array<SearchResult<Item>>) => 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<Array<SearchResult<Item>>>;
this.selectedValues$ =
observableCombineLatest(item$, this.relationships$).pipe(
map(([item, relatedItems]: [Item, Array<SearchResult<DSpaceObject>>]) => {
return relatedItems
.map((element: SearchResult<Item>) => {
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 * Method to move a relationship inside the list of relationships
* @param object The second item in the relationship, the submitted item being the first * This will update the view and update the right or left place field of the relationships in the list
* @param event
*/ */
removeSelection(object: SearchResult<Item>) { moveSelection(event: CdkDragDrop<Relationship>) {
this.selectableListService.deselectSingle(this.listId, object); this.zone.runOutsideAngular(() => {
this.store.dispatch(new RemoveRelationshipAction(this.item, object.indexableObject, this.model.relationship.relationshipType)) 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<RemoteData<Relationship>>;
}
})
).subscribe();
})
} }
/** /**
@@ -350,4 +379,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo
.filter((sub) => hasValue(sub)) .filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe()); .forEach((sub) => sub.unsubscribe());
} }
/**
* Prevent unnecessary rerendering so fields don't lose focus
*/
trackReorderable(index, reorderable: Reorderable) {
return hasValue(reorderable) ? reorderable.getId() : undefined;
}
} }

View File

@@ -0,0 +1,11 @@
<li *ngIf="metadataRepresentation">
<button type="button" class="close float-left" aria-label="Move button" cdkDragHandle>
<i aria-hidden="true" class="fas fa-arrows-alt fa-xs"></i>
</button>
<button type="button" class="close float-left" aria-label="Close button" (click)="removeSelection()">
<span aria-hidden="true">&times;</span>
</button>
<span class="d-inline-block align-middle ml-1">
<ds-metadata-representation-loader [mdRepresentation]="metadataRepresentation"></ds-metadata-representation-loader>
</span>
</li>

View File

@@ -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<ExistingMetadataListElementComponent>;
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);
});
})
});

View File

@@ -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<AppState>
) {
}
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

View File

@@ -11,6 +11,9 @@ import { DynamicDisabledModel } from './dynamic-disabled.model';
selector: 'ds-dynamic-disabled', selector: 'ds-dynamic-disabled',
templateUrl: './dynamic-disabled.component.html' templateUrl: './dynamic-disabled.component.html'
}) })
/**
* Component for displaying a form input with a disabled property
*/
export class DsDynamicDisabledComponent extends DynamicFormControlComponent { export class DsDynamicDisabledComponent extends DynamicFormControlComponent {
@Input() formId: string; @Input() formId: string;

View File

@@ -7,7 +7,7 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<ngb-tabset> <ngb-tabset>
<ngb-tab [title]="'submission.sections.describe.relationship-lookup.search-tab.tab-title.' + label | translate"> <ngb-tab [title]="'submission.sections.describe.relationship-lookup.search-tab.tab-title.' + label | translate : {count: (totalInternal$ | async)}">
<ng-template ngbTabContent> <ng-template ngbTabContent>
<ds-dynamic-lookup-relation-search-tab <ds-dynamic-lookup-relation-search-tab
[selection$]="selection$" [selection$]="selection$"
@@ -21,6 +21,20 @@
</ds-dynamic-lookup-relation-search-tab> </ds-dynamic-lookup-relation-search-tab>
</ng-template> </ng-template>
</ngb-tab> </ngb-tab>
<ngb-tab *ngFor="let source of (externalSourcesRD$ | async)?.payload?.page; let idx = index"
[title]="'submission.sections.describe.relationship-lookup.search-tab.tab-title.' + source.id | translate : {count: (totalExternal$ | async)[idx]}">
<ng-template ngbTabContent>
<ds-dynamic-lookup-relation-external-source-tab
[listId]="listId"
[repeatable]="repeatable"
[context]="context"
[externalSource]="source"
(selectObject)="select($event)"
(deselectObject)="deselect($event)"
class="d-block pt-3">
</ds-dynamic-lookup-relation-external-source-tab>
</ng-template>
</ngb-tab>
<ngb-tab [title]="'submission.sections.describe.relationship-lookup.selection-tab.tab-title' | translate : {count: (selection$ | async)?.length}"> <ngb-tab [title]="'submission.sections.describe.relationship-lookup.selection-tab.tab-title' | translate : {count: (selection$ | async)?.length}">
<ng-template ngbTabContent> <ng-template ngbTabContent>
<ds-dynamic-lookup-relation-selection-tab <ds-dynamic-lookup-relation-selection-tab

View File

@@ -13,6 +13,12 @@ import { Item } from '../../../../../core/shared/item.model';
import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model';
import { RelationshipOptions } from '../../models/relationship-options.model'; import { RelationshipOptions } from '../../models/relationship-options.model';
import { AddRelationshipAction, RemoveRelationshipAction } from './relationship.actions'; 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', () => { describe('DsDynamicLookupRelationModalComponent', () => {
let component: DsDynamicLookupRelationModalComponent; let component: DsDynamicLookupRelationModalComponent;
@@ -28,6 +34,24 @@ describe('DsDynamicLookupRelationModalComponent', () => {
let relationship; let relationship;
let nameVariant; let nameVariant;
let metadataField; 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() { function init() {
item = Object.assign(new Item(), { uuid: '7680ca97-e2bd-4398-bfa7-139a8673dc42', metadata: {} }); 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; relationship = { filter: 'filter', relationshipType: 'isAuthorOfPublication', nameVariants: true } as RelationshipOptions;
nameVariant = 'Doe, J.'; nameVariant = 'Doe, J.';
metadataField = 'dc.contributor.author'; 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(() => { beforeEach(async(() => {
@@ -49,6 +81,13 @@ describe('DsDynamicLookupRelationModalComponent', () => {
declarations: [DsDynamicLookupRelationModalComponent], declarations: [DsDynamicLookupRelationModalComponent],
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NgbModule.forRoot()], imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NgbModule.forRoot()],
providers: [ providers: [
{
provide: SearchConfigurationService, useValue: {
paginatedSearchOptions: observableOf(pSearchOptions)
}
},
{ provide: ExternalSourceService, useValue: externalSourceService },
{ provide: LookupRelationService, useValue: lookupRelationService },
{ {
provide: SelectableListService, useValue: selectableListService provide: SelectableListService, useValue: selectableListService
}, },

View File

@@ -1,5 +1,5 @@
import { Component, NgZone, OnDestroy, OnInit } from '@angular/core'; 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 { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { hasValue } from '../../../../empty.util'; import { hasValue } from '../../../../empty.util';
import { map, skip, switchMap, take } from 'rxjs/operators'; 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 { RelationshipOptions } from '../../models/relationship-options.model';
import { SearchResult } from '../../../../search/search-result.model'; import { SearchResult } from '../../../../search/search-result.model';
import { Item } from '../../../../../core/shared/item.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 { AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipAction } from './relationship.actions';
import { RelationshipService } from '../../../../../core/data/relationship.service'; import { RelationshipService } from '../../../../../core/data/relationship.service';
import { RelationshipTypeService } from '../../../../../core/data/relationship-type.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 { Context } from '../../../../../core/shared/context.model';
import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model'; import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model';
import { MetadataValue } from '../../../../../core/shared/metadata.models'; 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({ @Component({
selector: 'ds-dynamic-lookup-relation-modal', 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 * 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 { export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy {
/**
* The label to use to display i18n messages (describing the type of relationship)
*/
label: string; label: string;
/**
* Options for searching related items
*/
relationshipOptions: RelationshipOptions; relationshipOptions: RelationshipOptions;
/**
* The ID of the list to add/remove selected items to/from
*/
listId: string; listId: string;
/**
* The item we're adding relationships to
*/
item; item;
/**
* Is the selection repeatable?
*/
repeatable: boolean; repeatable: boolean;
/**
* The list of selected items
*/
selection$: Observable<ListableObject[]>; selection$: Observable<ListableObject[]>;
/**
* The context to display lists
*/
context: Context; context: Context;
/**
* The metadata-fields describing these relationships
*/
metadataFields: string; metadataFields: string;
/**
* A map of subscriptions within this component
*/
subMap: { subMap: {
[uuid: string]: Subscription [uuid: string]: Subscription
} = {}; } = {};
/**
* A list of the available external sources configured for this relationship
*/
externalSourcesRD$: Observable<RemoteData<PaginatedList<ExternalSource>>>;
/**
* The total amount of internal items for the current options
*/
totalInternal$: Observable<number>;
/**
* The total amount of results for each external source using the current options
*/
totalExternal$: Observable<number[]>;
constructor( constructor(
public modal: NgbActiveModal, public modal: NgbActiveModal,
private selectableListService: SelectableListService, private selectableListService: SelectableListService,
private relationshipService: RelationshipService, private relationshipService: RelationshipService,
private relationshipTypeService: RelationshipTypeService, private relationshipTypeService: RelationshipTypeService,
private externalSourceService: ExternalSourceService,
private lookupRelationService: LookupRelationService,
private searchConfigService: SearchConfigurationService,
private zone: NgZone, private zone: NgZone,
private store: Store<AppState> private store: Store<AppState>
) { ) {
@@ -70,13 +132,19 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
this.context = Context.SubmissionModal; this.context = Context.SubmissionModal;
} }
// this.setExistingNameVariants(); this.externalSourcesRD$ = this.externalSourceService.findAll();
this.setTotals();
} }
close() { close() {
this.modal.close(); this.modal.close();
} }
/**
* Select (a list of) objects and add them to the store
* @param selectableObjects
*/
select(...selectableObjects: Array<SearchResult<Item>>) { select(...selectableObjects: Array<SearchResult<Item>>) {
this.zone.runOutsideAngular( 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<Item>) { private addNameVariantSubscription(sri: SearchResult<Item>) {
const nameVariant$ = this.relationshipService.getNameVariant(this.listId, sri.indexableObject.uuid); const nameVariant$ = this.relationshipService.getNameVariant(this.listId, sri.indexableObject.uuid);
this.subMap[sri.indexableObject.uuid] = nameVariant$.pipe( 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))) ).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<SearchResult<Item>>) { deselect(...selectableObjects: Array<SearchResult<Item>>) {
this.zone.runOutsideAngular( this.zone.runOutsideAngular(
() => selectableObjects.forEach((object) => { () => 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() { private setExistingNameVariants() {
const virtualMDs: MetadataValue[] = this.item.allMetadata(this.metadataFields).filter((mdValue) => mdValue.isVirtual); 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() { ngOnDestroy() {
Object.values(this.subMap).forEach((subscription) => subscription.unsubscribe()); Object.values(this.subMap).forEach((subscription) => subscription.unsubscribe());
} }

View File

@@ -0,0 +1,31 @@
<div class="row">
<div class="col-4">
<h3>{{ 'submission.sections.describe.relationship-lookup.selection-tab.settings' | translate}}</h3>
<ds-page-size-selector></ds-page-size-selector>
</div>
<div class="col-8">
<ds-search-form [query]="(searchConfigService.paginatedSearchOptions | async)?.query" [inPlaceSearch]="true"></ds-search-form>
<div>
<h3>{{ 'submission.sections.describe.relationship-lookup.selection-tab.title.' + externalSource.id | translate}}</h3>
<ng-container *ngVar="(entriesRD$ | async) as entriesRD">
<ds-viewable-collection *ngIf="entriesRD?.hasSucceeded && !entriesRD?.isLoading && entriesRD?.payload?.page?.length > 0" @fadeIn
[objects]="entriesRD"
[selectable]="true"
[selectionConfig]="{ repeatable: repeatable, listId: listId }"
[config]="initialPagination"
[hideGear]="true"
[context]="context"
(deselectObject)="deselectObject.emit($event)"
(selectObject)="selectObject.emit($event)">
</ds-viewable-collection>
<ds-loading *ngIf="!entriesRD || !entriesRD?.payload || entriesRD?.isLoading"
message="{{'loading.search-results' | translate}}"></ds-loading>
<ds-error *ngIf="entriesRD?.hasFailed && (!entriesRD?.error || entriesRD?.error?.statusCode != 400)"
message="{{'error.search-results' | translate}}"></ds-error>
<div *ngIf="entriesRD?.payload?.page?.length == 0 || entriesRD?.error?.statusCode == 400" id="empty-external-entry-list">
{{ 'search.results.empty' | translate }}
</div>
</ng-container>
</div>
</div>
</div>

View File

@@ -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<DsDynamicLookupRelationExternalSourceTabComponent>;
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();
});
});
});

View File

@@ -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<ListableObject> = new EventEmitter<ListableObject>();
/**
* Send an event to select an object from the list
*/
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
/**
* 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<RemoteData<PaginatedList<ExternalSourceEntry>>>;
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)))
)
}
}

View File

@@ -3,6 +3,7 @@ import { Actions, Effect, ofType } from '@ngrx/effects';
import { debounceTime, map, mergeMap, take, tap } from 'rxjs/operators'; import { debounceTime, map, mergeMap, take, tap } from 'rxjs/operators';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { RelationshipService } from '../../../../../core/data/relationship.service'; import { RelationshipService } from '../../../../../core/data/relationship.service';
import { getSucceededRemoteData } from '../../../../../core/shared/operators';
import { AddRelationshipAction, RelationshipAction, RelationshipActionTypes, RemoveRelationshipAction, UpdateRelationshipAction } from './relationship.actions'; import { AddRelationshipAction, RelationshipAction, RelationshipActionTypes, RemoveRelationshipAction, UpdateRelationshipAction } from './relationship.actions';
import { Item } from '../../../../../core/shared/item.model'; import { Item } from '../../../../../core/shared/item.model';
import { hasNoValue, hasValue, hasValueOperator } from '../../../../empty.util'; import { hasNoValue, hasValue, hasValueOperator } from '../../../../empty.util';
@@ -88,7 +89,7 @@ export class RelationshipEffects {
this.nameVariantUpdates[identifier] = nameVariant; this.nameVariantUpdates[identifier] = nameVariant;
} else { } else {
this.relationshipService.updateNameVariant(item1, item2, relationshipType, nameVariant) this.relationshipService.updateNameVariant(item1, item2, relationshipType, nameVariant)
.pipe() .pipe(getSucceededRemoteData())
.subscribe(); .subscribe();
} }
} }

View File

@@ -3,7 +3,7 @@
[resultCount]="(resultsRD$ | async)?.payload?.totalElements" [resultCount]="(resultsRD$ | async)?.payload?.totalElements"
[inPlaceSearch]="true" [showViewModes]="false"></ds-search-sidebar> [inPlaceSearch]="true" [showViewModes]="false"></ds-search-sidebar>
<div class="col-8"> <div class="col-8">
<ds-search-form [inPlaceSearch]="true"></ds-search-form> <ds-search-form [query]="(searchConfigService.paginatedSearchOptions | async)?.query" [inPlaceSearch]="true"></ds-search-form>
<ds-search-labels [inPlaceSearch]="true"></ds-search-labels> <ds-search-labels [inPlaceSearch]="true"></ds-search-labels>
@@ -56,8 +56,8 @@
</div> </div>
</div> </div>
<ds-search-results [searchResults]="(resultsRD$ | async)" <ds-search-results [searchResults]="(resultsRD$ | async)"
[sortConfig]="this.searchConfig?.sort" [sortConfig]="this.lookupRelationService.searchConfig?.sort"
[searchConfig]="this.searchConfig" [searchConfig]="this.lookupRelationService.searchConfig"
[selectable]="true" [selectable]="true"
[selectionConfig]="{ repeatable: repeatable, listId: listId }" [selectionConfig]="{ repeatable: repeatable, listId: listId }"
[linkType]="linkTypes.ExternalLink" [linkType]="linkTypes.ExternalLink"

View File

@@ -15,6 +15,8 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../../testing/utils'
import { PaginatedList } from '../../../../../../core/data/paginated-list'; import { PaginatedList } from '../../../../../../core/data/paginated-list';
import { ItemSearchResult } from '../../../../../object-collection/shared/item-search-result.model'; import { ItemSearchResult } from '../../../../../object-collection/shared/item-search-result.model';
import { Item } from '../../../../../../core/shared/item.model'; import { Item } from '../../../../../../core/shared/item.model';
import { ActivatedRoute } from '@angular/router';
import { LookupRelationService } from '../../../../../../core/data/lookup-relation.service';
describe('DsDynamicLookupRelationSearchTabComponent', () => { describe('DsDynamicLookupRelationSearchTabComponent', () => {
let component: DsDynamicLookupRelationSearchTabComponent; let component: DsDynamicLookupRelationSearchTabComponent;
@@ -34,6 +36,7 @@ describe('DsDynamicLookupRelationSearchTabComponent', () => {
let results; let results;
let selectableListService; let selectableListService;
let lookupRelationService;
function init() { function init() {
relationship = { filter: 'filter', relationshipType: 'isAuthorOfPublication', nameVariants: true } as RelationshipOptions; relationship = { filter: 'filter', relationshipType: 'isAuthorOfPublication', nameVariants: true } as RelationshipOptions;
@@ -51,6 +54,10 @@ describe('DsDynamicLookupRelationSearchTabComponent', () => {
results = new PaginatedList(undefined, [searchResult1, searchResult2, searchResult3]); results = new PaginatedList(undefined, [searchResult1, searchResult2, searchResult3]);
selectableListService = jasmine.createSpyObj('selectableListService', ['deselect', 'select', 'deselectAll']); selectableListService = jasmine.createSpyObj('selectableListService', ['deselect', 'select', 'deselectAll']);
lookupRelationService = jasmine.createSpyObj('lookupRelationService', {
getLocalResults: createSuccessfulRemoteDataObject$(results)
});
lookupRelationService.searchConfig = {};
} }
beforeEach(async(() => { beforeEach(async(() => {
@@ -75,6 +82,8 @@ describe('DsDynamicLookupRelationSearchTabComponent', () => {
} }
} }
}, },
{ provide: ActivatedRoute, useValue: { snapshot: { queryParams: {} } } },
{ provide: LookupRelationService, useValue: lookupRelationService }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}) })

View File

@@ -11,14 +11,16 @@ import { RelationshipOptions } from '../../../models/relationship-options.model'
import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model';
import { ListableObject } from '../../../../../object-collection/shared/listable-object.model'; import { ListableObject } from '../../../../../object-collection/shared/listable-object.model';
import { SearchService } from '../../../../../../core/shared/search/search.service'; 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 { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service';
import { hasValue, isNotEmpty } from '../../../../../empty.util'; import { hasValue, isNotEmpty } from '../../../../../empty.util';
import { concat, map, multicast, switchMap, take, takeWhile, tap } from 'rxjs/operators'; 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 { getSucceededRemoteData } from '../../../../../../core/shared/operators';
import { RouteService } from '../../../../../../core/services/route.service'; import { RouteService } from '../../../../../../core/services/route.service';
import { CollectionElementLinkType } from '../../../../../object-collection/collection-element-link.type'; import { CollectionElementLinkType } from '../../../../../object-collection/collection-element-link.type';
import { Context } from '../../../../../../core/shared/context.model'; import { Context } from '../../../../../../core/shared/context.model';
import { LookupRelationService } from '../../../../../../core/data/lookup-relation.service';
@Component({ @Component({
selector: 'ds-dynamic-lookup-relation-search-tab', 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 * 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 { export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDestroy {
/**
* Options for searching related items
*/
@Input() relationship: RelationshipOptions; @Input() relationship: RelationshipOptions;
/**
* The ID of the list to add/remove selected items to/from
*/
@Input() listId: string; @Input() listId: string;
/**
* Is the selection repeatable?
*/
@Input() repeatable: boolean; @Input() repeatable: boolean;
/**
* The list of selected items
*/
@Input() selection$: Observable<ListableObject[]>; @Input() selection$: Observable<ListableObject[]>;
/**
* The context to display lists
*/
@Input() context: Context; @Input() context: Context;
/**
* Send an event to deselect an object from the list
*/
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
/**
* Send an event to select an object from the list
*/
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
/**
* Search results
*/
resultsRD$: Observable<RemoteData<PaginatedList<SearchResult<Item>>>>; resultsRD$: Observable<RemoteData<PaginatedList<SearchResult<Item>>>>;
searchConfig: PaginatedSearchOptions;
/**
* Are all results selected?
*/
allSelected: boolean; allSelected: boolean;
/**
* Are some results selected?
*/
someSelected$: Observable<boolean>; someSelected$: Observable<boolean>;
/**
* Is it currently loading to select all results?
*/
selectAllLoading: boolean; selectAllLoading: boolean;
/**
* Subscription to unsubscribe from
*/
subscription; subscription;
/**
* The initial pagination to use
*/
initialPagination = Object.assign(new PaginationComponentOptions(), { initialPagination = Object.assign(new PaginationComponentOptions(), {
id: 'submission-relation-list', id: 'submission-relation-list',
pageSize: 5 pageSize: 5
}); });
/**
* The type of links to display
*/
linkTypes = CollectionElementLinkType; linkTypes = CollectionElementLinkType;
constructor( constructor(
private searchService: SearchService, private searchService: SearchService,
private router: Router, private router: Router,
private route: ActivatedRoute,
private selectableListService: SelectableListService, private selectableListService: SelectableListService,
private searchConfigService: SearchConfigurationService, public searchConfigService: SearchConfigurationService,
private routeService: RouteService, 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.someSelected$ = this.selection$.pipe(map((selection) => isNotEmpty(selection)));
this.resultsRD$ = this.searchConfigService.paginatedSearchOptions.pipe( this.resultsRD$ = this.searchConfigService.paginatedSearchOptions.pipe(
map((options) => { switchMap((options) => this.lookupRelationService.getLocalResults(this.relationship, 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<PaginatedList<SearchResult<Item>>>) => rd.isLoading),
concat(subject.pipe(take(1)))
)
) as any
)
})
) as Observable<RemoteData<PaginatedList<SearchResult<Item>>>>;
} }
/** /**
@@ -100,7 +141,7 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest
*/ */
resetRoute() { resetRoute() {
this.router.navigate([], { 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, currentPage: 1,
pageSize: 9999 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<RemoteData<PaginatedList<SearchResult<Item>>>>; const results$ = this.searchService.search(fullSearchConfig) as Observable<RemoteData<PaginatedList<SearchResult<Item>>>>;
results$.pipe( results$.pipe(
getSucceededRemoteData(), getSucceededRemoteData(),

View File

@@ -73,11 +73,6 @@ describe('DsDynamicLookupRelationSelectionTabComponent', () => {
expect(component).toBeTruthy(); 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', () => { it('should call navigate on the router when is called resetRoute', () => {
component.selectionRD$ = createSelection([]); component.selectionRD$ = createSelection([]);
fixture.detectChanges(); fixture.detectChanges();

View File

@@ -29,15 +29,49 @@ import { Context } from '../../../../../../core/shared/context.model';
* Tab for inside the lookup model that represents the currently selected relationships * Tab for inside the lookup model that represents the currently selected relationships
*/ */
export class DsDynamicLookupRelationSelectionTabComponent { export class DsDynamicLookupRelationSelectionTabComponent {
/**
* The label to use to display i18n messages (describing the type of relationship)
*/
@Input() label: string; @Input() label: string;
/**
* The ID of the list to add/remove selected items to/from
*/
@Input() listId: string; @Input() listId: string;
/**
* Is the selection repeatable?
*/
@Input() repeatable: boolean; @Input() repeatable: boolean;
/**
* The list of selected items
*/
@Input() selection$: Observable<ListableObject[]>; @Input() selection$: Observable<ListableObject[]>;
/**
* The paginated list of selected items
*/
@Input() selectionRD$: Observable<RemoteData<PaginatedList<ListableObject>>>; @Input() selectionRD$: Observable<RemoteData<PaginatedList<ListableObject>>>;
/**
* The context to display lists
*/
@Input() context: Context; @Input() context: Context;
/**
* Send an event to deselect an object from the list
*/
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
/**
* Send an event to select an object from the list
*/
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
/**
* The initial pagination to use
*/
initialPagination = Object.assign(new PaginationComponentOptions(), { initialPagination = Object.assign(new PaginationComponentOptions(), {
id: 'submission-relation-list', id: 'submission-relation-list',
pageSize: 5 pageSize: 5
@@ -51,7 +85,6 @@ export class DsDynamicLookupRelationSelectionTabComponent {
* Set up the selection and pagination on load * Set up the selection and pagination on load
*/ */
ngOnInit() { ngOnInit() {
this.resetRoute();
this.selectionRD$ = this.searchConfigService.paginatedSearchOptions this.selectionRD$ = this.searchConfigService.paginatedSearchOptions
.pipe( .pipe(
map((options: PaginatedSearchOptions) => options.pagination), 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 }),
});
}
} }

View File

@@ -225,10 +225,14 @@ export class PaginationComponent implements OnDestroy, OnInit {
} }
/** /**
* @param cdRef
* ChangeDetectorRef is a singleton service provided by Angular.
* @param route * @param route
* Route is a singleton service provided by Angular. * Route is a singleton service provided by Angular.
* @param router * @param router
* Router is a singleton service provided by Angular. * Router is a singleton service provided by Angular.
* @param hostWindowService
* the HostWindowService singleton.
*/ */
constructor(private cdRef: ChangeDetectorRef, constructor(private cdRef: ChangeDetectorRef,
private route: ActivatedRoute, private route: ActivatedRoute,
@@ -243,7 +247,7 @@ export class PaginationComponent implements OnDestroy, OnInit {
* The page being navigated to. * The page being navigated to.
*/ */
public doPageChange(page: number) { 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. * The page size being navigated to.
*/ */
public doPageSizeChange(pageSize: number) { 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. * The sort direction being navigated to.
*/ */
public doSortDirectionChange(sortDirection: SortDirection) { 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. * The sort field being navigated to.
*/ */
public doSortFieldChange(field: string) { public doSortFieldChange(field: string) {
this.updateRoute({ page: 1, sortField: field }); this.updateRoute({ pageId: this.id, page: 1, sortField: field });
} }
/** /**
@@ -413,6 +417,8 @@ export class PaginationComponent implements OnDestroy, OnInit {
* Method to update all pagination variables to the current query parameters * Method to update all pagination variables to the current query parameters
*/ */
private setFields() { private setFields() {
// 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 // (+) converts string to a number
const page = this.currentQueryParams.page; const page = this.currentQueryParams.page;
if (this.currentPage !== +page) { if (this.currentPage !== +page) {
@@ -435,6 +441,7 @@ export class PaginationComponent implements OnDestroy, OnInit {
} }
this.cdRef.detectChanges(); this.cdRef.detectChanges();
} }
}
/** /**
* Method to validate the current page value * Method to validate the current page value

View File

@@ -24,6 +24,7 @@ import { getSucceededRemoteData } from '../../../../../core/shared/operators';
import { InputSuggestion } from '../../../../input-suggestions/input-suggestions.model'; import { InputSuggestion } from '../../../../input-suggestions/input-suggestions.model';
import { SearchOptions } from '../../../search-options.model'; import { SearchOptions } from '../../../search-options.model';
import { SEARCH_CONFIG_SERVICE } from '../../../../../+my-dspace-page/my-dspace-page.component'; import { SEARCH_CONFIG_SERVICE } from '../../../../../+my-dspace-page/my-dspace-page.component';
import { currentPath } from '../../../../utils/route.utils';
@Component({ @Component({
selector: 'ds-search-facet-filter', selector: 'ds-search-facet-filter',
@@ -185,7 +186,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
*/ */
public getSearchLink(): string { public getSearchLink(): string {
if (this.inPlaceSearch) { if (this.inPlaceSearch) {
return ''; return currentPath(this.router);
} }
return this.searchService.getSearchLink(); return this.searchService.getSearchLink();
} }

View File

@@ -48,10 +48,7 @@ import { LogOutComponent } from './log-out/log-out.component';
import { FormComponent } from './form/form.component'; import { FormComponent } from './form/form.component';
import { DsDynamicTypeaheadComponent } from './form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.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 { DsDynamicScrollableDropdownComponent } from './form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component';
import { import { DsDynamicFormControlContainerComponent, dsDynamicFormControlMapFn } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component';
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 { 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 { DYNAMIC_FORM_CONTROL_MAP_FN, DynamicFormsCoreModule } from '@ng-dynamic-forms/core';
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap'; 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 { SidebarDropdownComponent } from './sidebar/sidebar-dropdown.component';
import { SidebarFilterComponent } from './sidebar/filter/sidebar-filter.component'; import { SidebarFilterComponent } from './sidebar/filter/sidebar-filter.component';
import { SidebarFilterSelectedOptionComponent } from './sidebar/filter/sidebar-filter-selected-option.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 { 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 = [ const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here // Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -198,6 +197,7 @@ const MODULES = [
MomentModule, MomentModule,
TextMaskModule, TextMaskModule,
MenuModule, MenuModule,
DragDropModule
]; ];
const ROOT_MODULES = [ const ROOT_MODULES = [
@@ -334,7 +334,8 @@ const COMPONENTS = [
ItemSelectComponent, ItemSelectComponent,
CollectionSelectComponent, CollectionSelectComponent,
MetadataRepresentationLoaderComponent, MetadataRepresentationLoaderComponent,
SelectableListItemControlComponent SelectableListItemControlComponent,
ExistingMetadataListElementComponent
]; ];
const ENTRY_COMPONENTS = [ const ENTRY_COMPONENTS = [
@@ -395,7 +396,8 @@ const ENTRY_COMPONENTS = [
SearchFacetRangeOptionComponent, SearchFacetRangeOptionComponent,
SearchAuthorityFilterComponent, SearchAuthorityFilterComponent,
DsDynamicLookupRelationSearchTabComponent, DsDynamicLookupRelationSearchTabComponent,
DsDynamicLookupRelationSelectionTabComponent DsDynamicLookupRelationSelectionTabComponent,
DsDynamicLookupRelationExternalSourceTabComponent
]; ];
const SHARED_ITEM_PAGE_COMPONENTS = [ const SHARED_ITEM_PAGE_COMPONENTS = [
@@ -437,7 +439,8 @@ const DIRECTIVES = [
...DIRECTIVES, ...DIRECTIVES,
...ENTRY_COMPONENTS, ...ENTRY_COMPONENTS,
...SHARED_ITEM_PAGE_COMPONENTS, ...SHARED_ITEM_PAGE_COMPONENTS,
PublicationSearchResultListElementComponent PublicationSearchResultListElementComponent,
ExistingMetadataListElementComponent
], ],
providers: [ providers: [
...PROVIDERS ...PROVIDERS

View File

@@ -70,6 +70,5 @@ describe('PageWithSidebarComponent', () => {
it('should open the menu', () => { it('should open the menu', () => {
expect(menu.classList).toContain('active'); expect(menu.classList).toContain('active');
}); });
}); });
}); });

View File

@@ -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 { FormControl } from '@angular/forms';
import { BehaviorSubject, combineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; 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 { Collection } from '../../../core/shared/collection.model';
import { CommunityDataService } from '../../../core/data/community-data.service'; import { CommunityDataService } from '../../../core/data/community-data.service';
@@ -227,8 +247,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
} else { } else {
return listCollection.filter((v) => v.collection.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1).slice(0, 5); return listCollection.filter((v) => v.collection.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1).slice(0, 5);
} }
}) }));
);
} }
} }
} }

View File

@@ -113,6 +113,13 @@
"parameter": "nospace", "parameter": "nospace",
"property-declaration": "nospace", "property-declaration": "nospace",
"variable-declaration": "nospace" "variable-declaration": "nospace"
},
{
"call-signature": "onespace",
"index-signature": "onespace",
"parameter": "onespace",
"property-declaration": "onespace",
"variable-declaration": "onespace"
} }
], ],
"unified-signatures": true, "unified-signatures": true,