diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index fc4c0aee570..478f183da44 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -1566,6 +1566,8 @@ "search.results.no-results-link": "quotes around it", + "search.results.empty": "Your search returned no results.", + "search.sidebar.close": "Back to results", @@ -1639,13 +1641,21 @@ "submission.sections.describe.relationship-lookup.selected": "Selected {{ size }} items", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Author": "Search for Authors", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Author": "Local Authors ({{ count }})", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Local Journals ({{ count }})", + + "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": "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.Journal": "Search for Journals", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Publishers ({{ 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.orcidV2": "ORCID ({{ 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.lcname": "LC Names ({{ count }})", "submission.sections.describe.relationship-lookup.search-tab.tab-title.Funding Agency": "Search for Funding Agencies", @@ -1679,6 +1689,14 @@ "submission.sections.describe.relationship-lookup.selection-tab.title.Journal Issue": "Selected Issue", + "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaJournal": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Search Results", + "submission.sections.describe.relationship-lookup.name-variant.notification.content": "Would you like to save \"{{ value }}\" as a name variant for this person so you and others can reuse it for future submissions? If you don\'t you can still use it for this submission.", "submission.sections.describe.relationship-lookup.name-variant.notification.confirm": "Save a new name variant", diff --git a/src/app/+search-page/configuration-search-page.component.ts b/src/app/+search-page/configuration-search-page.component.ts index 33d99a9cd2b..2cde216c059 100644 --- a/src/app/+search-page/configuration-search-page.component.ts +++ b/src/app/+search-page/configuration-search-page.component.ts @@ -5,10 +5,10 @@ import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angu import { pushInOut } from '../shared/animations/push'; import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; -import { Router } from '@angular/router'; import { hasValue } from '../shared/empty.util'; import { RouteService } from '../core/services/route.service'; import { SearchService } from '../core/shared/search/search.service'; +import { Router } from '@angular/router'; /** * This component renders a search page using a configuration as input. @@ -61,5 +61,8 @@ export class ConfigurationSearchPageComponent extends SearchComponent implements if (hasValue(this.configuration)) { this.routeService.setParameter('configuration', this.configuration); } + if (hasValue(this.fixedFilterQuery)) { + this.routeService.setParameter('fixedFilter', this.fixedFilterQuery); + } } } diff --git a/src/app/+search-page/search-page.module.ts b/src/app/+search-page/search-page.module.ts index e5ce6700131..ca6cbf9e5c4 100644 --- a/src/app/+search-page/search-page.module.ts +++ b/src/app/+search-page/search-page.module.ts @@ -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 }) diff --git a/src/app/core/cache/models/normalized-external-source-entry.model.ts b/src/app/core/cache/models/normalized-external-source-entry.model.ts new file mode 100644 index 00000000000..e8e3c695c31 --- /dev/null +++ b/src/app/core/cache/models/normalized-external-source-entry.model.ts @@ -0,0 +1,36 @@ +import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; +import { NormalizedObject } from './normalized-object.model'; +import { ExternalSourceEntry } from '../../shared/external-source-entry.model'; +import { mapsTo } from '../builders/build-decorators'; +import { MetadataMap, MetadataMapSerializer } from '../../shared/metadata.models'; + +/** + * Normalized model class for an external source entry + */ +@mapsTo(ExternalSourceEntry) +@inheritSerialization(NormalizedObject) +export class NormalizedExternalSourceEntry extends NormalizedObject { + /** + * Unique identifier + */ + @autoserialize + id: string; + + /** + * The value to display + */ + @autoserialize + display: string; + + /** + * The value to store the entry with + */ + @autoserialize + value: string; + + /** + * Metadata of the entry + */ + @autoserializeAs(MetadataMapSerializer) + metadata: MetadataMap; +} diff --git a/src/app/core/cache/models/normalized-external-source.model.ts b/src/app/core/cache/models/normalized-external-source.model.ts new file mode 100644 index 00000000000..fd9a42fb721 --- /dev/null +++ b/src/app/core/cache/models/normalized-external-source.model.ts @@ -0,0 +1,29 @@ +import { autoserialize, inheritSerialization } from 'cerialize'; +import { NormalizedObject } from './normalized-object.model'; +import { ExternalSource } from '../../shared/external-source.model'; +import { mapsTo } from '../builders/build-decorators'; + +/** + * Normalized model class for an external source + */ +@mapsTo(ExternalSource) +@inheritSerialization(NormalizedObject) +export class NormalizedExternalSource extends NormalizedObject { + /** + * Unique identifier + */ + @autoserialize + id: string; + + /** + * The name of this external source + */ + @autoserialize + name: string; + + /** + * Is the source hierarchical? + */ + @autoserialize + hierarchical: boolean; +} diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 4fdef02357e..efd83d33d57 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -136,6 +136,10 @@ import { SearchConfigurationService } from './shared/search/search-configuration import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service'; import { RelationshipTypeService } from './data/relationship-type.service'; import { SidebarService } from '../shared/sidebar/sidebar.service'; +import { NormalizedExternalSource } from './cache/models/normalized-external-source.model'; +import { NormalizedExternalSourceEntry } from './cache/models/normalized-external-source-entry.model'; +import { ExternalSourceService } from './data/external-source.service'; +import { LookupRelationService } from './data/lookup-relation.service'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -247,6 +251,8 @@ const PROVIDERS = [ SearchConfigurationService, SelectableListService, RelationshipTypeService, + ExternalSourceService, + LookupRelationService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, @@ -292,7 +298,9 @@ export const normalizedModels = NormalizedPoolTask, NormalizedRelationship, NormalizedRelationshipType, - NormalizedItemType + NormalizedItemType, + NormalizedExternalSource, + NormalizedExternalSourceEntry ]; @NgModule({ diff --git a/src/app/core/data/external-source.service.spec.ts b/src/app/core/data/external-source.service.spec.ts new file mode 100644 index 00000000000..77a2a85dfd4 --- /dev/null +++ b/src/app/core/data/external-source.service.spec.ts @@ -0,0 +1,76 @@ +import { ExternalSourceService } from './external-source.service'; +import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { ExternalSourceEntry } from '../shared/external-source-entry.model'; +import { of as observableOf } from 'rxjs'; +import { GetRequest } from './request.models'; + +describe('ExternalSourceService', () => { + let service: ExternalSourceService; + + let requestService; + let rdbService; + let halService; + + const entries = [ + Object.assign(new ExternalSourceEntry(), { + id: '0001-0001-0001-0001', + display: 'John Doe', + value: 'John, Doe', + metadata: { + 'dc.identifier.uri': [ + { + value: 'https://orcid.org/0001-0001-0001-0001' + } + ] + } + }), + Object.assign(new ExternalSourceEntry(), { + id: '0001-0001-0001-0002', + display: 'Sampson Megan', + value: 'Sampson, Megan', + metadata: { + 'dc.identifier.uri': [ + { + value: 'https://orcid.org/0001-0001-0001-0002' + } + ] + } + }) + ]; + + function init() { + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: 'request-uuid', + configure: {} + }); + rdbService = jasmine.createSpyObj('rdbService', { + buildList: createSuccessfulRemoteDataObject$(createPaginatedList(entries)) + }); + halService = jasmine.createSpyObj('halService', { + getEndpoint: observableOf('external-sources-REST-endpoint') + }); + service = new ExternalSourceService(requestService, rdbService, undefined, undefined, undefined, halService, undefined, undefined, undefined); + } + + beforeEach(() => { + init(); + }); + + describe('getExternalSourceEntries', () => { + let result; + + beforeEach(() => { + result = service.getExternalSourceEntries('test'); + }); + + it('should configure a GetRequest', () => { + expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest)); + }); + + it('should return the entries', () => { + result.subscribe((resultRD) => { + expect(resultRD.payload.page).toBe(entries); + }); + }); + }); +}); diff --git a/src/app/core/data/external-source.service.ts b/src/app/core/data/external-source.service.ts new file mode 100644 index 00000000000..c32c13a20fd --- /dev/null +++ b/src/app/core/data/external-source.service.ts @@ -0,0 +1,85 @@ +import { Injectable } from '@angular/core'; +import { DataService } from './data.service'; +import { ExternalSource } from '../shared/external-source.model'; +import { RequestService } from './request.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { FindListOptions, GetRequest } from './request.models'; +import { Observable } from 'rxjs/internal/Observable'; +import { distinctUntilChanged, map, switchMap } from 'rxjs/operators'; +import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { hasValue, isNotEmptyOperator } from '../../shared/empty.util'; +import { configureRequest } from '../shared/operators'; +import { RemoteData } from './remote-data'; +import { PaginatedList } from './paginated-list'; +import { ExternalSourceEntry } from '../shared/external-source-entry.model'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; + +/** + * A service handling all external source requests + */ +@Injectable() +export class ExternalSourceService extends DataService { + protected linkPath = 'externalsources'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + super(); + } + + /** + * Get the endpoint to browse external sources + * @param options + * @param linkPath + */ + getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable { + return this.halService.getEndpoint(linkPath); + } + + /** + * Get the endpoint for an external source's entries + * @param externalSourceId The id of the external source to fetch entries for + */ + getEntriesEndpoint(externalSourceId: string): Observable { + return this.getBrowseEndpoint().pipe( + map((href) => this.getIDHref(href, externalSourceId)), + switchMap((href) => this.halService.getEndpoint('entries', href)) + ); + } + + /** + * Get the entries for an external source + * @param externalSourceId The id of the external source to fetch entries for + * @param searchOptions The search options to limit results to + */ + getExternalSourceEntries(externalSourceId: string, searchOptions?: PaginatedSearchOptions): Observable>> { + const requestUuid = this.requestService.generateRequestId(); + + const href$ = this.getEntriesEndpoint(externalSourceId).pipe( + isNotEmptyOperator(), + distinctUntilChanged(), + map((endpoint: string) => hasValue(searchOptions) ? searchOptions.toRestUrl(endpoint) : endpoint) + ); + + href$.pipe( + map((endpoint: string) => new GetRequest(requestUuid, endpoint)), + configureRequest(this.requestService) + ).subscribe(); + + return this.rdbService.buildList(href$); + } +} diff --git a/src/app/core/data/lookup-relation.service.spec.ts b/src/app/core/data/lookup-relation.service.spec.ts new file mode 100644 index 00000000000..321fd8d218a --- /dev/null +++ b/src/app/core/data/lookup-relation.service.spec.ts @@ -0,0 +1,116 @@ +import { LookupRelationService } from './lookup-relation.service'; +import { ExternalSourceService } from './external-source.service'; +import { SearchService } from '../shared/search/search.service'; +import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { PaginatedList } from './paginated-list'; +import { PageInfo } from '../shared/page-info.model'; +import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model'; +import { SearchResult } from '../../shared/search/search-result.model'; +import { Item } from '../shared/item.model'; +import { skip, take } from 'rxjs/operators'; +import { ExternalSource } from '../shared/external-source.model'; + +describe('LookupRelationService', () => { + let service: LookupRelationService; + let externalSourceService: ExternalSourceService; + let searchService: SearchService; + + const totalExternal = 8; + const optionsWithQuery = new PaginatedSearchOptions({ query: 'test-query' }); + const relationship = Object.assign(new RelationshipOptions(), { + filter: 'test-filter', + configuration: 'test-configuration' + }); + const localResults = [ + Object.assign(new SearchResult(), { + indexableObject: Object.assign(new Item(), { + uuid: 'test-item-uuid', + handle: 'test-item-handle' + }) + }) + ]; + const externalSource = Object.assign(new ExternalSource(), { + id: 'orcidV2', + name: 'orcidV2', + hierarchical: false + }); + + function init() { + externalSourceService = jasmine.createSpyObj('externalSourceService', { + getExternalSourceEntries: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: totalExternal, totalPages: totalExternal, currentPage: 1 }), [{}])) + }); + searchService = jasmine.createSpyObj('searchService', { + search: createSuccessfulRemoteDataObject$(createPaginatedList(localResults)) + }); + service = new LookupRelationService(externalSourceService, searchService); + } + + beforeEach(() => { + init(); + }); + + describe('getLocalResults', () => { + let result; + + beforeEach(() => { + result = service.getLocalResults(relationship, optionsWithQuery); + }); + + it('should return the local results', () => { + result.subscribe((resultsRD) => { + expect(resultsRD.payload.page).toBe(localResults); + }); + }); + + it('should set the searchConfig to contain a fixedFilter and configuration', () => { + expect(service.searchConfig).toEqual(Object.assign(new PaginatedSearchOptions({}), optionsWithQuery, + { fixedFilter: relationship.filter, configuration: relationship.searchConfiguration } + )); + }); + }); + + describe('getTotalLocalResults', () => { + let result; + + beforeEach(() => { + result = service.getTotalLocalResults(relationship, optionsWithQuery); + }); + + it('should start with 0', () => { + result.pipe(take(1)).subscribe((amount) => { + expect(amount).toEqual(0) + }); + }); + + it('should return the correct total amount', () => { + result.pipe(skip(1)).subscribe((amount) => { + expect(amount).toEqual(localResults.length) + }); + }); + + it('should not set searchConfig', () => { + expect(service.searchConfig).toBeUndefined(); + }); + }); + + describe('getTotalExternalResults', () => { + let result; + + beforeEach(() => { + result = service.getTotalExternalResults(externalSource, optionsWithQuery); + }); + + it('should start with 0', () => { + result.pipe(take(1)).subscribe((amount) => { + expect(amount).toEqual(0) + }); + }); + + it('should return the correct total amount', () => { + result.pipe(skip(1)).subscribe((amount) => { + expect(amount).toEqual(totalExternal) + }); + }); + }); +}); diff --git a/src/app/core/data/lookup-relation.service.ts b/src/app/core/data/lookup-relation.service.ts new file mode 100644 index 00000000000..ad977e42dc1 --- /dev/null +++ b/src/app/core/data/lookup-relation.service.ts @@ -0,0 +1,94 @@ +import { ExternalSourceService } from './external-source.service'; +import { SearchService } from '../shared/search/search.service'; +import { concat, map, multicast, startWith, take, takeWhile } from 'rxjs/operators'; +import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { ReplaySubject } from 'rxjs/internal/ReplaySubject'; +import { RemoteData } from './remote-data'; +import { PaginatedList } from './paginated-list'; +import { SearchResult } from '../../shared/search/search-result.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model'; +import { Observable } from 'rxjs/internal/Observable'; +import { Item } from '../shared/item.model'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { getAllSucceededRemoteData, getRemoteDataPayload } from '../shared/operators'; +import { Injectable } from '@angular/core'; +import { ExternalSource } from '../shared/external-source.model'; +import { ExternalSourceEntry } from '../shared/external-source-entry.model'; + +/** + * A service for retrieving local and external entries information during a relation lookup + */ +@Injectable() +export class LookupRelationService { + /** + * The search config last used for retrieving local results + */ + public searchConfig: PaginatedSearchOptions; + + /** + * Pagination options for retrieving exactly one result + */ + private singleResultOptions = Object.assign(new PaginationComponentOptions(), { + id: 'single-result-options', + pageSize: 1 + }); + + constructor(protected externalSourceService: ExternalSourceService, + protected searchService: SearchService) { + } + + /** + * Retrieve the available local entries for a relationship + * @param relationship Relationship options + * @param searchOptions Search options to filter results + * @param setSearchConfig Optionally choose if we should store the used search config in a local variable (defaults to true) + */ + getLocalResults(relationship: RelationshipOptions, searchOptions: PaginatedSearchOptions, setSearchConfig = true): Observable>>> { + const newConfig = Object.assign(new PaginatedSearchOptions({}), searchOptions, + { fixedFilter: relationship.filter, configuration: relationship.searchConfiguration } + ); + if (setSearchConfig) { + this.searchConfig = newConfig; + } + return this.searchService.search(newConfig).pipe( + /* Make sure to only listen to the first x results, until loading is finished */ + /* TODO: in Rxjs 6.4.0 and up, we can replace this with takeWhile(predicate, true) - see https://stackoverflow.com/a/44644237 */ + multicast( + () => new ReplaySubject(1), + (subject) => subject.pipe( + takeWhile((rd: RemoteData>>) => rd.isLoading), + concat(subject.pipe(take(1))) + ) + ) as any + ) as Observable>>>; + } + + /** + * Calculate the total local entries available for the given relationship + * @param relationship Relationship options + * @param searchOptions Search options to filter results + */ + getTotalLocalResults(relationship: RelationshipOptions, searchOptions: PaginatedSearchOptions): Observable { + return this.getLocalResults(relationship, Object.assign(new PaginatedSearchOptions({}), searchOptions, { pagination: this.singleResultOptions }), false).pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload(), + map((results: PaginatedList>) => results.totalElements), + startWith(0) + ); + } + + /** + * Calculate the total external entries available for a given external source + * @param externalSource External Source + * @param searchOptions Search options to filter results + */ + getTotalExternalResults(externalSource: ExternalSource, searchOptions: PaginatedSearchOptions): Observable { + return this.externalSourceService.getExternalSourceEntries(externalSource.id, Object.assign(new PaginatedSearchOptions({}), searchOptions, { pagination: this.singleResultOptions })).pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload(), + map((results: PaginatedList) => results.totalElements), + startWith(0) + ); + } +} diff --git a/src/app/core/services/route.service.ts b/src/app/core/services/route.service.ts index 2a0df6d16a7..661f4acf94b 100644 --- a/src/app/core/services/route.service.ts +++ b/src/app/core/services/route.service.ts @@ -176,10 +176,20 @@ export class RouteService { ); } + /** + * Add a parameter to the current route + * @param key The parameter name + * @param value The parameter value + */ public addParameter(key, value) { this.store.dispatch(new AddParameterAction(key, value)); } + /** + * Set a parameter in the current route (overriding the previous value) + * @param key The parameter name + * @param value The parameter value + */ public setParameter(key, value) { this.store.dispatch(new SetParameterAction(key, value)); } diff --git a/src/app/core/shared/external-source-entry.model.ts b/src/app/core/shared/external-source-entry.model.ts new file mode 100644 index 00000000000..be52f96b071 --- /dev/null +++ b/src/app/core/shared/external-source-entry.model.ts @@ -0,0 +1,43 @@ +import { MetadataMap } from './metadata.models'; +import { ResourceType } from './resource-type'; +import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; +import { GenericConstructor } from './generic-constructor'; + +/** + * Model class for a single entry from an external source + */ +export class ExternalSourceEntry extends ListableObject { + static type = new ResourceType('externalSourceEntry'); + + /** + * Unique identifier + */ + id: string; + + /** + * The value to display + */ + display: string; + + /** + * The value to store the entry with + */ + value: string; + + /** + * Metadata of the entry + */ + metadata: MetadataMap; + + /** + * The link to the rest endpoint where this External Source Entry can be found + */ + self: string; + + /** + * Method that returns as which type of object this object should be rendered + */ + getRenderTypes(): Array> { + return [this.constructor as GenericConstructor]; + } +} diff --git a/src/app/core/shared/external-source.model.ts b/src/app/core/shared/external-source.model.ts new file mode 100644 index 00000000000..a158f18f5d6 --- /dev/null +++ b/src/app/core/shared/external-source.model.ts @@ -0,0 +1,29 @@ +import { ResourceType } from './resource-type'; +import { CacheableObject } from '../cache/object-cache.reducer'; + +/** + * Model class for an external source + */ +export class ExternalSource extends CacheableObject { + static type = new ResourceType('externalsource'); + + /** + * Unique identifier + */ + id: string; + + /** + * The name of this external source + */ + name: string; + + /** + * Is the source hierarchical? + */ + hierarchical: boolean; + + /** + * The link to the rest endpoint where this External Source can be found + */ + self: string; +} diff --git a/src/app/entity-groups/research-entities/research-entities.module.ts b/src/app/entity-groups/research-entities/research-entities.module.ts index 86c2a375da8..cef3b4539b8 100644 --- a/src/app/entity-groups/research-entities/research-entities.module.ts +++ b/src/app/entity-groups/research-entities/research-entities.module.ts @@ -25,6 +25,7 @@ import { PersonInputSuggestionsComponent } from './submission/item-list-elements import { NameVariantModalComponent } from './submission/name-variant-modal/name-variant-modal.component'; import { OrgUnitInputSuggestionsComponent } from './submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component'; import { OrgUnitSearchResultListSubmissionElementComponent } from './submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component'; +import { ExternalSourceEntryListSubmissionElementComponent } from './submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component'; const ENTRY_COMPONENTS = [ OrgUnitComponent, @@ -48,7 +49,8 @@ const ENTRY_COMPONENTS = [ PersonInputSuggestionsComponent, NameVariantModalComponent, OrgUnitSearchResultListSubmissionElementComponent, - OrgUnitInputSuggestionsComponent + OrgUnitInputSuggestionsComponent, + ExternalSourceEntryListSubmissionElementComponent ]; @NgModule({ diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html new file mode 100644 index 00000000000..55b8f38a5ee --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html @@ -0,0 +1,2 @@ +
{{object.display}}
+ diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.scss b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.spec.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.spec.ts new file mode 100644 index 00000000000..fa153b8c5e0 --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.spec.ts @@ -0,0 +1,47 @@ +import { ExternalSourceEntryListSubmissionElementComponent } from './external-source-entry-list-submission-element.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ExternalSourceEntry } from '../../../../../core/shared/external-source-entry.model'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +describe('ExternalSourceEntryListSubmissionElementComponent', () => { + let component: ExternalSourceEntryListSubmissionElementComponent; + let fixture: ComponentFixture; + + const uri = 'https://orcid.org/0001-0001-0001-0001'; + const entry = Object.assign(new ExternalSourceEntry(), { + id: '0001-0001-0001-0001', + display: 'John Doe', + value: 'John, Doe', + metadata: { + 'dc.identifier.uri': [ + { + value: uri + } + ] + } + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ExternalSourceEntryListSubmissionElementComponent], + imports: [TranslateModule.forRoot()], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ExternalSourceEntryListSubmissionElementComponent); + component = fixture.componentInstance; + component.object = entry; + fixture.detectChanges(); + }); + + it('should display the entry\'s display value', () => { + expect(fixture.nativeElement.textContent).toContain(entry.display); + }); + + it('should display the entry\'s uri', () => { + expect(fixture.nativeElement.textContent).toContain(uri); + }); +}); diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts new file mode 100644 index 00000000000..c0512b49956 --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts @@ -0,0 +1,28 @@ +import { AbstractListableElementComponent } from '../../../../../shared/object-collection/shared/object-collection-element/abstract-listable-element.component'; +import { ExternalSourceEntry } from '../../../../../core/shared/external-source-entry.model'; +import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { Context } from '../../../../../core/shared/context.model'; +import { Component, OnInit } from '@angular/core'; +import { Metadata } from '../../../../../core/shared/metadata.utils'; +import { MetadataValue } from '../../../../../core/shared/metadata.models'; + +@listableObjectComponent(ExternalSourceEntry, ViewMode.ListElement, Context.SubmissionModal) +@Component({ + selector: 'ds-external-source-entry-list-submission-element', + styleUrls: ['./external-source-entry-list-submission-element.component.scss'], + templateUrl: './external-source-entry-list-submission-element.component.html' +}) +/** + * The component for displaying a list element of an external source entry + */ +export class ExternalSourceEntryListSubmissionElementComponent extends AbstractListableElementComponent implements OnInit { + /** + * The metadata value for the object's uri + */ + uri: MetadataValue; + + ngOnInit(): void { + this.uri = Metadata.first(this.object.metadata, 'dc.identifier.uri'); + } +} diff --git a/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.ts b/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.ts index 75817d786a3..eb6f7d01ac5 100644 --- a/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.ts +++ b/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.ts @@ -10,7 +10,13 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; templateUrl: './name-variant-modal.component.html', styleUrls: ['./name-variant-modal.component.scss'] }) +/** + * The component for the modal to add a name variant to an item + */ export class NameVariantModalComponent { + /** + * The name variant + */ @Input() value: string; constructor(public modal: NgbActiveModal) { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.ts index 490be050efa..9032cb48cb9 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.ts @@ -11,6 +11,9 @@ import { DynamicDisabledModel } from './dynamic-disabled.model'; selector: 'ds-dynamic-disabled', templateUrl: './dynamic-disabled.component.html' }) +/** + * Component for displaying a form input with a disabled property + */ export class DsDynamicDisabledComponent extends DynamicFormControlComponent { @Input() formId: string; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html index 52f983e7230..46620aa00b6 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html @@ -7,7 +7,7 @@