mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-17 23:13:04 +00:00
Merge pull request #539 from atmire/Search-external-sources
Lookup relations - Search external sources
This commit is contained in:
@@ -1566,6 +1566,8 @@
|
||||
|
||||
"search.results.no-results-link": "quotes around it",
|
||||
|
||||
"search.results.empty": "Your search returned no results.",
|
||||
|
||||
|
||||
|
||||
"search.sidebar.close": "Back to results",
|
||||
@@ -1639,13 +1641,21 @@
|
||||
|
||||
"submission.sections.describe.relationship-lookup.selected": "Selected {{ size }} items",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Author": "Search for Authors",
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Author": "Local Authors ({{ count }})",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Search for Journals",
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Local Journals ({{ count }})",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Issue": "Search for Journal Issues",
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Issue": "Local Journal Issues ({{ count }})",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Volume": "Search for Journal Volumes",
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Volume": "Local Journal Volumes ({{ count }})",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaJournal": "Sherpa Journals ({{ count }})",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Publishers ({{ count }})",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.search-tab.tab-title.Funding Agency": "Search for Funding Agencies",
|
||||
|
||||
@@ -1679,6 +1689,14 @@
|
||||
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.Journal Issue": "Selected Issue",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.sherpaJournal": "Search Results",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Search Results",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.name-variant.notification.content": "Would you like to save \"{{ value }}\" as a name variant for this person so you and others can reuse it for future submissions? If you don\'t you can still use it for this submission.",
|
||||
|
||||
"submission.sections.describe.relationship-lookup.name-variant.notification.confirm": "Save a new name variant",
|
||||
|
@@ -5,10 +5,10 @@ import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angu
|
||||
import { pushInOut } from '../shared/animations/push';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
|
||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
import { RouteService } from '../core/services/route.service';
|
||||
import { SearchService } from '../core/shared/search/search.service';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
/**
|
||||
* This component renders a search page using a configuration as input.
|
||||
@@ -61,5 +61,8 @@ export class ConfigurationSearchPageComponent extends SearchComponent implements
|
||||
if (hasValue(this.configuration)) {
|
||||
this.routeService.setParameter('configuration', this.configuration);
|
||||
}
|
||||
if (hasValue(this.fixedFilterQuery)) {
|
||||
this.routeService.setParameter('fixedFilter', this.fixedFilterQuery);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -3,18 +3,28 @@ import { CommonModule } from '@angular/common';
|
||||
import { CoreModule } from '../core/core.module';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { SearchPageRoutingModule } from './search-page-routing.module';
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
import { SearchComponent } from './search.component';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { SidebarEffects } from '../shared/sidebar/sidebar-effects.service';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
|
||||
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
|
||||
import { SearchTrackerComponent } from './search-tracker.component';
|
||||
import { SearchPageComponent } from './search-page.component';
|
||||
import { SidebarFilterService } from '../shared/sidebar/filter/sidebar-filter.service';
|
||||
import { StatisticsModule } from '../statistics/statistics.module';
|
||||
import { SearchComponent } from './search.component';
|
||||
import { SearchTrackerComponent } from './search-tracker.component';
|
||||
import { SearchFilterService } from '../core/shared/search/search-filter.service';
|
||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||
|
||||
const effects = [
|
||||
SidebarEffects
|
||||
];
|
||||
|
||||
const components = [
|
||||
SearchPageComponent,
|
||||
SearchComponent,
|
||||
ConfigurationSearchPageComponent,
|
||||
SearchTrackerComponent
|
||||
SearchTrackerComponent,
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
@@ -22,11 +32,18 @@ const components = [
|
||||
SearchPageRoutingModule,
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
EffectsModule.forFeature(effects),
|
||||
CoreModule.forRoot(),
|
||||
StatisticsModule.forRoot(),
|
||||
],
|
||||
providers: [ConfigurationSearchPageGuard],
|
||||
declarations: components,
|
||||
providers: [
|
||||
SidebarService,
|
||||
SidebarFilterService,
|
||||
SearchFilterService,
|
||||
ConfigurationSearchPageGuard,
|
||||
SearchConfigurationService
|
||||
],
|
||||
exports: components
|
||||
})
|
||||
|
||||
|
36
src/app/core/cache/models/normalized-external-source-entry.model.ts
vendored
Normal file
36
src/app/core/cache/models/normalized-external-source-entry.model.ts
vendored
Normal 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;
|
||||
}
|
29
src/app/core/cache/models/normalized-external-source.model.ts
vendored
Normal file
29
src/app/core/cache/models/normalized-external-source.model.ts
vendored
Normal 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;
|
||||
}
|
@@ -136,6 +136,10 @@ import { SearchConfigurationService } from './shared/search/search-configuration
|
||||
import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service';
|
||||
import { RelationshipTypeService } from './data/relationship-type.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { NormalizedExternalSource } from './cache/models/normalized-external-source.model';
|
||||
import { NormalizedExternalSourceEntry } from './cache/models/normalized-external-source-entry.model';
|
||||
import { ExternalSourceService } from './data/external-source.service';
|
||||
import { LookupRelationService } from './data/lookup-relation.service';
|
||||
|
||||
/**
|
||||
* When not in production, endpoint responses can be mocked for testing purposes
|
||||
@@ -247,6 +251,8 @@ const PROVIDERS = [
|
||||
SearchConfigurationService,
|
||||
SelectableListService,
|
||||
RelationshipTypeService,
|
||||
ExternalSourceService,
|
||||
LookupRelationService,
|
||||
// register AuthInterceptor as HttpInterceptor
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
@@ -292,7 +298,9 @@ export const normalizedModels =
|
||||
NormalizedPoolTask,
|
||||
NormalizedRelationship,
|
||||
NormalizedRelationshipType,
|
||||
NormalizedItemType
|
||||
NormalizedItemType,
|
||||
NormalizedExternalSource,
|
||||
NormalizedExternalSourceEntry
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
76
src/app/core/data/external-source.service.spec.ts
Normal file
76
src/app/core/data/external-source.service.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
85
src/app/core/data/external-source.service.ts
Normal file
85
src/app/core/data/external-source.service.ts
Normal 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$);
|
||||
}
|
||||
}
|
116
src/app/core/data/lookup-relation.service.spec.ts
Normal file
116
src/app/core/data/lookup-relation.service.spec.ts
Normal 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)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
94
src/app/core/data/lookup-relation.service.ts
Normal file
94
src/app/core/data/lookup-relation.service.ts
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
@@ -176,10 +176,20 @@ export class RouteService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a parameter to the current route
|
||||
* @param key The parameter name
|
||||
* @param value The parameter value
|
||||
*/
|
||||
public addParameter(key, value) {
|
||||
this.store.dispatch(new AddParameterAction(key, value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a parameter in the current route (overriding the previous value)
|
||||
* @param key The parameter name
|
||||
* @param value The parameter value
|
||||
*/
|
||||
public setParameter(key, value) {
|
||||
this.store.dispatch(new SetParameterAction(key, value));
|
||||
}
|
||||
|
43
src/app/core/shared/external-source-entry.model.ts
Normal file
43
src/app/core/shared/external-source-entry.model.ts
Normal 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>];
|
||||
}
|
||||
}
|
29
src/app/core/shared/external-source.model.ts
Normal file
29
src/app/core/shared/external-source.model.ts
Normal 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;
|
||||
}
|
@@ -25,6 +25,7 @@ import { PersonInputSuggestionsComponent } from './submission/item-list-elements
|
||||
import { NameVariantModalComponent } from './submission/name-variant-modal/name-variant-modal.component';
|
||||
import { OrgUnitInputSuggestionsComponent } from './submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component';
|
||||
import { OrgUnitSearchResultListSubmissionElementComponent } from './submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component';
|
||||
import { ExternalSourceEntryListSubmissionElementComponent } from './submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component';
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
OrgUnitComponent,
|
||||
@@ -48,7 +49,8 @@ const ENTRY_COMPONENTS = [
|
||||
PersonInputSuggestionsComponent,
|
||||
NameVariantModalComponent,
|
||||
OrgUnitSearchResultListSubmissionElementComponent,
|
||||
OrgUnitInputSuggestionsComponent
|
||||
OrgUnitInputSuggestionsComponent,
|
||||
ExternalSourceEntryListSubmissionElementComponent
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@@ -0,0 +1,2 @@
|
||||
<div>{{object.display}}</div>
|
||||
<div *ngIf="uri"><a target="_blank" [href]="uri.value">{{uri.value}}</a></div>
|
@@ -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);
|
||||
});
|
||||
});
|
@@ -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');
|
||||
}
|
||||
}
|
@@ -10,7 +10,13 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
templateUrl: './name-variant-modal.component.html',
|
||||
styleUrls: ['./name-variant-modal.component.scss']
|
||||
})
|
||||
/**
|
||||
* The component for the modal to add a name variant to an item
|
||||
*/
|
||||
export class NameVariantModalComponent {
|
||||
/**
|
||||
* The name variant
|
||||
*/
|
||||
@Input() value: string;
|
||||
|
||||
constructor(public modal: NgbActiveModal) {
|
||||
|
@@ -11,6 +11,9 @@ import { DynamicDisabledModel } from './dynamic-disabled.model';
|
||||
selector: 'ds-dynamic-disabled',
|
||||
templateUrl: './dynamic-disabled.component.html'
|
||||
})
|
||||
/**
|
||||
* Component for displaying a form input with a disabled property
|
||||
*/
|
||||
export class DsDynamicDisabledComponent extends DynamicFormControlComponent {
|
||||
|
||||
@Input() formId: string;
|
||||
|
@@ -7,7 +7,7 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<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>
|
||||
<ds-dynamic-lookup-relation-search-tab
|
||||
[selection$]="selection$"
|
||||
@@ -21,6 +21,20 @@
|
||||
</ds-dynamic-lookup-relation-search-tab>
|
||||
</ng-template>
|
||||
</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}">
|
||||
<ng-template ngbTabContent>
|
||||
<ds-dynamic-lookup-relation-selection-tab
|
||||
@@ -42,4 +56,4 @@
|
||||
<div>
|
||||
<button type="button" class="btn btn-danger" (click)="close()">{{ ('submission.sections.describe.relationship-lookup.close' | translate) }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -13,6 +13,12 @@ import { Item } from '../../../../../core/shared/item.model';
|
||||
import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model';
|
||||
import { RelationshipOptions } from '../../models/relationship-options.model';
|
||||
import { AddRelationshipAction, RemoveRelationshipAction } from './relationship.actions';
|
||||
import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service';
|
||||
import { PaginatedSearchOptions } from '../../../../search/paginated-search-options.model';
|
||||
import { ExternalSource } from '../../../../../core/shared/external-source.model';
|
||||
import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../../../testing/utils';
|
||||
import { ExternalSourceService } from '../../../../../core/data/external-source.service';
|
||||
import { LookupRelationService } from '../../../../../core/data/lookup-relation.service';
|
||||
|
||||
describe('DsDynamicLookupRelationModalComponent', () => {
|
||||
let component: DsDynamicLookupRelationModalComponent;
|
||||
@@ -28,6 +34,24 @@ describe('DsDynamicLookupRelationModalComponent', () => {
|
||||
let relationship;
|
||||
let nameVariant;
|
||||
let metadataField;
|
||||
let pSearchOptions;
|
||||
let externalSourceService;
|
||||
let lookupRelationService;
|
||||
|
||||
const externalSources = [
|
||||
Object.assign(new ExternalSource(), {
|
||||
id: 'orcidV2',
|
||||
name: 'orcidV2',
|
||||
hierarchical: false
|
||||
}),
|
||||
Object.assign(new ExternalSource(), {
|
||||
id: 'sherpaPublisher',
|
||||
name: 'sherpaPublisher',
|
||||
hierarchical: false
|
||||
})
|
||||
];
|
||||
const totalLocal = 10;
|
||||
const totalExternal = 8;
|
||||
|
||||
function init() {
|
||||
item = Object.assign(new Item(), { uuid: '7680ca97-e2bd-4398-bfa7-139a8673dc42', metadata: {} });
|
||||
@@ -41,6 +65,14 @@ describe('DsDynamicLookupRelationModalComponent', () => {
|
||||
relationship = { filter: 'filter', relationshipType: 'isAuthorOfPublication', nameVariants: true } as RelationshipOptions;
|
||||
nameVariant = 'Doe, J.';
|
||||
metadataField = 'dc.contributor.author';
|
||||
pSearchOptions = new PaginatedSearchOptions({});
|
||||
externalSourceService = jasmine.createSpyObj('externalSourceService', {
|
||||
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(externalSources))
|
||||
});
|
||||
lookupRelationService = jasmine.createSpyObj('lookupRelationService', {
|
||||
getTotalLocalResults: observableOf(totalLocal),
|
||||
getTotalExternalResults: observableOf(totalExternal)
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async(() => {
|
||||
@@ -49,6 +81,13 @@ describe('DsDynamicLookupRelationModalComponent', () => {
|
||||
declarations: [DsDynamicLookupRelationModalComponent],
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NgbModule.forRoot()],
|
||||
providers: [
|
||||
{
|
||||
provide: SearchConfigurationService, useValue: {
|
||||
paginatedSearchOptions: observableOf(pSearchOptions)
|
||||
}
|
||||
},
|
||||
{ provide: ExternalSourceService, useValue: externalSourceService },
|
||||
{ provide: LookupRelationService, useValue: lookupRelationService },
|
||||
{
|
||||
provide: SelectableListService, useValue: selectableListService
|
||||
},
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Component, NgZone, OnDestroy, OnInit } from '@angular/core';
|
||||
import { combineLatest, Observable, Subscription } from 'rxjs';
|
||||
import { combineLatest, Observable, Subscription, zip as observableZip } from 'rxjs';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { hasValue } from '../../../../empty.util';
|
||||
import { map, skip, switchMap, take } from 'rxjs/operators';
|
||||
@@ -11,7 +11,11 @@ import { ListableObject } from '../../../../object-collection/shared/listable-ob
|
||||
import { RelationshipOptions } from '../../models/relationship-options.model';
|
||||
import { SearchResult } from '../../../../search/search-result.model';
|
||||
import { Item } from '../../../../../core/shared/item.model';
|
||||
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../../core/shared/operators';
|
||||
import {
|
||||
getAllSucceededRemoteData,
|
||||
getRemoteDataPayload,
|
||||
getSucceededRemoteData
|
||||
} from '../../../../../core/shared/operators';
|
||||
import { AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipAction } from './relationship.actions';
|
||||
import { RelationshipService } from '../../../../../core/data/relationship.service';
|
||||
import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service';
|
||||
@@ -20,6 +24,11 @@ import { AppState } from '../../../../../app.reducer';
|
||||
import { Context } from '../../../../../core/shared/context.model';
|
||||
import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model';
|
||||
import { MetadataValue } from '../../../../../core/shared/metadata.models';
|
||||
import { LookupRelationService } from '../../../../../core/data/lookup-relation.service';
|
||||
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../../../core/data/paginated-list';
|
||||
import { ExternalSource } from '../../../../../core/shared/external-source.model';
|
||||
import { ExternalSourceService } from '../../../../../core/data/external-source.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dynamic-lookup-relation-modal',
|
||||
@@ -37,23 +46,76 @@ import { MetadataValue } from '../../../../../core/shared/metadata.models';
|
||||
* Represents a modal where the submitter can select items to be added as a certain relationship type to the object being submitted
|
||||
*/
|
||||
export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* The label to use to display i18n messages (describing the type of relationship)
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* Options for searching related items
|
||||
*/
|
||||
relationshipOptions: RelationshipOptions;
|
||||
|
||||
/**
|
||||
* The ID of the list to add/remove selected items to/from
|
||||
*/
|
||||
listId: string;
|
||||
|
||||
/**
|
||||
* The item we're adding relationships to
|
||||
*/
|
||||
item;
|
||||
|
||||
/**
|
||||
* Is the selection repeatable?
|
||||
*/
|
||||
repeatable: boolean;
|
||||
|
||||
/**
|
||||
* The list of selected items
|
||||
*/
|
||||
selection$: Observable<ListableObject[]>;
|
||||
|
||||
/**
|
||||
* The context to display lists
|
||||
*/
|
||||
context: Context;
|
||||
|
||||
/**
|
||||
* The metadata-fields describing these relationships
|
||||
*/
|
||||
metadataFields: string;
|
||||
|
||||
/**
|
||||
* A map of subscriptions within this component
|
||||
*/
|
||||
subMap: {
|
||||
[uuid: string]: Subscription
|
||||
} = {};
|
||||
|
||||
/**
|
||||
* A list of the available external sources configured for this relationship
|
||||
*/
|
||||
externalSourcesRD$: Observable<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(
|
||||
public modal: NgbActiveModal,
|
||||
private selectableListService: SelectableListService,
|
||||
private relationshipService: RelationshipService,
|
||||
private relationshipTypeService: RelationshipTypeService,
|
||||
private externalSourceService: ExternalSourceService,
|
||||
private lookupRelationService: LookupRelationService,
|
||||
private searchConfigService: SearchConfigurationService,
|
||||
private zone: NgZone,
|
||||
private store: Store<AppState>
|
||||
) {
|
||||
@@ -70,6 +132,9 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
||||
this.context = Context.SubmissionModal;
|
||||
}
|
||||
|
||||
this.externalSourcesRD$ = this.externalSourceService.findAll();
|
||||
|
||||
this.setTotals();
|
||||
// this.setExistingNameVariants();
|
||||
}
|
||||
|
||||
@@ -77,6 +142,10 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select (a list of) objects and add them to the store
|
||||
* @param selectableObjects
|
||||
*/
|
||||
select(...selectableObjects: Array<SearchResult<Item>>) {
|
||||
this.zone.runOutsideAngular(
|
||||
() => {
|
||||
@@ -104,6 +173,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>) {
|
||||
const nameVariant$ = this.relationshipService.getNameVariant(this.listId, sri.indexableObject.uuid);
|
||||
this.subMap[sri.indexableObject.uuid] = nameVariant$.pipe(
|
||||
@@ -111,6 +184,10 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
||||
).subscribe((nameVariant: string) => this.store.dispatch(new UpdateRelationshipAction(this.item, sri.indexableObject, this.relationshipOptions.relationshipType, nameVariant)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Deselect (a list of) objects and remove them from the store
|
||||
* @param selectableObjects
|
||||
*/
|
||||
deselect(...selectableObjects: Array<SearchResult<Item>>) {
|
||||
this.zone.runOutsideAngular(
|
||||
() => selectableObjects.forEach((object) => {
|
||||
@@ -120,6 +197,9 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set existing name variants for items by the item's virtual metadata
|
||||
*/
|
||||
private setExistingNameVariants() {
|
||||
const virtualMDs: MetadataValue[] = this.item.allMetadata(this.metadataFields).filter((mdValue) => mdValue.isVirtual);
|
||||
|
||||
@@ -154,6 +234,28 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate and set the total entries available for each tab
|
||||
*/
|
||||
setTotals() {
|
||||
this.totalInternal$ = this.searchConfigService.paginatedSearchOptions.pipe(
|
||||
switchMap((options) => this.lookupRelationService.getTotalLocalResults(this.relationshipOptions, options))
|
||||
);
|
||||
|
||||
const externalSourcesAndOptions$ = combineLatest(
|
||||
this.externalSourcesRD$.pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
getRemoteDataPayload()
|
||||
),
|
||||
this.searchConfigService.paginatedSearchOptions
|
||||
);
|
||||
|
||||
this.totalExternal$ = externalSourcesAndOptions$.pipe(
|
||||
switchMap(([sources, options]) =>
|
||||
observableZip(...sources.page.map((source: ExternalSource) => this.lookupRelationService.getTotalExternalResults(source, options))))
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
Object.values(this.subMap).forEach((subscription) => subscription.unsubscribe());
|
||||
}
|
||||
|
@@ -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>
|
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@@ -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)))
|
||||
)
|
||||
}
|
||||
}
|
@@ -3,7 +3,7 @@
|
||||
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
|
||||
[inPlaceSearch]="true" [showViewModes]="false"></ds-search-sidebar>
|
||||
<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>
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<ds-search-results [searchResults]="(resultsRD$ | async)"
|
||||
[sortConfig]="this.searchConfig?.sort"
|
||||
[searchConfig]="this.searchConfig"
|
||||
[sortConfig]="this.lookupRelationService.searchConfig?.sort"
|
||||
[searchConfig]="this.lookupRelationService.searchConfig"
|
||||
[selectable]="true"
|
||||
[selectionConfig]="{ repeatable: repeatable, listId: listId }"
|
||||
[linkType]="linkTypes.ExternalLink"
|
||||
@@ -66,4 +66,4 @@
|
||||
(selectObject)="selectObject.emit($event)">
|
||||
</ds-search-results>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -15,6 +15,8 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../../testing/utils'
|
||||
import { PaginatedList } from '../../../../../../core/data/paginated-list';
|
||||
import { ItemSearchResult } from '../../../../../object-collection/shared/item-search-result.model';
|
||||
import { Item } from '../../../../../../core/shared/item.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { LookupRelationService } from '../../../../../../core/data/lookup-relation.service';
|
||||
|
||||
describe('DsDynamicLookupRelationSearchTabComponent', () => {
|
||||
let component: DsDynamicLookupRelationSearchTabComponent;
|
||||
@@ -34,6 +36,7 @@ describe('DsDynamicLookupRelationSearchTabComponent', () => {
|
||||
|
||||
let results;
|
||||
let selectableListService;
|
||||
let lookupRelationService;
|
||||
|
||||
function init() {
|
||||
relationship = { filter: 'filter', relationshipType: 'isAuthorOfPublication', nameVariants: true } as RelationshipOptions;
|
||||
@@ -51,6 +54,10 @@ describe('DsDynamicLookupRelationSearchTabComponent', () => {
|
||||
|
||||
results = new PaginatedList(undefined, [searchResult1, searchResult2, searchResult3]);
|
||||
selectableListService = jasmine.createSpyObj('selectableListService', ['deselect', 'select', 'deselectAll']);
|
||||
lookupRelationService = jasmine.createSpyObj('lookupRelationService', {
|
||||
getLocalResults: createSuccessfulRemoteDataObject$(results)
|
||||
});
|
||||
lookupRelationService.searchConfig = {};
|
||||
}
|
||||
|
||||
beforeEach(async(() => {
|
||||
@@ -75,6 +82,8 @@ describe('DsDynamicLookupRelationSearchTabComponent', () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
{ provide: ActivatedRoute, useValue: { snapshot: { queryParams: {} } } },
|
||||
{ provide: LookupRelationService, useValue: lookupRelationService }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
})
|
||||
|
@@ -11,14 +11,16 @@ import { RelationshipOptions } from '../../../models/relationship-options.model'
|
||||
import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model';
|
||||
import { ListableObject } from '../../../../../object-collection/shared/listable-object.model';
|
||||
import { SearchService } from '../../../../../../core/shared/search/search.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service';
|
||||
import { hasValue, isNotEmpty } from '../../../../../empty.util';
|
||||
import { concat, map, multicast, switchMap, take, takeWhile, tap } from 'rxjs/operators';
|
||||
import { DSpaceObject } from '../../../../../../core/shared/dspace-object.model';
|
||||
import { getSucceededRemoteData } from '../../../../../../core/shared/operators';
|
||||
import { RouteService } from '../../../../../../core/services/route.service';
|
||||
import { CollectionElementLinkType } from '../../../../../object-collection/collection-element-link.type';
|
||||
import { Context } from '../../../../../../core/shared/context.model';
|
||||
import { LookupRelationService } from '../../../../../../core/data/lookup-relation.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dynamic-lookup-relation-search-tab',
|
||||
@@ -36,32 +38,87 @@ import { Context } from '../../../../../../core/shared/context.model';
|
||||
* Tab for inside the lookup model that represents the items that can be used as a relationship in this submission
|
||||
*/
|
||||
export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Options for searching related items
|
||||
*/
|
||||
@Input() relationship: RelationshipOptions;
|
||||
|
||||
/**
|
||||
* The ID of the list to add/remove selected items to/from
|
||||
*/
|
||||
@Input() listId: string;
|
||||
|
||||
/**
|
||||
* Is the selection repeatable?
|
||||
*/
|
||||
@Input() repeatable: boolean;
|
||||
|
||||
/**
|
||||
* The list of selected items
|
||||
*/
|
||||
@Input() selection$: Observable<ListableObject[]>;
|
||||
|
||||
/**
|
||||
* 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>();
|
||||
|
||||
/**
|
||||
* Search results
|
||||
*/
|
||||
resultsRD$: Observable<RemoteData<PaginatedList<SearchResult<Item>>>>;
|
||||
searchConfig: PaginatedSearchOptions;
|
||||
|
||||
/**
|
||||
* Are all results selected?
|
||||
*/
|
||||
allSelected: boolean;
|
||||
|
||||
/**
|
||||
* Are some results selected?
|
||||
*/
|
||||
someSelected$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Is it currently loading to select all results?
|
||||
*/
|
||||
selectAllLoading: boolean;
|
||||
|
||||
/**
|
||||
* Subscription to unsubscribe from
|
||||
*/
|
||||
subscription;
|
||||
|
||||
/**
|
||||
* The initial pagination to use
|
||||
*/
|
||||
initialPagination = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'submission-relation-list',
|
||||
pageSize: 5
|
||||
});
|
||||
|
||||
/**
|
||||
* The type of links to display
|
||||
*/
|
||||
linkTypes = CollectionElementLinkType;
|
||||
|
||||
constructor(
|
||||
private searchService: SearchService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private selectableListService: SelectableListService,
|
||||
private searchConfigService: SearchConfigurationService,
|
||||
public searchConfigService: SearchConfigurationService,
|
||||
private routeService: RouteService,
|
||||
public lookupRelationService: LookupRelationService
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -75,24 +132,8 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest
|
||||
|
||||
this.someSelected$ = this.selection$.pipe(map((selection) => isNotEmpty(selection)));
|
||||
this.resultsRD$ = this.searchConfigService.paginatedSearchOptions.pipe(
|
||||
map((options) => {
|
||||
return Object.assign(new PaginatedSearchOptions({}), options, { fixedFilter: this.relationship.filter, configuration: this.relationship.searchConfiguration })
|
||||
}),
|
||||
switchMap((options) => {
|
||||
this.searchConfig = options;
|
||||
return this.searchService.search(options).pipe(
|
||||
/* Make sure to only listen to the first x results, until loading is finished */
|
||||
/* TODO: in Rxjs 6.4.0 and up, we can replace this with takeWhile(predicate, true) - see https://stackoverflow.com/a/44644237 */
|
||||
multicast(
|
||||
() => new ReplaySubject(1),
|
||||
(subject) => subject.pipe(
|
||||
takeWhile((rd: RemoteData<PaginatedList<SearchResult<Item>>>) => rd.isLoading),
|
||||
concat(subject.pipe(take(1)))
|
||||
)
|
||||
) as any
|
||||
)
|
||||
})
|
||||
) as Observable<RemoteData<PaginatedList<SearchResult<Item>>>>;
|
||||
switchMap((options) => this.lookupRelationService.getLocalResults(this.relationship, options))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,7 +141,7 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest
|
||||
*/
|
||||
resetRoute() {
|
||||
this.router.navigate([], {
|
||||
queryParams: Object.assign({}, { page: 1, pageSize: this.initialPagination.pageSize }),
|
||||
queryParams: Object.assign({}, { pageSize: this.initialPagination.pageSize }, this.route.snapshot.queryParams, { page: 1 })
|
||||
});
|
||||
}
|
||||
|
||||
@@ -143,7 +184,7 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest
|
||||
currentPage: 1,
|
||||
pageSize: 9999
|
||||
});
|
||||
const fullSearchConfig = Object.assign(this.searchConfig, { pagination: fullPagination });
|
||||
const fullSearchConfig = Object.assign(this.lookupRelationService.searchConfig, { pagination: fullPagination });
|
||||
const results$ = this.searchService.search(fullSearchConfig) as Observable<RemoteData<PaginatedList<SearchResult<Item>>>>;
|
||||
results$.pipe(
|
||||
getSucceededRemoteData(),
|
||||
|
@@ -73,11 +73,6 @@ describe('DsDynamicLookupRelationSelectionTabComponent', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call navigate on the router when is called resetRoute', () => {
|
||||
component.resetRoute();
|
||||
expect(router.navigate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call navigate on the router when is called resetRoute', () => {
|
||||
component.selectionRD$ = createSelection([]);
|
||||
fixture.detectChanges();
|
||||
|
@@ -29,15 +29,49 @@ import { Context } from '../../../../../../core/shared/context.model';
|
||||
* Tab for inside the lookup model that represents the currently selected relationships
|
||||
*/
|
||||
export class DsDynamicLookupRelationSelectionTabComponent {
|
||||
/**
|
||||
* The label to use to display i18n messages (describing the type of relationship)
|
||||
*/
|
||||
@Input() label: string;
|
||||
|
||||
/**
|
||||
* The ID of the list to add/remove selected items to/from
|
||||
*/
|
||||
@Input() listId: string;
|
||||
|
||||
/**
|
||||
* Is the selection repeatable?
|
||||
*/
|
||||
@Input() repeatable: boolean;
|
||||
|
||||
/**
|
||||
* The list of selected items
|
||||
*/
|
||||
@Input() selection$: Observable<ListableObject[]>;
|
||||
|
||||
/**
|
||||
* The paginated list of selected items
|
||||
*/
|
||||
@Input() selectionRD$: Observable<RemoteData<PaginatedList<ListableObject>>>;
|
||||
|
||||
/**
|
||||
* 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 use
|
||||
*/
|
||||
initialPagination = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'submission-relation-list',
|
||||
pageSize: 5
|
||||
@@ -51,7 +85,6 @@ export class DsDynamicLookupRelationSelectionTabComponent {
|
||||
* Set up the selection and pagination on load
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.resetRoute();
|
||||
this.selectionRD$ = this.searchConfigService.paginatedSearchOptions
|
||||
.pipe(
|
||||
map((options: PaginatedSearchOptions) => options.pagination),
|
||||
@@ -75,13 +108,4 @@ export class DsDynamicLookupRelationSelectionTabComponent {
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to reset the route when the window is opened to make sure no strange pagination issues appears
|
||||
*/
|
||||
resetRoute() {
|
||||
this.router.navigate([], {
|
||||
queryParams: Object.assign({}, { page: 1, pageSize: this.initialPagination.pageSize }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -176,6 +176,7 @@ import { SidebarFilterComponent } from './sidebar/filter/sidebar-filter.componen
|
||||
import { SidebarFilterSelectedOptionComponent } from './sidebar/filter/sidebar-filter-selected-option.component';
|
||||
import { MetadataRepresentationListComponent } from '../+item-page/simple/metadata-representation-list/metadata-representation-list.component';
|
||||
import { SelectableListItemControlComponent } from './object-collection/shared/selectable-list-item-control/selectable-list-item-control.component';
|
||||
import { DsDynamicLookupRelationExternalSourceTabComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component';
|
||||
|
||||
const MODULES = [
|
||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||
@@ -395,7 +396,8 @@ const ENTRY_COMPONENTS = [
|
||||
SearchFacetRangeOptionComponent,
|
||||
SearchAuthorityFilterComponent,
|
||||
DsDynamicLookupRelationSearchTabComponent,
|
||||
DsDynamicLookupRelationSelectionTabComponent
|
||||
DsDynamicLookupRelationSelectionTabComponent,
|
||||
DsDynamicLookupRelationExternalSourceTabComponent
|
||||
];
|
||||
|
||||
const SHARED_ITEM_PAGE_COMPONENTS = [
|
||||
|
Reference in New Issue
Block a user