[CST-3088] fixed code

This commit is contained in:
Giuseppe Digilio
2020-07-01 22:57:26 +02:00
parent 7e80bcf9e9
commit 44381d7653
17 changed files with 223 additions and 92 deletions

View File

@@ -251,7 +251,6 @@ export class AuthInterceptor implements HttpInterceptor {
// Pass on the new request instead of the original request. // Pass on the new request instead of the original request.
return next.handle(newReq).pipe( return next.handle(newReq).pipe(
// tap((response) => console.log('next.handle: ', response)),
map((response) => { map((response) => {
// Intercept a Login/Logout response // Intercept a Login/Logout response
if (response instanceof HttpResponse && this.isSuccess(response) && this.isAuthRequest(response)) { if (response instanceof HttpResponse && this.isSuccess(response) && this.isAuthRequest(response)) {

View File

@@ -144,6 +144,7 @@ import { Vocabulary } from './submission/vocabularies/models/vocabulary.model';
import { VocabularyEntriesResponseParsingService } from './submission/vocabularies/vocabulary-entries-response-parsing.service'; import { VocabularyEntriesResponseParsingService } from './submission/vocabularies/vocabulary-entries-response-parsing.service';
import { VocabularyEntryDetail } from './submission/vocabularies/models/vocabulary-entry-detail.model'; import { VocabularyEntryDetail } from './submission/vocabularies/models/vocabulary-entry-detail.model';
import { VocabularyService } from './submission/vocabularies/vocabulary.service'; 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 * When not in production, endpoint responses can be mocked for testing purposes
@@ -268,7 +269,8 @@ const PROVIDERS = [
FilteredDiscoveryPageResponseParsingService, FilteredDiscoveryPageResponseParsingService,
{ provide: NativeWindowService, useFactory: NativeWindowFactory }, { provide: NativeWindowService, useFactory: NativeWindowFactory },
VocabularyService, VocabularyService,
VocabularyEntriesResponseParsingService VocabularyEntriesResponseParsingService,
VocabularyTreeviewService
]; ];
/** /**

View File

@@ -115,12 +115,6 @@ export abstract class DataService<T extends CacheableObject> {
result$ = this.getSearchEndpoint(searchMethod); 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))); return result$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args, ...linksToFollow)));
} }
@@ -351,7 +345,7 @@ export abstract class DataService<T extends CacheableObject> {
return hrefObs.pipe( return hrefObs.pipe(
find((href: string) => hasValue(href)), find((href: string) => hasValue(href)),
tap((href: string) => { tap((href: string) => {
this.requestService.removeByHrefSubstring(href); this.requestService.removeByHrefSubstring(searchMethod);
const request = new FindListRequest(this.requestService.generateRequestId(), href, options); const request = new FindListRequest(this.requestService.generateRequestId(), href, options);
if (hasValue(this.responseMsToLive)) { if (hasValue(this.responseMsToLive)) {
request.responseMsToLive = this.responseMsToLive; request.responseMsToLive = this.responseMsToLive;

View File

@@ -20,6 +20,7 @@ import { VocabularyService } from './vocabulary.service';
import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock';
import { VocabularyOptions } from './models/vocabulary-options.model'; import { VocabularyOptions } from './models/vocabulary-options.model';
import { VocabularyFindOptions } from './models/vocabulary-find-options.model';
describe('VocabularyService', () => { describe('VocabularyService', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;
@@ -208,9 +209,9 @@ describe('VocabularyService', () => {
const endpointURL = `https://rest.api/rest/api/submission/vocabularies`; const endpointURL = `https://rest.api/rest/api/submission/vocabularies`;
const requestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}`; const requestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}`;
const entryDetailEndpointURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails`; const entryDetailEndpointURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails`;
const entryDetailRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:SCB110/${hierarchicalVocabulary.id}:testValue`; const entryDetailRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/${hierarchicalVocabulary.id}:testValue`;
const entryDetailParentRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:SCB110/${hierarchicalVocabulary.id}:testValue/parent`; const entryDetailParentRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/${hierarchicalVocabulary.id}:testValue/parent`;
const entryDetailChildrenRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:SCB110/${hierarchicalVocabulary.id}:testValue/children`; const entryDetailChildrenRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/${hierarchicalVocabulary.id}:testValue/children`;
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
const vocabularyId = 'types'; const vocabularyId = 'types';
const metadata = 'dc.type'; const metadata = 'dc.type';
@@ -490,7 +491,7 @@ describe('VocabularyService', () => {
scheduler = getTestScheduler(); scheduler = getTestScheduler();
halService = jasmine.createSpyObj('halService', { halService = jasmine.createSpyObj('halService', {
getEndpoint: cold('a', { a: endpointURL }) getEndpoint: cold('a', { a: entryDetailEndpointURL })
}); });
responseCacheEntry = new RequestEntry(); responseCacheEntry = new RequestEntry();
@@ -517,9 +518,10 @@ describe('VocabularyService', () => {
spyOn((service as any).vocabularyEntryDetailDataService, 'findById').and.callThrough(); spyOn((service as any).vocabularyEntryDetailDataService, 'findById').and.callThrough();
spyOn((service as any).vocabularyEntryDetailDataService, 'findAll').and.callThrough(); spyOn((service as any).vocabularyEntryDetailDataService, 'findAll').and.callThrough();
spyOn((service as any).vocabularyEntryDetailDataService, 'findByHref').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, 'searchBy').and.callThrough();
spyOn((service as any).vocabularyEntryDetailDataService, 'getSearchByHref').and.returnValue(observableOf(searchRequestURL)); 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)); spyOn((service as any).vocabularyEntryDetailDataService, 'getBrowseEndpoint').and.returnValue(observableOf(entryDetailEndpointURL));
}); });
@@ -528,7 +530,7 @@ describe('VocabularyService', () => {
}); });
describe('findEntryDetailByHref', () => { describe('findEntryDetailByHref', () => {
it('should proxy the call to vocabularyDataService.findVocabularyByHref', () => { it('should proxy the call to vocabularyDataService.findEntryDetailByHref', () => {
scheduler.schedule(() => service.findEntryDetailByHref(entryDetailRequestURL)); scheduler.schedule(() => service.findEntryDetailByHref(entryDetailRequestURL));
scheduler.flush(); scheduler.flush();
@@ -563,7 +565,7 @@ describe('VocabularyService', () => {
describe('getEntryDetailParent', () => { describe('getEntryDetailParent', () => {
it('should proxy the call to vocabularyDataService.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(); scheduler.flush();
expect((service as any).vocabularyEntryDetailDataService.findByHref).toHaveBeenCalledWith(entryDetailParentRequestURL); expect((service as any).vocabularyEntryDetailDataService.findByHref).toHaveBeenCalledWith(entryDetailParentRequestURL);
@@ -580,10 +582,20 @@ describe('VocabularyService', () => {
describe('getEntryDetailChildren', () => { describe('getEntryDetailChildren', () => {
it('should proxy the call to vocabularyDataService.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(); scheduler.flush();
expect((service as any).vocabularyEntryDetailDataService.findAllByHref).toHaveBeenCalledWith(entryDetailChildrenRequestURL); expect((service as any).vocabularyEntryDetailDataService.findAllByHref).toHaveBeenCalledWith(entryDetailChildrenRequestURL, options);
}); });
it('should return a RemoteData<PaginatedList<ResourcePolicy>> for the object with the given URL', () => { it('should return a RemoteData<PaginatedList<ResourcePolicy>> for the object with the given URL', () => {
@@ -595,9 +607,19 @@ describe('VocabularyService', () => {
}); });
}); });
describe('searchByTop', () => { describe('searchByTop', () => {
it('should proxy the call to vocabularyEntryDetailDataService.searchBy', () => { 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')]; options.searchParams = [new RequestParam('vocabulary', 'srsc')];
scheduler.schedule(() => service.searchTopEntries('srsc', pageInfo)); scheduler.schedule(() => service.searchTopEntries('srsc', pageInfo));
scheduler.flush(); 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();
});
});
}); });
}); });

View File

@@ -353,10 +353,26 @@ export class VocabularyService {
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
*/ */
searchTopEntries(name: string, pageInfo: PageInfo, ...linksToFollow: Array<FollowLinkConfig<VocabularyEntryDetail>>): Observable<RemoteData<PaginatedList<VocabularyEntryDetail>>> { searchTopEntries(name: string, pageInfo: PageInfo, ...linksToFollow: Array<FollowLinkConfig<VocabularyEntryDetail>>): Observable<RemoteData<PaginatedList<VocabularyEntryDetail>>> {
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)]; options.searchParams = [new RequestParam('vocabulary', name)];
return this.vocabularyEntryDetailDataService.searchBy(this.searchTopMethod, options, ...linksToFollow) return this.vocabularyEntryDetailDataService.searchBy(this.searchTopMethod, options, ...linksToFollow)
} }
/**
* Clear all search Top Requests
*/
clearSearchTopRequests(): void {
this.requestService.removeByHrefSubstring(`search/${this.searchTopMethod}`);
}
} }
/** /**

View File

@@ -59,12 +59,13 @@ export abstract class DsDynamicVocabularyComponent extends DynamicFormControlCom
} }
initValue$ = initEntry$.pipe(map((initEntry: VocabularyEntry) => { initValue$ = initEntry$.pipe(map((initEntry: VocabularyEntry) => {
if (isNotEmpty(initEntry)) { if (isNotEmpty(initEntry)) {
return new FormFieldMetadataValueObject( // Integrate FormFieldMetadataValueObject with retrieved information
initEntry.value, return Object.assign(new FormFieldMetadataValueObject(), this.model.value, {
null, value: initEntry.value,
initEntry.authority, authority: initEntry.authority,
initEntry.display display: initEntry.display,
); otherInformation: initEntry.otherInformation
});
} else { } else {
return this.model.value as any; return this.model.value as any;
} }

View File

@@ -20,6 +20,7 @@ import { createTestComponent } from '../../../../../testing/utils.test';
import { AuthorityConfidenceStateDirective } from '../../../../../authority-confidence/authority-confidence-state.directive'; import { AuthorityConfidenceStateDirective } from '../../../../../authority-confidence/authority-confidence-state.directive';
import { ObjNgFor } from '../../../../../utils/object-ngfor.pipe'; import { ObjNgFor } from '../../../../../utils/object-ngfor.pipe';
import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model';
import { createSuccessfulRemoteDataObject$ } from '../../../../../remote-data.utils';
export let TYPEAHEAD_TEST_GROUP; export let TYPEAHEAD_TEST_GROUP;
@@ -34,7 +35,7 @@ function init() {
vocabularyOptions: { vocabularyOptions: {
closed: false, closed: false,
metadata: 'typeahead', metadata: 'typeahead',
name: 'EVENTAuthority', name: 'vocabulary',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as VocabularyOptions, } as VocabularyOptions,
disabled: false, disabled: false,
@@ -50,13 +51,47 @@ function init() {
}; };
} }
describe('DsDynamicTypeaheadComponent test suite', () => { fdescribe('DsDynamicTypeaheadComponent test suite', () => {
let testComp: TestComponent; let testComp: TestComponent;
let typeaheadComp: DsDynamicTypeaheadComponent; let typeaheadComp: DsDynamicTypeaheadComponent;
let testFixture: ComponentFixture<TestComponent>; let testFixture: ComponentFixture<TestComponent>;
let typeaheadFixture: ComponentFixture<DsDynamicTypeaheadComponent>; let typeaheadFixture: ComponentFixture<DsDynamicTypeaheadComponent>;
let service: any;
let html; 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 // async beforeEach
beforeEach(async(() => { beforeEach(async(() => {
@@ -113,7 +148,7 @@ describe('DsDynamicTypeaheadComponent test suite', () => {
})); }));
}); });
describe('', () => { describe('not hiearchical', () => {
describe('when init model value is empty', () => { describe('when init model value is empty', () => {
beforeEach(() => { beforeEach(() => {
@@ -121,6 +156,8 @@ describe('DsDynamicTypeaheadComponent test suite', () => {
typeaheadComp = typeaheadFixture.componentInstance; // FormComponent test instance typeaheadComp = typeaheadFixture.componentInstance; // FormComponent test instance
typeaheadComp.group = TYPEAHEAD_TEST_GROUP; typeaheadComp.group = TYPEAHEAD_TEST_GROUP;
typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG); typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG);
service = (typeaheadComp as any).vocabularyService;
spyOn(service, 'findVocabularyById').and.returnValue(createSuccessfulRemoteDataObject$(vocabulary));
typeaheadFixture.detectChanges(); typeaheadFixture.detectChanges();
}); });

View File

@@ -119,7 +119,8 @@ export class DsDynamicTypeaheadComponent extends DsDynamicVocabularyComponent im
} }
this.vocabulary$ = this.vocabularyService.findVocabularyById(this.model.vocabularyOptions.name).pipe( this.vocabulary$ = this.vocabularyService.findVocabularyById(this.model.vocabularyOptions.name).pipe(
getFirstSucceededRemoteDataPayload() getFirstSucceededRemoteDataPayload(),
distinctUntilChanged()
); );
this.group.get(this.model.id).valueChanges.pipe( this.group.get(this.model.id).valueChanges.pipe(

View File

@@ -11,6 +11,7 @@ export interface OtherInformation {
* A class representing a specific input-form field's value * A class representing a specific input-form field's value
*/ */
export class FormFieldMetadataValueObject implements MetadataValueInterface { export class FormFieldMetadataValueObject implements MetadataValueInterface {
metadata?: string;
value: any; value: any;
display: string; display: string;
language: any; language: any;
@@ -43,6 +44,9 @@ export class FormFieldMetadataValueObject implements MetadataValueInterface {
} }
this.place = place; this.place = place;
if (isNotEmpty(metadata)) {
this.metadata = metadata;
}
this.otherInformation = otherInformation; this.otherInformation = otherInformation;
} }

View File

@@ -182,8 +182,8 @@ export abstract class FieldParser {
fieldIds.forEach((id) => { fieldIds.forEach((id) => {
if (this.initFormValues.hasOwnProperty(id)) { if (this.initFormValues.hasOwnProperty(id)) {
const valueObj: FormFieldMetadataValueObject = Object.assign(new FormFieldMetadataValueObject(), this.initFormValues[id][innerIndex]); const valueObj: FormFieldMetadataValueObject = Object.assign(new FormFieldMetadataValueObject(), this.initFormValues[id][innerIndex]);
// valueObj.metadata = id; // Set metadata name, used for Qualdrop fields
// valueObj.value = this.initFormValues[id][innerIndex]; valueObj.metadata = id;
values.push(valueObj); values.push(valueObj);
} }
}); });

View File

@@ -6,6 +6,7 @@ import { PaginatedList } from '../../core/data/paginated-list';
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model'; import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model';
import { Vocabulary } from '../../core/submission/vocabularies/models/vocabulary.model';
export class VocabularyServiceStub { export class VocabularyServiceStub {
@@ -37,4 +38,8 @@ export class VocabularyServiceStub {
getVocabularyEntryByID(id: string, vocabularyOptions: VocabularyOptions): Observable<VocabularyEntry> { getVocabularyEntryByID(id: string, vocabularyOptions: VocabularyOptions): Observable<VocabularyEntry> {
return observableOf(Object.assign(new VocabularyEntry(), { authority: 1, display: 'one', value: 1 })); return observableOf(Object.assign(new VocabularyEntry(), { authority: 1, display: 'one', value: 1 }));
} }
findVocabularyById(id: string): Observable<RemoteData<Vocabulary>> {
return;
}
} }

View File

@@ -1,5 +1,5 @@
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title">{{'treeview.header' | translate}}</h4> <h4 class="modal-title">{{'vocabulary-treeview.header' | translate}}</h4>
<button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss('Cross click')"> <button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss('Cross click')">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
@@ -13,19 +13,19 @@
<input type="text" class="form-control" [(ngModel)]="searchText" (keyup.enter)="search()"> <input type="text" class="form-control" [(ngModel)]="searchText" (keyup.enter)="search()">
<div class="input-group-append" id="button-addon4"> <div class="input-group-append" id="button-addon4">
<button class="btn btn-outline-primary" type="button" (click)="search()" [disabled]="!isSearchEnabled()"> <button class="btn btn-outline-primary" type="button" (click)="search()" [disabled]="!isSearchEnabled()">
{{'treeview.search.form.search' | translate}} {{'vocabulary-treeview.search.form.search' | translate}}
</button> </button>
<button class="btn btn-outline-secondary" type="button" (click)="reset()"> <button class="btn btn-outline-secondary" type="button" (click)="reset()">
{{'treeview.search.form.reset' | translate}} {{'vocabulary-treeview.search.form.reset' | translate}}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="treeview-container"> <div class="treeview-container">
<ds-loading *ngIf="searching | async" [showMessage]="false"></ds-loading> <ds-loading *ngIf="loading | async" [showMessage]="false"></ds-loading>
<h4 *ngIf="!(searching | async) && dataSource.data.length === 0" class="text-center text-muted mt-4" > <h4 *ngIf="!(loading | async) && dataSource.data.length === 0" class="text-center text-muted mt-4" >
<span>{{'treeview.search.no-result' | translate}}</span> <span>{{'vocabulary-treeview.search.no-result' | translate}}</span>
</h4> </h4>
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl"> <cdk-tree [dataSource]="dataSource" [treeControl]="treeControl">
<!-- Leaf node --> <!-- Leaf node -->
@@ -62,13 +62,13 @@
<cdk-tree-node *cdkTreeNodeDef="let node; when: isLoadMore" cdkTreeNodePadding> <cdk-tree-node *cdkTreeNodeDef="let node; when: isLoadMore" cdkTreeNodePadding>
<button class="btn btn-outline-secondary btn-sm" (click)="loadMore(node.loadMoreParentItem)"> <button class="btn btn-outline-secondary btn-sm" (click)="loadMore(node.loadMoreParentItem)">
{{'treeview.load-more' | translate}}... {{'vocabulary-treeview.load-more' | translate}}...
</button> </button>
</cdk-tree-node> </cdk-tree-node>
<cdk-tree-node *cdkTreeNodeDef="let node; when: isLoadMoreRoot"> <cdk-tree-node *cdkTreeNodeDef="let node; when: isLoadMoreRoot">
<button class="btn btn-outline-secondary btn-sm" (click)="loadMoreRoot(node)"> <button class="btn btn-outline-secondary btn-sm" (click)="loadMoreRoot(node)">
{{'treeview.load-more' | translate}}... {{'vocabulary-treeview.load-more' | translate}}...
</button> </button>
</cdk-tree-node> </cdk-tree-node>
</cdk-tree> </cdk-tree>

View File

@@ -16,6 +16,7 @@ import { TreeviewFlatNode } from './vocabulary-treeview-node.model';
import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model'; import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model';
import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model'; import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model';
describe('VocabularyTreeviewComponent test suite', () => { describe('VocabularyTreeviewComponent test suite', () => {
@@ -37,7 +38,7 @@ describe('VocabularyTreeviewComponent test suite', () => {
getData: jasmine.createSpy('getData'), getData: jasmine.createSpy('getData'),
loadMore: jasmine.createSpy('loadMore'), loadMore: jasmine.createSpy('loadMore'),
loadMoreRoot: jasmine.createSpy('loadMoreRoot'), loadMoreRoot: jasmine.createSpy('loadMoreRoot'),
isSearching: jasmine.createSpy('isSearching'), isLoading: jasmine.createSpy('isLoading'),
searchByQuery: jasmine.createSpy('searchByQuery'), searchByQuery: jasmine.createSpy('searchByQuery'),
restoreNodes: jasmine.createSpy('restoreNodes'), restoreNodes: jasmine.createSpy('restoreNodes'),
cleanTree: jasmine.createSpy('cleanTree'), cleanTree: jasmine.createSpy('cleanTree'),
@@ -100,7 +101,7 @@ describe('VocabularyTreeviewComponent test suite', () => {
comp = fixture.componentInstance; comp = fixture.componentInstance;
compAsAny = comp; compAsAny = comp;
vocabularyTreeviewServiceStub.getData.and.returnValue(observableOf([])); vocabularyTreeviewServiceStub.getData.and.returnValue(observableOf([]));
vocabularyTreeviewServiceStub.isSearching.and.returnValue(observableOf(false)); vocabularyTreeviewServiceStub.isLoading.and.returnValue(observableOf(false));
comp.vocabularyOptions = vocabularyOptions; comp.vocabularyOptions = vocabularyOptions;
comp.selectedItem = null; comp.selectedItem = null;
}); });
@@ -118,19 +119,27 @@ describe('VocabularyTreeviewComponent test suite', () => {
}); });
it('should should init component properly with init value as FormFieldMetadataValueObject', () => { 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(); fixture.detectChanges();
expect(comp.dataSource.data).toEqual([]); 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', () => { it('should should init component properly with init value as VocabularyEntry', () => {
const authority = new VocabularyEntryDetail(); const currentValue = new VocabularyEntry();
authority.id = 'auth001'; currentValue.value = 'testValue';
comp.selectedItem = authority; currentValue.otherInformation = {
id: 'entryID'
};
comp.selectedItem = currentValue;
fixture.detectChanges(); fixture.detectChanges();
expect(comp.dataSource.data).toEqual([]); 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', () => { it('should call loadMore function', () => {

View File

@@ -16,6 +16,7 @@ import { VocabularyTreeviewService } from './vocabulary-treeview.service';
import { LOAD_MORE, LOAD_MORE_ROOT, TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model'; import { LOAD_MORE, LOAD_MORE_ROOT, TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model';
import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model'; import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model';
import { PageInfo } from '../../core/shared/page-info.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 * Component that show a hierarchical vocabulary in a tree view
@@ -78,9 +79,9 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
searchText: string; searchText: string;
/** /**
* A boolean representing if a search operation is pending * A boolean representing if tree is loading
*/ */
searching: Observable<boolean>; loading: Observable<boolean>;
/** /**
* An event fired when a vocabulary entry is selected. * 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( this.description = this.translate.get(descriptionLabel).pipe(
filter((msg) => msg !== descriptionLabel), filter((msg) => msg !== descriptionLabel),
startWith('') startWith('')
@@ -207,13 +208,13 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
// set isAuthenticated // set isAuthenticated
this.isAuthenticated = this.store.pipe(select(isAuthenticated)); this.isAuthenticated = this.store.pipe(select(isAuthenticated));
this.searching = this.vocabularyTreeviewService.isSearching(); this.loading = this.vocabularyTreeviewService.isLoading();
this.isAuthenticated.pipe( this.isAuthenticated.pipe(
find((isAuth) => isAuth) find((isAuth) => isAuth)
).subscribe(() => { ).subscribe(() => {
const valueId: string = (this.selectedItem) ? (this.selectedItem.authority || this.selectedItem.id) : null; const entryId: string = (this.selectedItem) ? this.getEntryId(this.selectedItem) : null;
this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), valueId); this.vocabularyTreeviewService.initialize(this.vocabularyOptions, new PageInfo(), entryId);
}); });
} }
@@ -292,4 +293,11 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit {
.filter((sub) => hasValue(sub)) .filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe()); .forEach((sub) => sub.unsubscribe());
} }
/**
* Return an id for a given {@link VocabularyEntry}
*/
private getEntryId(entry: VocabularyEntry): string {
return entry.authority || entry.otherInformation.id || undefined;
}
} }

View File

@@ -50,39 +50,47 @@ describe('VocabularyTreeviewService test suite', () => {
let nodeMapWithChildren: Map<string, TreeviewNode>; let nodeMapWithChildren: Map<string, TreeviewNode>;
let searchNodeMap: Map<string, TreeviewNode>; let searchNodeMap: Map<string, TreeviewNode>;
let vocabularyOptions; let vocabularyOptions;
let pageInfo: PageInfo;
const vocabularyServiceStub = jasmine.createSpyObj('VocabularyService', { const vocabularyServiceStub = jasmine.createSpyObj('VocabularyService', {
getVocabularyEntriesByValue: jasmine.createSpy('getVocabularyEntriesByValue'), getVocabularyEntriesByValue: jasmine.createSpy('getVocabularyEntriesByValue'),
getEntryDetailParent: jasmine.createSpy('getEntryDetailParent'), getEntryDetailParent: jasmine.createSpy('getEntryDetailParent'),
findEntryDetailByValue: jasmine.createSpy('findEntryDetailByValue'), findEntryDetailByValue: jasmine.createSpy('findEntryDetailByValue'),
searchTopEntries: jasmine.createSpy('searchTopEntries'), searchTopEntries: jasmine.createSpy('searchTopEntries'),
getEntryDetailChildren: jasmine.createSpy('getEntryDetailChildren') getEntryDetailChildren: jasmine.createSpy('getEntryDetailChildren'),
clearSearchTopRequests: jasmine.createSpy('clearSearchTopRequests')
}); });
function init() { 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); loadMoreNode = new TreeviewNode(LOAD_MORE_NODE, false, new PageInfo(), item);
loadMoreRootNode = new TreeviewNode(LOAD_MORE_ROOT_NODE, false, new PageInfo(), null); loadMoreRootNode = new TreeviewNode(LOAD_MORE_ROOT_NODE, false, new PageInfo(), null);
loadMoreRootFlatNode = new TreeviewFlatNode(LOAD_MORE_ROOT_NODE, 1, false, new PageInfo(), null); loadMoreRootFlatNode = new TreeviewFlatNode(LOAD_MORE_ROOT_NODE, 1, false, new PageInfo(), null);
item = new VocabularyEntryDetail(); item = new VocabularyEntryDetail();
item.id = item.value = item.display = 'root1'; item.id = item.value = item.display = 'root1';
item.otherInformation = { children: 'root1-child1::root1-child2', id: 'root1' }; item.otherInformation = { hasChildren: 'true', id: 'root1' };
itemNode = new TreeviewNode(item, true); itemNode = new TreeviewNode(item, true, pageInfo);
searchItemNode = new TreeviewNode(item, true, new PageInfo(), null, true); searchItemNode = new TreeviewNode(item, true, new PageInfo(), null, true);
item2 = new VocabularyEntryDetail(); item2 = new VocabularyEntryDetail();
item2.id = item2.value = item2.display = 'root2'; item2.id = item2.value = item2.display = 'root2';
item2.otherInformation = { id: 'root2' }; item2.otherInformation = { id: 'root2' };
itemNode2 = new TreeviewNode(item2); itemNode2 = new TreeviewNode(item2, false, pageInfo);
item3 = new VocabularyEntryDetail(); item3 = new VocabularyEntryDetail();
item3.id = item3.value = item3.display = 'root3'; item3.id = item3.value = item3.display = 'root3';
item3.otherInformation = { id: 'root3' }; item3.otherInformation = { id: 'root3' };
itemNode3 = new TreeviewNode(item3); itemNode3 = new TreeviewNode(item3, false, pageInfo);
child = new VocabularyEntryDetail(); child = new VocabularyEntryDetail();
child.id = child.value = child.display = 'root1-child1'; 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); childNode = new TreeviewNode(child);
searchChildNode = new TreeviewNode(child, true, new PageInfo(), item, true); searchChildNode = new TreeviewNode(child, true, new PageInfo(), item, true);
@@ -167,12 +175,6 @@ describe('VocabularyTreeviewService test suite', () => {
describe('initialize', () => { describe('initialize', () => {
it('should set vocabularyName and call retrieveTopNodes method', () => { 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', { serviceAsAny.vocabularyService.searchTopEntries.and.returnValue(hot('-a', {
a: createSuccessfulRemoteDataObject(new PaginatedList(pageInfo, [item, item2, item3])) a: createSuccessfulRemoteDataObject(new PaginatedList(pageInfo, [item, item2, item3]))
})); }));
@@ -181,16 +183,12 @@ describe('VocabularyTreeviewService test suite', () => {
scheduler.flush(); scheduler.flush();
expect(serviceAsAny.vocabularyName).toEqual(vocabularyOptions.name); 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]); expect(serviceAsAny.dataChange.value).toEqual([itemNode, itemNode2, itemNode3]);
}); });
it('should set initValueHierarchy', () => { 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', { serviceAsAny.vocabularyService.searchTopEntries.and.returnValue(hot('-c', {
a: createSuccessfulRemoteDataObject(new PaginatedList(pageInfo, [item, item2, item3])) a: createSuccessfulRemoteDataObject(new PaginatedList(pageInfo, [item, item2, item3]))
})); }));
@@ -239,7 +237,7 @@ describe('VocabularyTreeviewService test suite', () => {
}); });
it('should add children nodes properly', () => { it('should add children nodes properly', () => {
const pageInfo = Object.assign(new PageInfo(), { pageInfo = Object.assign(new PageInfo(), {
elementsPerPage: 1, elementsPerPage: 1,
totalElements: 2, totalElements: 2,
totalPages: 2, totalPages: 2,
@@ -260,7 +258,7 @@ describe('VocabularyTreeviewService test suite', () => {
}); });
it('should add loadMore node properly', () => { it('should add loadMore node properly', () => {
const pageInfo = Object.assign(new PageInfo(), { pageInfo = Object.assign(new PageInfo(), {
elementsPerPage: 1, elementsPerPage: 1,
totalElements: 2, totalElements: 2,
totalPages: 2, totalPages: 2,
@@ -285,7 +283,7 @@ describe('VocabularyTreeviewService test suite', () => {
describe('searchByQuery', () => { describe('searchByQuery', () => {
it('should set tree data properly after a search', () => { it('should set tree data properly after a search', () => {
const pageInfo = Object.assign(new PageInfo(), { pageInfo = Object.assign(new PageInfo(), {
elementsPerPage: 1, elementsPerPage: 1,
totalElements: 1, totalElements: 1,
totalPages: 1, totalPages: 1,

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; 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 { findIndex } from 'lodash';
import { LOAD_MORE_NODE, LOAD_MORE_ROOT_NODE, TreeviewFlatNode, TreeviewNode } from './vocabulary-treeview-node.model'; 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[] = []; private initValueHierarchy: string[] = [];
/** /**
* A boolean representing if a search operation is pending * A boolean representing if any operation is pending
*/ */
private searching = new BehaviorSubject<boolean>(false); private loading = new BehaviorSubject<boolean>(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 * Initialize instance variables
@@ -92,8 +96,10 @@ export class VocabularyTreeviewService {
* @param initValueId The entry id of the node to mark as selected, if any * @param initValueId The entry id of the node to mark as selected, if any
*/ */
initialize(options: VocabularyOptions, pageInfo: PageInfo, initValueId?: string): void { initialize(options: VocabularyOptions, pageInfo: PageInfo, initValueId?: string): void {
this.loading.next(true);
this.vocabularyOptions = options; this.vocabularyOptions = options;
this.vocabularyName = options.name; this.vocabularyName = options.name;
this.pageInfo = pageInfo;
if (isNotEmpty(initValueId)) { if (isNotEmpty(initValueId)) {
this.getNodeHierarchyById(initValueId) this.getNodeHierarchyById(initValueId)
.subscribe((hierarchy: string[]) => { .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<boolean> { isLoading(): Observable<boolean> {
return this.searching; return this.loading;
} }
/** /**
* Perform a search operation by query * Perform a search operation by query
*/ */
searchByQuery(query: string) { searchByQuery(query: string) {
this.searching.next(true); this.loading.next(true);
if (isEmpty(this.storedNodes)) { if (isEmpty(this.storedNodes)) {
this.storedNodes = this.dataChange.value; this.storedNodes = this.dataChange.value;
this.storedNodeMap = this.nodeMap; this.storedNodeMap = this.nodeMap;
@@ -192,7 +198,7 @@ export class VocabularyTreeviewService {
merge(this.hideSearchingWhenUnsubscribed$) merge(this.hideSearchingWhenUnsubscribed$)
).subscribe((nodes: TreeviewNode[]) => { ).subscribe((nodes: TreeviewNode[]) => {
this.dataChange.next(nodes); 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 * Reset tree state with the one before the search
*/ */
restoreNodes() { restoreNodes() {
this.searching.next(false); this.loading.next(false);
this.dataChange.next(this.storedNodes); this.dataChange.next(this.storedNodes);
this.nodeMap = this.storedNodeMap; this.nodeMap = this.storedNodeMap;
@@ -224,8 +230,8 @@ export class VocabularyTreeviewService {
const entryDetail: VocabularyEntryDetail = Object.assign(new VocabularyEntryDetail(), entry, { const entryDetail: VocabularyEntryDetail = Object.assign(new VocabularyEntryDetail(), entry, {
id: entryId id: entryId
}); });
const hasChildren = entry.hasOtherInformation() && isNotEmpty((entry.otherInformation as any).children); const hasChildren = entry.hasOtherInformation() && (entry.otherInformation as any)!.hasChildren == 'true';
const pageInfo: PageInfo = new PageInfo(); const pageInfo: PageInfo = this.pageInfo;
const isInInitValueHierarchy = this.initValueHierarchy.includes(entryId); const isInInitValueHierarchy = this.initValueHierarchy.includes(entryId);
const result = new TreeviewNode( const result = new TreeviewNode(
entryDetail, entryDetail,
@@ -295,8 +301,10 @@ export class VocabularyTreeviewService {
this.vocabularyService.searchTopEntries(this.vocabularyName, pageInfo).pipe( this.vocabularyService.searchTopEntries(this.vocabularyName, pageInfo).pipe(
getFirstSucceededRemoteDataPayload() getFirstSucceededRemoteDataPayload()
).subscribe((list: PaginatedList<VocabularyEntryDetail>) => { ).subscribe((list: PaginatedList<VocabularyEntryDetail>) => {
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); nodes.push(...newNodes);
if ((list.pageInfo.currentPage + 1) <= list.pageInfo.totalPages) { if ((list.pageInfo.currentPage + 1) <= list.pageInfo.totalPages) {
// Need a new load more node // Need a new load more node
const newPageInfo: PageInfo = Object.assign(new PageInfo(), list.pageInfo, { const newPageInfo: PageInfo = Object.assign(new PageInfo(), list.pageInfo, {
@@ -306,6 +314,7 @@ export class VocabularyTreeviewService {
loadMoreNode.updatePageInfo(newPageInfo); loadMoreNode.updatePageInfo(newPageInfo);
nodes.push(loadMoreNode); nodes.push(loadMoreNode);
} }
this.loading.next(false);
// Notify the change. // Notify the change.
this.dataChange.next(nodes); this.dataChange.next(nodes);
}); });
@@ -328,8 +337,7 @@ export class VocabularyTreeviewService {
if (isNotEmpty(children)) { if (isNotEmpty(children)) {
const newChildren = children const newChildren = children
.filter((entry: TreeviewNode) => { .filter((entry: TreeviewNode) => {
const ii = findIndex(node.children, (nodeEntry) => nodeEntry.item.id === entry.item.id); return findIndex(node.children, (nodeEntry) => nodeEntry.item.id === entry.item.id) === -1;
return ii === -1;
}); });
newChildren.forEach((entry: TreeviewNode) => { newChildren.forEach((entry: TreeviewNode) => {
entry.loadMoreParentItem = node.item entry.loadMoreParentItem = node.item

View File

@@ -2712,6 +2712,24 @@
"title": "DSpace", "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", "administrativeView.search.results.head": "Administrative Search",
"menu.section.admin_search": "Admin Search", "menu.section.admin_search": "Admin Search",