diff --git a/src/app/core/auth/auth.interceptor.ts b/src/app/core/auth/auth.interceptor.ts index f4e7aa2fd3..3366cdb3d8 100644 --- a/src/app/core/auth/auth.interceptor.ts +++ b/src/app/core/auth/auth.interceptor.ts @@ -251,7 +251,6 @@ export class AuthInterceptor implements HttpInterceptor { // Pass on the new request instead of the original request. return next.handle(newReq).pipe( - // tap((response) => console.log('next.handle: ', response)), map((response) => { // Intercept a Login/Logout response if (response instanceof HttpResponse && this.isSuccess(response) && this.isAuthRequest(response)) { diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 07b600084e..6e8e2abefb 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -144,6 +144,7 @@ import { Vocabulary } from './submission/vocabularies/models/vocabulary.model'; import { VocabularyEntriesResponseParsingService } from './submission/vocabularies/vocabulary-entries-response-parsing.service'; import { VocabularyEntryDetail } from './submission/vocabularies/models/vocabulary-entry-detail.model'; import { VocabularyService } from './submission/vocabularies/vocabulary.service'; +import { VocabularyTreeviewService } from '../shared/vocabulary-treeview/vocabulary-treeview.service'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -268,7 +269,8 @@ const PROVIDERS = [ FilteredDiscoveryPageResponseParsingService, { provide: NativeWindowService, useFactory: NativeWindowFactory }, VocabularyService, - VocabularyEntriesResponseParsingService + VocabularyEntriesResponseParsingService, + VocabularyTreeviewService ]; /** diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 5c56b7f040..b478c9a5c2 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -115,12 +115,6 @@ export abstract class DataService { result$ = this.getSearchEndpoint(searchMethod); - if (hasValue(options.searchParams)) { - options.searchParams.forEach((param: RequestParam) => { - args.push(`${param.fieldName}=${param.fieldValue}`); - }) - } - return result$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args, ...linksToFollow))); } @@ -351,7 +345,7 @@ export abstract class DataService { return hrefObs.pipe( find((href: string) => hasValue(href)), tap((href: string) => { - this.requestService.removeByHrefSubstring(href); + this.requestService.removeByHrefSubstring(searchMethod); const request = new FindListRequest(this.requestService.generateRequestId(), href, options); if (hasValue(this.responseMsToLive)) { request.responseMsToLive = this.responseMsToLive; diff --git a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts index b0c0dba069..94ac30ab56 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts @@ -20,6 +20,7 @@ import { VocabularyService } from './vocabulary.service'; import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; import { VocabularyOptions } from './models/vocabulary-options.model'; +import { VocabularyFindOptions } from './models/vocabulary-find-options.model'; describe('VocabularyService', () => { let scheduler: TestScheduler; @@ -208,9 +209,9 @@ describe('VocabularyService', () => { const endpointURL = `https://rest.api/rest/api/submission/vocabularies`; const requestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}`; const entryDetailEndpointURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails`; - const entryDetailRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:SCB110/${hierarchicalVocabulary.id}:testValue`; - const entryDetailParentRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:SCB110/${hierarchicalVocabulary.id}:testValue/parent`; - const entryDetailChildrenRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:SCB110/${hierarchicalVocabulary.id}:testValue/children`; + const entryDetailRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/${hierarchicalVocabulary.id}:testValue`; + const entryDetailParentRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/${hierarchicalVocabulary.id}:testValue/parent`; + const entryDetailChildrenRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/${hierarchicalVocabulary.id}:testValue/children`; const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; const vocabularyId = 'types'; const metadata = 'dc.type'; @@ -490,7 +491,7 @@ describe('VocabularyService', () => { scheduler = getTestScheduler(); halService = jasmine.createSpyObj('halService', { - getEndpoint: cold('a', { a: endpointURL }) + getEndpoint: cold('a', { a: entryDetailEndpointURL }) }); responseCacheEntry = new RequestEntry(); @@ -517,9 +518,10 @@ describe('VocabularyService', () => { spyOn((service as any).vocabularyEntryDetailDataService, 'findById').and.callThrough(); spyOn((service as any).vocabularyEntryDetailDataService, 'findAll').and.callThrough(); spyOn((service as any).vocabularyEntryDetailDataService, 'findByHref').and.callThrough(); + spyOn((service as any).vocabularyEntryDetailDataService, 'findAllByHref').and.callThrough(); spyOn((service as any).vocabularyEntryDetailDataService, 'searchBy').and.callThrough(); spyOn((service as any).vocabularyEntryDetailDataService, 'getSearchByHref').and.returnValue(observableOf(searchRequestURL)); - spyOn((service as any).vocabularyEntryDetailDataService, 'getFindAllHref').and.returnValue(observableOf(entriesRequestURL)); + spyOn((service as any).vocabularyEntryDetailDataService, 'getFindAllHref').and.returnValue(observableOf(entryDetailChildrenRequestURL)); spyOn((service as any).vocabularyEntryDetailDataService, 'getBrowseEndpoint').and.returnValue(observableOf(entryDetailEndpointURL)); }); @@ -528,7 +530,7 @@ describe('VocabularyService', () => { }); describe('findEntryDetailByHref', () => { - it('should proxy the call to vocabularyDataService.findVocabularyByHref', () => { + it('should proxy the call to vocabularyDataService.findEntryDetailByHref', () => { scheduler.schedule(() => service.findEntryDetailByHref(entryDetailRequestURL)); scheduler.flush(); @@ -563,7 +565,7 @@ describe('VocabularyService', () => { describe('getEntryDetailParent', () => { it('should proxy the call to vocabularyDataService.getEntryDetailParent', () => { - scheduler.schedule(() => service.getEntryDetailParent('testValue', hierarchicalVocabulary.id)); + scheduler.schedule(() => service.getEntryDetailParent('testValue', hierarchicalVocabulary.id).subscribe()); scheduler.flush(); expect((service as any).vocabularyEntryDetailDataService.findByHref).toHaveBeenCalledWith(entryDetailParentRequestURL); @@ -580,10 +582,20 @@ describe('VocabularyService', () => { describe('getEntryDetailChildren', () => { it('should proxy the call to vocabularyDataService.getEntryDetailChildren', () => { - scheduler.schedule(() => service.getEntryDetailChildren('testValue', hierarchicalVocabulary.id, new PageInfo())); + const options: VocabularyFindOptions = new VocabularyFindOptions( + null, + null, + null, + null, + null, + null, + pageInfo.elementsPerPage, + pageInfo.currentPage + ); + scheduler.schedule(() => service.getEntryDetailChildren('testValue', hierarchicalVocabulary.id, pageInfo).subscribe()); scheduler.flush(); - expect((service as any).vocabularyEntryDetailDataService.findAllByHref).toHaveBeenCalledWith(entryDetailChildrenRequestURL); + expect((service as any).vocabularyEntryDetailDataService.findAllByHref).toHaveBeenCalledWith(entryDetailChildrenRequestURL, options); }); it('should return a RemoteData> for the object with the given URL', () => { @@ -595,9 +607,19 @@ describe('VocabularyService', () => { }); }); + describe('searchByTop', () => { it('should proxy the call to vocabularyEntryDetailDataService.searchBy', () => { - const options = new FindListOptions(); + const options: VocabularyFindOptions = new VocabularyFindOptions( + null, + null, + null, + null, + null, + null, + pageInfo.elementsPerPage, + pageInfo.currentPage + ); options.searchParams = [new RequestParam('vocabulary', 'srsc')]; scheduler.schedule(() => service.searchTopEntries('srsc', pageInfo)); scheduler.flush(); @@ -615,5 +637,14 @@ describe('VocabularyService', () => { }); + describe('clearSearchTopRequests', () => { + it('should remove requests on the data service\'s endpoint', (done) => { + service.clearSearchTopRequests(); + + expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(`search/${(service as any).searchTopMethod}`); + done(); + }); + }); + }); }); diff --git a/src/app/core/submission/vocabularies/vocabulary.service.ts b/src/app/core/submission/vocabularies/vocabulary.service.ts index a7cf1e1d9e..1dac3b56a6 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.ts @@ -353,10 +353,26 @@ export class VocabularyService { * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ searchTopEntries(name: string, pageInfo: PageInfo, ...linksToFollow: Array>): Observable>> { - const options = new FindListOptions(); + const options: VocabularyFindOptions = new VocabularyFindOptions( + null, + null, + null, + null, + null, + null, + pageInfo.elementsPerPage, + pageInfo.currentPage + ); options.searchParams = [new RequestParam('vocabulary', name)]; return this.vocabularyEntryDetailDataService.searchBy(this.searchTopMethod, options, ...linksToFollow) } + + /** + * Clear all search Top Requests + */ + clearSearchTopRequests(): void { + this.requestService.removeByHrefSubstring(`search/${this.searchTopMethod}`); + } } /** diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-vocabulary.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-vocabulary.component.ts index 92cf623447..14ae2128b5 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-vocabulary.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-vocabulary.component.ts @@ -59,12 +59,13 @@ export abstract class DsDynamicVocabularyComponent extends DynamicFormControlCom } initValue$ = initEntry$.pipe(map((initEntry: VocabularyEntry) => { if (isNotEmpty(initEntry)) { - return new FormFieldMetadataValueObject( - initEntry.value, - null, - initEntry.authority, - initEntry.display - ); + // Integrate FormFieldMetadataValueObject with retrieved information + return Object.assign(new FormFieldMetadataValueObject(), this.model.value, { + value: initEntry.value, + authority: initEntry.authority, + display: initEntry.display, + otherInformation: initEntry.otherInformation + }); } else { return this.model.value as any; } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.spec.ts index b827cd3536..72fc7fcba0 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.spec.ts @@ -20,6 +20,7 @@ import { createTestComponent } from '../../../../../testing/utils.test'; import { AuthorityConfidenceStateDirective } from '../../../../../authority-confidence/authority-confidence-state.directive'; import { ObjNgFor } from '../../../../../utils/object-ngfor.pipe'; import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../remote-data.utils'; export let TYPEAHEAD_TEST_GROUP; @@ -34,7 +35,7 @@ function init() { vocabularyOptions: { closed: false, metadata: 'typeahead', - name: 'EVENTAuthority', + name: 'vocabulary', scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' } as VocabularyOptions, disabled: false, @@ -50,13 +51,47 @@ function init() { }; } -describe('DsDynamicTypeaheadComponent test suite', () => { +fdescribe('DsDynamicTypeaheadComponent test suite', () => { let testComp: TestComponent; let typeaheadComp: DsDynamicTypeaheadComponent; let testFixture: ComponentFixture; let typeaheadFixture: ComponentFixture; + let service: any; let html; + let vocabulary = { + id: 'vocabulary', + name: 'vocabulary', + scrollable: true, + hierarchical: false, + preloadLevel: 0, + type: 'vocabulary', + _links: { + self: { + url: 'self' + }, + entries: { + url: 'entries' + } + } + } + + let hierarchicalVocabulary = { + id: 'hierarchicalVocabulary', + name: 'hierarchicalVocabulary', + scrollable: true, + hierarchical: true, + preloadLevel: 2, + type: 'vocabulary', + _links: { + self: { + url: 'self' + }, + entries: { + url: 'entries' + } + } + } // async beforeEach beforeEach(async(() => { @@ -113,7 +148,7 @@ describe('DsDynamicTypeaheadComponent test suite', () => { })); }); - describe('', () => { + describe('not hiearchical', () => { describe('when init model value is empty', () => { beforeEach(() => { @@ -121,6 +156,8 @@ describe('DsDynamicTypeaheadComponent test suite', () => { typeaheadComp = typeaheadFixture.componentInstance; // FormComponent test instance typeaheadComp.group = TYPEAHEAD_TEST_GROUP; typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG); + service = (typeaheadComp as any).vocabularyService; + spyOn(service, 'findVocabularyById').and.returnValue(createSuccessfulRemoteDataObject$(vocabulary)); typeaheadFixture.detectChanges(); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts index dd4da48849..93e5b228e7 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts @@ -119,7 +119,8 @@ export class DsDynamicTypeaheadComponent extends DsDynamicVocabularyComponent im } this.vocabulary$ = this.vocabularyService.findVocabularyById(this.model.vocabularyOptions.name).pipe( - getFirstSucceededRemoteDataPayload() + getFirstSucceededRemoteDataPayload(), + distinctUntilChanged() ); this.group.get(this.model.id).valueChanges.pipe( diff --git a/src/app/shared/form/builder/models/form-field-metadata-value.model.ts b/src/app/shared/form/builder/models/form-field-metadata-value.model.ts index ff0afe97fd..15dbdbe8d9 100644 --- a/src/app/shared/form/builder/models/form-field-metadata-value.model.ts +++ b/src/app/shared/form/builder/models/form-field-metadata-value.model.ts @@ -11,6 +11,7 @@ export interface OtherInformation { * A class representing a specific input-form field's value */ export class FormFieldMetadataValueObject implements MetadataValueInterface { + metadata?: string; value: any; display: string; language: any; @@ -43,6 +44,9 @@ export class FormFieldMetadataValueObject implements MetadataValueInterface { } this.place = place; + if (isNotEmpty(metadata)) { + this.metadata = metadata; + } this.otherInformation = otherInformation; } diff --git a/src/app/shared/form/builder/parsers/field-parser.ts b/src/app/shared/form/builder/parsers/field-parser.ts index 540c9c7b4e..c03b366158 100644 --- a/src/app/shared/form/builder/parsers/field-parser.ts +++ b/src/app/shared/form/builder/parsers/field-parser.ts @@ -182,8 +182,8 @@ export abstract class FieldParser { fieldIds.forEach((id) => { if (this.initFormValues.hasOwnProperty(id)) { const valueObj: FormFieldMetadataValueObject = Object.assign(new FormFieldMetadataValueObject(), this.initFormValues[id][innerIndex]); - // valueObj.metadata = id; - // valueObj.value = this.initFormValues[id][innerIndex]; + // Set metadata name, used for Qualdrop fields + valueObj.metadata = id; values.push(valueObj); } }); diff --git a/src/app/shared/testing/vocabulary-service.stub.ts b/src/app/shared/testing/vocabulary-service.stub.ts index 0639294dc4..425281581a 100644 --- a/src/app/shared/testing/vocabulary-service.stub.ts +++ b/src/app/shared/testing/vocabulary-service.stub.ts @@ -6,6 +6,7 @@ import { PaginatedList } from '../../core/data/paginated-list'; import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; import { RemoteData } from '../../core/data/remote-data'; import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model'; +import { Vocabulary } from '../../core/submission/vocabularies/models/vocabulary.model'; export class VocabularyServiceStub { @@ -37,4 +38,8 @@ export class VocabularyServiceStub { getVocabularyEntryByID(id: string, vocabularyOptions: VocabularyOptions): Observable { return observableOf(Object.assign(new VocabularyEntry(), { authority: 1, display: 'one', value: 1 })); } + + findVocabularyById(id: string): Observable> { + return; + } } diff --git a/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.html b/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.html index 9084013c4f..3f57e6a4c3 100644 --- a/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.html +++ b/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.html @@ -1,5 +1,5 @@
- -

- {{'treeview.search.no-result' | translate}} + +

+ {{'vocabulary-treeview.search.no-result' | translate}}

@@ -62,13 +62,13 @@ diff --git a/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.spec.ts b/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.spec.ts index 39f4274280..9af00b6be7 100644 --- a/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.spec.ts +++ b/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.spec.ts @@ -16,6 +16,7 @@ import { TreeviewFlatNode } from './vocabulary-treeview-node.model'; import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model'; import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model'; import { PageInfo } from '../../core/shared/page-info.model'; +import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model'; describe('VocabularyTreeviewComponent test suite', () => { @@ -37,7 +38,7 @@ describe('VocabularyTreeviewComponent test suite', () => { getData: jasmine.createSpy('getData'), loadMore: jasmine.createSpy('loadMore'), loadMoreRoot: jasmine.createSpy('loadMoreRoot'), - isSearching: jasmine.createSpy('isSearching'), + isLoading: jasmine.createSpy('isLoading'), searchByQuery: jasmine.createSpy('searchByQuery'), restoreNodes: jasmine.createSpy('restoreNodes'), cleanTree: jasmine.createSpy('cleanTree'), @@ -100,7 +101,7 @@ describe('VocabularyTreeviewComponent test suite', () => { comp = fixture.componentInstance; compAsAny = comp; vocabularyTreeviewServiceStub.getData.and.returnValue(observableOf([])); - vocabularyTreeviewServiceStub.isSearching.and.returnValue(observableOf(false)); + vocabularyTreeviewServiceStub.isLoading.and.returnValue(observableOf(false)); comp.vocabularyOptions = vocabularyOptions; comp.selectedItem = null; }); @@ -118,19 +119,27 @@ describe('VocabularyTreeviewComponent test suite', () => { }); it('should should init component properly with init value as FormFieldMetadataValueObject', () => { - comp.selectedItem = new FormFieldMetadataValueObject('test', null, 'auth001'); + const currentValue = new FormFieldMetadataValueObject(); + currentValue.value = 'testValue'; + currentValue.otherInformation = { + id: 'entryID' + }; + comp.selectedItem = currentValue; fixture.detectChanges(); expect(comp.dataSource.data).toEqual([]); - expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), 'auth001'); + expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), 'entryID'); }); - it('should should init component properly with init value as AuthorityEntry', () => { - const authority = new VocabularyEntryDetail(); - authority.id = 'auth001'; - comp.selectedItem = authority; + it('should should init component properly with init value as VocabularyEntry', () => { + const currentValue = new VocabularyEntry(); + currentValue.value = 'testValue'; + currentValue.otherInformation = { + id: 'entryID' + }; + comp.selectedItem = currentValue; fixture.detectChanges(); expect(comp.dataSource.data).toEqual([]); - expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), 'auth001'); + expect(vocabularyTreeviewServiceStub.initialize).toHaveBeenCalledWith(comp.vocabularyOptions, new PageInfo(), 'entryID'); }); it('should call loadMore function', () => { diff --git a/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts b/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts index 34c9a52c60..8c7543849d 100644 --- a/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts +++ b/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts @@ -16,6 +16,7 @@ import { VocabularyTreeviewService } from './vocabulary-treeview.service'; import { LOAD_MORE, LOAD_MORE_ROOT, TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model'; import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model'; import { PageInfo } from '../../core/shared/page-info.model'; +import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model'; /** * Component that show a hierarchical vocabulary in a tree view @@ -78,9 +79,9 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { searchText: string; /** - * A boolean representing if a search operation is pending + * A boolean representing if tree is loading */ - searching: Observable; + loading: Observable; /** * An event fired when a vocabulary entry is selected. @@ -198,7 +199,7 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { }) ); - const descriptionLabel = 'tree.description.' + this.vocabularyOptions.name; + const descriptionLabel = 'vocabulary-treeview.tree.description.' + this.vocabularyOptions.name; this.description = this.translate.get(descriptionLabel).pipe( filter((msg) => msg !== descriptionLabel), startWith('') @@ -207,13 +208,13 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { // set isAuthenticated this.isAuthenticated = this.store.pipe(select(isAuthenticated)); - this.searching = this.vocabularyTreeviewService.isSearching(); + this.loading = this.vocabularyTreeviewService.isLoading(); this.isAuthenticated.pipe( find((isAuth) => isAuth) ).subscribe(() => { - const valueId: string = (this.selectedItem) ? (this.selectedItem.authority || this.selectedItem.id) : null; - this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), valueId); + const entryId: string = (this.selectedItem) ? this.getEntryId(this.selectedItem) : null; + this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), entryId); }); } @@ -292,4 +293,11 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { .filter((sub) => hasValue(sub)) .forEach((sub) => sub.unsubscribe()); } + + /** + * Return an id for a given {@link VocabularyEntry} + */ + private getEntryId(entry: VocabularyEntry): string { + return entry.authority || entry.otherInformation.id || undefined; + } } diff --git a/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.spec.ts b/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.spec.ts index 32ef635792..2f03549898 100644 --- a/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.spec.ts +++ b/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.spec.ts @@ -50,39 +50,47 @@ describe('VocabularyTreeviewService test suite', () => { let nodeMapWithChildren: Map; let searchNodeMap: Map; let vocabularyOptions; + let pageInfo: PageInfo; const vocabularyServiceStub = jasmine.createSpyObj('VocabularyService', { getVocabularyEntriesByValue: jasmine.createSpy('getVocabularyEntriesByValue'), getEntryDetailParent: jasmine.createSpy('getEntryDetailParent'), findEntryDetailByValue: jasmine.createSpy('findEntryDetailByValue'), searchTopEntries: jasmine.createSpy('searchTopEntries'), - getEntryDetailChildren: jasmine.createSpy('getEntryDetailChildren') + getEntryDetailChildren: jasmine.createSpy('getEntryDetailChildren'), + clearSearchTopRequests: jasmine.createSpy('clearSearchTopRequests') }); function init() { + pageInfo = Object.assign(new PageInfo(), { + elementsPerPage: 1, + totalElements: 3, + totalPages: 1, + currentPage: 1 + }); loadMoreNode = new TreeviewNode(LOAD_MORE_NODE, false, new PageInfo(), item); loadMoreRootNode = new TreeviewNode(LOAD_MORE_ROOT_NODE, false, new PageInfo(), null); loadMoreRootFlatNode = new TreeviewFlatNode(LOAD_MORE_ROOT_NODE, 1, false, new PageInfo(), null); item = new VocabularyEntryDetail(); item.id = item.value = item.display = 'root1'; - item.otherInformation = { children: 'root1-child1::root1-child2', id: 'root1' }; - itemNode = new TreeviewNode(item, true); + item.otherInformation = { hasChildren: 'true', id: 'root1' }; + itemNode = new TreeviewNode(item, true, pageInfo); searchItemNode = new TreeviewNode(item, true, new PageInfo(), null, true); item2 = new VocabularyEntryDetail(); item2.id = item2.value = item2.display = 'root2'; item2.otherInformation = { id: 'root2' }; - itemNode2 = new TreeviewNode(item2); + itemNode2 = new TreeviewNode(item2, false, pageInfo); item3 = new VocabularyEntryDetail(); item3.id = item3.value = item3.display = 'root3'; item3.otherInformation = { id: 'root3' }; - itemNode3 = new TreeviewNode(item3); + itemNode3 = new TreeviewNode(item3, false, pageInfo); child = new VocabularyEntryDetail(); child.id = child.value = child.display = 'root1-child1'; - child.otherInformation = { parent: 'root1', children: 'root1-child1-child1', id: 'root1-child1' }; + child.otherInformation = { parent: 'root1', hasChildren: 'true', id: 'root1-child1' }; childNode = new TreeviewNode(child); searchChildNode = new TreeviewNode(child, true, new PageInfo(), item, true); @@ -167,12 +175,6 @@ describe('VocabularyTreeviewService test suite', () => { describe('initialize', () => { it('should set vocabularyName and call retrieveTopNodes method', () => { - const pageInfo = Object.assign(new PageInfo(), { - elementsPerPage: 1, - totalElements: 3, - totalPages: 1, - currentPage: 1 - }); serviceAsAny.vocabularyService.searchTopEntries.and.returnValue(hot('-a', { a: createSuccessfulRemoteDataObject(new PaginatedList(pageInfo, [item, item2, item3])) })); @@ -181,16 +183,12 @@ describe('VocabularyTreeviewService test suite', () => { scheduler.flush(); expect(serviceAsAny.vocabularyName).toEqual(vocabularyOptions.name); + expect(serviceAsAny.pageInfo).toEqual(pageInfo); + console.log(serviceAsAny.dataChange.value[0].pageInfo, itemNode.pageInfo); expect(serviceAsAny.dataChange.value).toEqual([itemNode, itemNode2, itemNode3]); }); it('should set initValueHierarchy', () => { - const pageInfo = Object.assign(new PageInfo(), { - elementsPerPage: 1, - totalElements: 3, - totalPages: 1, - currentPage: 1 - }); serviceAsAny.vocabularyService.searchTopEntries.and.returnValue(hot('-c', { a: createSuccessfulRemoteDataObject(new PaginatedList(pageInfo, [item, item2, item3])) })); @@ -239,7 +237,7 @@ describe('VocabularyTreeviewService test suite', () => { }); it('should add children nodes properly', () => { - const pageInfo = Object.assign(new PageInfo(), { + pageInfo = Object.assign(new PageInfo(), { elementsPerPage: 1, totalElements: 2, totalPages: 2, @@ -260,7 +258,7 @@ describe('VocabularyTreeviewService test suite', () => { }); it('should add loadMore node properly', () => { - const pageInfo = Object.assign(new PageInfo(), { + pageInfo = Object.assign(new PageInfo(), { elementsPerPage: 1, totalElements: 2, totalPages: 2, @@ -285,7 +283,7 @@ describe('VocabularyTreeviewService test suite', () => { describe('searchByQuery', () => { it('should set tree data properly after a search', () => { - const pageInfo = Object.assign(new PageInfo(), { + pageInfo = Object.assign(new PageInfo(), { elementsPerPage: 1, totalElements: 1, totalPages: 1, diff --git a/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts b/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts index bfaddf414e..8beead5419 100644 --- a/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts +++ b/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; -import { flatMap, map, merge, scan, take, tap } from 'rxjs/operators'; +import { flatMap, map, merge, scan } from 'rxjs/operators'; import { findIndex } from 'lodash'; import { LOAD_MORE_NODE, LOAD_MORE_ROOT_NODE, TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model'; @@ -56,14 +56,18 @@ export class VocabularyTreeviewService { private initValueHierarchy: string[] = []; /** - * A boolean representing if a search operation is pending + * A boolean representing if any operation is pending */ - private searching = new BehaviorSubject(false); + private loading = new BehaviorSubject(false); /** - * An observable to change the searching status + * The {@link PageInfo} object */ - private hideSearchingWhenUnsubscribed$ = new Observable(() => () => this.searching.next(false)); + private pageInfo: PageInfo; + /** + * An observable to change the loading status + */ + private hideSearchingWhenUnsubscribed$ = new Observable(() => () => this.loading.next(false)); /** * Initialize instance variables @@ -92,8 +96,10 @@ export class VocabularyTreeviewService { * @param initValueId The entry id of the node to mark as selected, if any */ initialize(options: VocabularyOptions, pageInfo: PageInfo, initValueId?: string): void { + this.loading.next(true); this.vocabularyOptions = options; this.vocabularyName = options.name; + this.pageInfo = pageInfo; if (isNotEmpty(initValueId)) { this.getNodeHierarchyById(initValueId) .subscribe((hierarchy: string[]) => { @@ -160,17 +166,17 @@ export class VocabularyTreeviewService { } /** - * Check if a search operation is pending + * Check if any operation is pending */ - isSearching(): Observable { - return this.searching; + isLoading(): Observable { + return this.loading; } /** * Perform a search operation by query */ searchByQuery(query: string) { - this.searching.next(true); + this.loading.next(true); if (isEmpty(this.storedNodes)) { this.storedNodes = this.dataChange.value; this.storedNodeMap = this.nodeMap; @@ -192,7 +198,7 @@ export class VocabularyTreeviewService { merge(this.hideSearchingWhenUnsubscribed$) ).subscribe((nodes: TreeviewNode[]) => { this.dataChange.next(nodes); - this.searching.next(false); + this.loading.next(false); }) } @@ -200,7 +206,7 @@ export class VocabularyTreeviewService { * Reset tree state with the one before the search */ restoreNodes() { - this.searching.next(false); + this.loading.next(false); this.dataChange.next(this.storedNodes); this.nodeMap = this.storedNodeMap; @@ -224,8 +230,8 @@ export class VocabularyTreeviewService { const entryDetail: VocabularyEntryDetail = Object.assign(new VocabularyEntryDetail(), entry, { id: entryId }); - const hasChildren = entry.hasOtherInformation() && isNotEmpty((entry.otherInformation as any).children); - const pageInfo: PageInfo = new PageInfo(); + const hasChildren = entry.hasOtherInformation() && (entry.otherInformation as any)!.hasChildren == 'true'; + const pageInfo: PageInfo = this.pageInfo; const isInInitValueHierarchy = this.initValueHierarchy.includes(entryId); const result = new TreeviewNode( entryDetail, @@ -295,8 +301,10 @@ export class VocabularyTreeviewService { this.vocabularyService.searchTopEntries(this.vocabularyName, pageInfo).pipe( getFirstSucceededRemoteDataPayload() ).subscribe((list: PaginatedList) => { - const newNodes: TreeviewNode[] = list.page.map((entry: VocabularyEntryDetail) => this._generateNode(entry)) + this.vocabularyService.clearSearchTopRequests(); + const newNodes: TreeviewNode[] = list.page.map((entry: VocabularyEntryDetail) => this._generateNode(entry)); nodes.push(...newNodes); + if ((list.pageInfo.currentPage + 1) <= list.pageInfo.totalPages) { // Need a new load more node const newPageInfo: PageInfo = Object.assign(new PageInfo(), list.pageInfo, { @@ -306,6 +314,7 @@ export class VocabularyTreeviewService { loadMoreNode.updatePageInfo(newPageInfo); nodes.push(loadMoreNode); } + this.loading.next(false); // Notify the change. this.dataChange.next(nodes); }); @@ -328,8 +337,7 @@ export class VocabularyTreeviewService { if (isNotEmpty(children)) { const newChildren = children .filter((entry: TreeviewNode) => { - const ii = findIndex(node.children, (nodeEntry) => nodeEntry.item.id === entry.item.id); - return ii === -1; + return findIndex(node.children, (nodeEntry) => nodeEntry.item.id === entry.item.id) === -1; }); newChildren.forEach((entry: TreeviewNode) => { entry.loadMoreParentItem = node.item diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 3a79984ec3..26c4829e4c 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2712,6 +2712,24 @@ "title": "DSpace", + + + "vocabulary-treeview.header": "Hierarchical tree view", + + "vocabulary-treeview.load-more": "Load more", + + "vocabulary-treeview.search.form.reset": "Reset", + + "vocabulary-treeview.search.form.search": "Search", + + "vocabulary-treeview.search.no-result": "There were no items to show", + + "vocabulary-treeview.tree.description.nsi": "The Norwegian Science Index", + + "vocabulary-treeview.tree.description.srsc": "Research Subject Categories", + + + "administrativeView.search.results.head": "Administrative Search", "menu.section.admin_search": "Admin Search",