From e1b80bcbafc6ce71d1dbdaecc601452e2739761e Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 23 Jun 2020 18:12:02 +0200 Subject: [PATCH 01/62] [CST-3088] changed buildHrefFromFindOptions in the way to use search params while building url --- src/app/core/data/data.service.spec.ts | 24 +++++++++++++++++++++++- src/app/core/data/data.service.ts | 19 ++++++++++++++----- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index a99fc54269..31013c5132 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -18,6 +18,7 @@ import { FindListOptions, PatchRequest } from './request.models'; import { RequestService } from './request.service'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { RequestParam } from '../cache/models/request-param.model'; const endpoint = 'https://rest.api/core'; @@ -150,7 +151,8 @@ describe('DataService', () => { currentPage: 6, elementsPerPage: 10, sort: sortOptions, - startsWith: 'ab' + startsWith: 'ab', + }; const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` + `&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`; @@ -160,6 +162,26 @@ describe('DataService', () => { }); }); + it('should include all searchParams in href if any provided in options', () => { + options = { searchParams: [ + new RequestParam('param1', 'test'), + new RequestParam('param2', 'test2'), + ] }; + const expected = `${endpoint}?param1=test¶m2=test2`; + + (service as any).getFindAllHref(options).subscribe((value) => { + expect(value).toBe(expected); + }); + }); + + it('should include linkPath in href if any provided', () => { + const expected = `${endpoint}/test/entries`; + + (service as any).getFindAllHref({}, 'test/entries').subscribe((value) => { + expect(value).toBe(expected); + }); + }); + it('should include single linksToFollow as embed', () => { const expected = `${endpoint}?embed=bundles`; diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index ca59daa5af..d7e24892ef 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -86,13 +86,17 @@ export abstract class DataService { * Return an observable that emits created HREF * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - protected getFindAllHref(options: FindListOptions = {}, linkPath?: string, ...linksToFollow: Array>): Observable { - let result$: Observable; + public getFindAllHref(options: FindListOptions = {}, linkPath?: string, ...linksToFollow: Array>): Observable { + let endpoint$: Observable; const args = []; - result$ = this.getBrowseEndpoint(options, linkPath).pipe(distinctUntilChanged()); + endpoint$ = this.getBrowseEndpoint(options).pipe( + filter((href: string) => isNotEmpty(href)), + map((href: string) => isNotEmpty(linkPath) ? `${href}/${linkPath}` : href), + distinctUntilChanged() + ); - return result$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args, ...linksToFollow))); + return endpoint$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args, ...linksToFollow))); } /** @@ -104,7 +108,7 @@ export abstract class DataService { * Return an observable that emits created HREF * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - protected getSearchByHref(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable { + public getSearchByHref(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable { let result$: Observable; const args = []; @@ -145,6 +149,11 @@ export abstract class DataService { if (hasValue(options.startsWith)) { args = [...args, `startsWith=${options.startsWith}`]; } + if (hasValue(options.searchParams)) { + options.searchParams.forEach((param: RequestParam) => { + args = [...args, `${param.fieldName}=${param.fieldValue}`]; + }) + } args = this.addEmbedParams(args, ...linksToFollow); if (isNotEmpty(args)) { return new URLCombiner(href, `?${args.join('&')}`).toString(); From 1156bd3934932858da65f25a2dd902401cb5852d Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 23 Jun 2020 18:17:47 +0200 Subject: [PATCH 02/62] [CST-3088] Added vocabulary service and models --- src/app/core/core.module.ts | 28 +- src/app/core/data/request.models.ts | 10 + .../models/vocabularies.resource-type.ts | 11 + .../models/vocabulary-entry.model.ts | 103 +++++++ .../vocabularies/models/vocabulary.model.ts | 61 ++++ ...y-entries-response-parsing.service.spec.ts | 111 ++++++++ ...bulary-entries-response-parsing.service.ts | 20 ++ .../vocabularies/vocabulary.service.spec.ts | 269 ++++++++++++++++++ .../vocabularies/vocabulary.service.ts | 194 +++++++++++++ 9 files changed, 796 insertions(+), 11 deletions(-) create mode 100644 src/app/core/submission/vocabularies/models/vocabularies.resource-type.ts create mode 100644 src/app/core/submission/vocabularies/models/vocabulary-entry.model.ts create mode 100644 src/app/core/submission/vocabularies/models/vocabulary.model.ts create mode 100644 src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.spec.ts create mode 100644 src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.ts create mode 100644 src/app/core/submission/vocabularies/vocabulary.service.spec.ts create mode 100644 src/app/core/submission/vocabularies/vocabulary.service.ts diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 715f7a5cc0..a92b52b339 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -16,8 +16,8 @@ import { MenuService } from '../shared/menu/menu.service'; import { EndpointMockingRestService } from '../shared/mocks/dspace-rest-v2/endpoint-mocking-rest.service'; import { MOCK_RESPONSE_MAP, - ResponseMapMock, - mockResponseMap + mockResponseMap, + ResponseMapMock } from '../shared/mocks/dspace-rest-v2/mocks/response-map.mock'; import { NotificationsService } from '../shared/notifications/notifications.service'; import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service'; @@ -145,6 +145,9 @@ import { Version } from './shared/version.model'; import { VersionHistory } from './shared/version-history.model'; import { WorkflowActionDataService } from './data/workflow-action-data.service'; import { WorkflowAction } from './tasks/models/workflow-action-object.model'; +import { VocabularyEntry } from './submission/vocabularies/models/vocabulary-entry.model'; +import { Vocabulary } from './submission/vocabularies/models/vocabulary.model'; +import { VocabularyEntriesResponseParsingService } from './submission/vocabularies/vocabulary-entries-response-parsing.service'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -178,7 +181,7 @@ const PROVIDERS = [ SiteDataService, DSOResponseParsingService, { provide: MOCK_RESPONSE_MAP, useValue: mockResponseMap }, - { provide: DSpaceRESTv2Service, useFactory: restServiceFactory, deps: [MOCK_RESPONSE_MAP, HttpClient]}, + { provide: DSpaceRESTv2Service, useFactory: restServiceFactory, deps: [MOCK_RESPONSE_MAP, HttpClient] }, DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService, @@ -272,7 +275,8 @@ const PROVIDERS = [ }, NotificationsService, FilteredDiscoveryPageResponseParsingService, - { provide: NativeWindowService, useFactory: NativeWindowFactory } + { provide: NativeWindowService, useFactory: NativeWindowFactory }, + VocabularyEntriesResponseParsingService ]; /** @@ -314,7 +318,9 @@ export const models = ExternalSourceEntry, Version, VersionHistory, - WorkflowAction + WorkflowAction, + Vocabulary, + VocabularyEntry ]; @NgModule({ @@ -333,6 +339,12 @@ export const models = }) export class CoreModule { + constructor(@Optional() @SkipSelf() parentModule: CoreModule) { + if (isNotEmpty(parentModule)) { + throw new Error('CoreModule is already loaded. Import it in the AppModule only'); + } + } + static forRoot(): ModuleWithProviders { return { ngModule: CoreModule, @@ -341,10 +353,4 @@ export class CoreModule { ] }; } - - constructor(@Optional() @SkipSelf() parentModule: CoreModule) { - if (isNotEmpty(parentModule)) { - throw new Error('CoreModule is already loaded. Import it in the AppModule only'); - } - } } diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index 5866cce797..c9fe1fe0ce 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -20,6 +20,7 @@ import { URLCombiner } from '../url-combiner/url-combiner'; import { TaskResponseParsingService } from '../tasks/task-response-parsing.service'; import { ContentSourceResponseParsingService } from './content-source-response-parsing.service'; import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service'; +import { VocabularyEntriesResponseParsingService } from '../submission/vocabularies/vocabulary-entries-response-parsing.service'; /* tslint:disable:max-classes-per-file */ @@ -442,6 +443,15 @@ export class MyDSpaceRequest extends GetRequest { public responseMsToLive = 10 * 1000; } +/** + * Request to get vocabulary entries + */ +export class VocabularyEntriesRequest extends FindListRequest { + getResponseParser(): GenericConstructor { + return VocabularyEntriesResponseParsingService; + } +} + export class RequestError extends Error { statusCode: number; statusText: string; diff --git a/src/app/core/submission/vocabularies/models/vocabularies.resource-type.ts b/src/app/core/submission/vocabularies/models/vocabularies.resource-type.ts new file mode 100644 index 0000000000..4d49abd823 --- /dev/null +++ b/src/app/core/submission/vocabularies/models/vocabularies.resource-type.ts @@ -0,0 +1,11 @@ +import { ResourceType } from '../../../shared/resource-type'; + +/** + * The resource type for vocabulary models + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ + +export const VOCABULARY = new ResourceType('vocabulary'); +export const VOCABULARY_ENTRY = new ResourceType('vocabularyEntry'); diff --git a/src/app/core/submission/vocabularies/models/vocabulary-entry.model.ts b/src/app/core/submission/vocabularies/models/vocabulary-entry.model.ts new file mode 100644 index 0000000000..5fe951c8aa --- /dev/null +++ b/src/app/core/submission/vocabularies/models/vocabulary-entry.model.ts @@ -0,0 +1,103 @@ +import { autoserialize, deserialize } from 'cerialize'; + +import { HALLink } from '../../../shared/hal-link.model'; +import { VOCABULARY_ENTRY } from './vocabularies.resource-type'; +import { typedObject } from '../../../cache/builders/build-decorators'; +import { excludeFromEquals } from '../../../utilities/equals.decorators'; +import { PLACEHOLDER_PARENT_METADATA } from '../../../../shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; +import { OtherInformation } from '../../../../shared/form/builder/models/form-field-metadata-value.model'; +import { isNotEmpty } from '../../../../shared/empty.util'; +import { ListableObject } from '../../../../shared/object-collection/shared/listable-object.model'; +import { GenericConstructor } from '../../../shared/generic-constructor'; + +/** + * Model class for a Vocabulary + */ +@typedObject +export class VocabularyEntry extends ListableObject { + static type = VOCABULARY_ENTRY; + + /** + * The identifier of this vocabulary entry + */ + @autoserialize + authority: string; + + /** + * The display value of this vocabulary entry + */ + @autoserialize + display: string; + + /** + * The value of this vocabulary entry + */ + @autoserialize + value: string; + + /** + * An object containing additional information related to this vocabulary entry + */ + @autoserialize + otherInformation: OtherInformation; + + /** + * A string representing the kind of vocabulary entry + */ + @excludeFromEquals + @autoserialize + public type: any; + + /** + * The {@link HALLink}s for this ExternalSourceEntry + */ + @deserialize + _links: { + self: HALLink; + vocabularyEntryDetail: HALLink; + }; + + /** + * This method checks if entry has an authority value + * + * @return boolean + */ + hasAuthority(): boolean { + return isNotEmpty(this.authority); + } + + /** + * This method checks if entry has a value + * + * @return boolean + */ + hasValue(): boolean { + return isNotEmpty(this.value); + } + + /** + * This method checks if entry has related information object + * + * @return boolean + */ + hasOtherInformation(): boolean { + return isNotEmpty(this.otherInformation); + } + + /** + * This method checks if entry has a placeholder as value + * + * @return boolean + */ + hasPlaceholder(): boolean { + return this.hasValue() && this.value === PLACEHOLDER_PARENT_METADATA; + } + + /** + * 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/submission/vocabularies/models/vocabulary.model.ts b/src/app/core/submission/vocabularies/models/vocabulary.model.ts new file mode 100644 index 0000000000..8672d1c6ed --- /dev/null +++ b/src/app/core/submission/vocabularies/models/vocabulary.model.ts @@ -0,0 +1,61 @@ +import { autoserialize, deserialize } from 'cerialize'; + +import { HALLink } from '../../../shared/hal-link.model'; +import { VOCABULARY } from './vocabularies.resource-type'; +import { CacheableObject } from '../../../cache/object-cache.reducer'; +import { typedObject } from '../../../cache/builders/build-decorators'; +import { excludeFromEquals } from '../../../utilities/equals.decorators'; + +/** + * Model class for a Vocabulary + */ +@typedObject +export class Vocabulary implements CacheableObject { + static type = VOCABULARY; + /** + * The identifier of this Vocabulary + */ + @autoserialize + id: string; + + /** + * The name of this Vocabulary + */ + @autoserialize + name: string; + + /** + * True if it is possible to scroll all the entries in the vocabulary without providing a filter parameter + */ + @autoserialize + scrollable: boolean; + + /** + * True if the vocabulary exposes a tree structure where some entries are parent of others + */ + @autoserialize + hierarchical: boolean; + + /** + * For hierarchical vocabularies express the preference to preload the tree at a specific + * level of depth (0 only the top nodes are shown, 1 also their children are preloaded and so on) + */ + @autoserialize + preloadLevel: any; + + /** + * A string representing the kind of Vocabulary model + */ + @excludeFromEquals + @autoserialize + public type: any; + + /** + * The {@link HALLink}s for this Vocabulary + */ + @deserialize + _links: { + self: HALLink, + entries: HALLink + }; +} diff --git a/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.spec.ts b/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.spec.ts new file mode 100644 index 0000000000..592ed234a7 --- /dev/null +++ b/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.spec.ts @@ -0,0 +1,111 @@ +import { getMockObjectCacheService } from '../../../shared/mocks/object-cache.service.mock'; +import { ErrorResponse, GenericSuccessResponse } from '../../cache/response.models'; +import { DSpaceRESTV2Response } from '../../dspace-rest-v2/dspace-rest-v2-response.model'; +import { VocabularyEntriesResponseParsingService } from './vocabulary-entries-response-parsing.service'; +import { VocabularyEntriesRequest } from '../../data/request.models'; + +fdescribe('VocabularyEntriesResponseParsingService', () => { + let service: VocabularyEntriesResponseParsingService; + const metadata = 'dc.type'; + const collectionUUID = '8b39g7ya-5a4b-438b-851f-be1d5b4a1c5a'; + const entriesRequestURL = `https://rest.api/rest/api/submission/vocabularies/types/entries?metadata=${metadata}&collection=${collectionUUID}` + + beforeEach(() => { + service = new VocabularyEntriesResponseParsingService(getMockObjectCacheService()); + }); + + describe('parse', () => { + const request = new VocabularyEntriesRequest('client/f5b4ccb8-fbb0-4548-b558-f234d9fdfad6', entriesRequestURL); + + const validResponse = { + payload: { + _embedded: { + entries: [ + { + display: 'testValue1', + value: 'testValue1', + otherInformation: {}, + type: 'vocabularyEntry' + }, + { + display: 'testValue2', + value: 'testValue2', + otherInformation: {}, + type: 'vocabularyEntry' + }, + { + display: 'testValue3', + value: 'testValue3', + otherInformation: {}, + type: 'vocabularyEntry' + }, + { + authority: 'authorityId1', + display: 'testValue1', + value: 'testValue1', + otherInformation: { + id: 'VR131402', + parent: 'Research Subject Categories::SOCIAL SCIENCES::Social sciences::Social work', + hasChildren: 'false', + note: 'Familjeforskning' + }, + type: 'vocabularyEntry', + _links: { + vocabularyEntryDetail: { + href: 'https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:VR131402' + } + } + } + ] + }, + _links: { + first: { + href: 'https://rest.api/discover/browses/author/entries?page=0&size=5' + }, + self: { + href: 'https://rest.api/discover/browses/author/entries' + }, + next: { + href: 'https://rest.api/discover/browses/author/entries?page=1&size=5' + }, + last: { + href: 'https://rest.api/discover/browses/author/entries?page=9&size=5' + } + }, + page: { + size: 5, + totalElements: 50, + totalPages: 10, + number: 0 + } + }, + statusCode: 200, + statusText: 'OK' + } as DSpaceRESTV2Response; + + const invalidResponseNotAList = { + statusCode: 200, + statusText: 'OK' + } as DSpaceRESTV2Response; + + const invalidResponseStatusCode = { + payload: {}, statusCode: 500, statusText: 'Internal Server Error' + } as DSpaceRESTV2Response; + + it('should return a GenericSuccessResponse if data contains a valid browse entries response', () => { + const response = service.parse(request, validResponse); + expect(response.constructor).toBe(GenericSuccessResponse); + }); + + it('should return an ErrorResponse if data contains an invalid browse entries response', () => { + const response = service.parse(request, invalidResponseNotAList); + expect(response.constructor).toBe(ErrorResponse); + }); + + it('should return an ErrorResponse if data contains a statuscode other than 200', () => { + const response = service.parse(request, invalidResponseStatusCode); + expect(response.constructor).toBe(ErrorResponse); + }); + + }); +}); diff --git a/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.ts b/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.ts new file mode 100644 index 0000000000..680b949733 --- /dev/null +++ b/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; + +import { ObjectCacheService } from '../../cache/object-cache.service'; +import { BrowseEntriesResponseParsingService } from '../../data/browse-entries-response-parsing.service'; + +/** + * A service responsible for parsing data for a vocabulary entries response + */ +@Injectable() +export class VocabularyEntriesResponseParsingService extends BrowseEntriesResponseParsingService { + + protected toCache = false; + + constructor( + protected objectCache: ObjectCacheService, + ) { + super(objectCache); + } + +} diff --git a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts new file mode 100644 index 0000000000..2596e368b5 --- /dev/null +++ b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts @@ -0,0 +1,269 @@ +import { HttpClient } from '@angular/common/http'; + +import { cold, getTestScheduler, hot } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; + +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../../cache/object-cache.service'; +import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { RequestService } from '../../data/request.service'; +import { FindListOptions, VocabularyEntriesRequest } from '../../data/request.models'; +import { RequestParam } from '../../cache/models/request-param.model'; +import { PageInfo } from '../../shared/page-info.model'; +import { PaginatedList } from '../../data/paginated-list'; +import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { RequestEntry } from '../../data/request.reducer'; +import { RestResponse } from '../../cache/response.models'; +import { VocabularyService } from './vocabulary.service'; +import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; +import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; + +describe('VocabularyService', () => { + let scheduler: TestScheduler; + let service: VocabularyService; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let halService: HALEndpointService; + let responseCacheEntry: RequestEntry; + + const vocabulary: any = { + id: 'types', + name: 'types', + scrollable: true, + hierarchical: false, + preloadLevel: 1, + type: 'vocabulary', + uuid: 'vocabulary-types', + _links: { + self: { + href: 'https://rest.api/rest/api/submission/vocabularies/types' + }, + entries: { + href: 'https://rest.api/rest/api/submission/vocabularies/types/entries' + }, + } + }; + + const anotherVocabulary: any = { + id: 'srsc', + name: 'srsc', + scrollable: false, + hierarchical: true, + preloadLevel: 2, + type: 'vocabulary', + uuid: 'vocabulary-srsc', + _links: { + self: { + href: 'https://rest.api/rest/api/submission/vocabularies/types' + }, + entries: { + href: 'https://rest.api/rest/api/submission/vocabularies/types/entries' + }, + } + }; + + const vocabularyEntry: any = { + display: 'testValue1', + value: 'testValue1', + otherInformation: {}, + type: 'vocabularyEntry' + }; + + const vocabularyEntryWithAuthority: any = { + authority: 'authorityId1', + display: 'testValue1', + value: 'testValue1', + otherInformation: { + id: 'VR131402', + parent: 'Research Subject Categories::SOCIAL SCIENCES::Social sciences::Social work', + hasChildren: 'false', + note: 'Familjeforskning' + }, + type: 'vocabularyEntry', + _links: { + vocabularyEntryDetail: { + href: 'https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:VR131402' + } + } + }; + const endpointURL = `https://rest.api/rest/api/submission/vocabularies`; + const requestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}`; + const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; + const vocabularyId = 'types'; + const metadata = 'dc.type'; + const collectionUUID = '8b39g7ya-5a4b-438b-851f-be1d5b4a1c5a'; + const searchRequestURL = `https://rest.api/rest/api/submission/vocabularies/search/byMetadataAndCollection?metadata=${metadata}&collection=${collectionUUID}`; + const entriesRequestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}/entries?metadata=${metadata}&collection=${collectionUUID}`; + + const pageInfo = new PageInfo(); + const array = [vocabulary, anotherVocabulary]; + const paginatedList = new PaginatedList(pageInfo, array); + const vocabularyRD = createSuccessfulRemoteDataObject(vocabulary); + const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); + const getRequestEntry$ = (successful: boolean) => { + return observableOf({ + response: { isSuccessful: successful, payload: vocabulary } as any + } as RequestEntry) + }; + objectCache = {} as ObjectCacheService; + const notificationsService = {} as NotificationsService; + const http = {} as HttpClient; + const comparator = {} as any; + + function initTestService() { + return new VocabularyService( + requestService, + rdbService, + objectCache, + halService, + notificationsService, + http, + comparator + ); + } + + beforeEach(() => { + scheduler = getTestScheduler(); + + halService = jasmine.createSpyObj('halService', { + getEndpoint: cold('a', { a: endpointURL }) + }); + + responseCacheEntry = new RequestEntry(); + responseCacheEntry.response = new RestResponse(true, 200, 'Success'); + + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: requestUUID, + configure: true, + removeByHrefSubstring: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: observableOf(responseCacheEntry), + }); + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: hot('a|', { + a: vocabularyRD + }), + buildList: hot('a|', { + a: paginatedListRD + }), + }); + + service = initTestService(); + + spyOn((service as any).dataService, 'findById').and.callThrough(); + spyOn((service as any).dataService, 'findAll').and.callThrough(); + spyOn((service as any).dataService, 'findByHref').and.callThrough(); + spyOn((service as any).dataService, 'searchBy').and.callThrough(); + spyOn((service as any).dataService, 'getSearchByHref').and.returnValue(observableOf(searchRequestURL)); + spyOn((service as any).dataService, 'getFindAllHref').and.returnValue(observableOf(entriesRequestURL)); + }); + + afterEach(() => { + service = null; + }); + + describe('findById', () => { + it('should proxy the call to dataservice.findById', () => { + scheduler.schedule(() => service.findById(vocabularyId)); + scheduler.flush(); + + expect((service as any).dataService.findById).toHaveBeenCalledWith(vocabularyId); + }); + + it('should return a RemoteData for the object with the given id', () => { + const result = service.findById(vocabularyId); + const expected = cold('a|', { + a: vocabularyRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('findByHref', () => { + it('should proxy the call to dataservice.findByHref', () => { + scheduler.schedule(() => service.findByHref(requestURL)); + scheduler.flush(); + + expect((service as any).dataService.findByHref).toHaveBeenCalledWith(requestURL); + }); + + it('should return a RemoteData for the object with the given URL', () => { + const result = service.findByHref(requestURL); + const expected = cold('a|', { + a: vocabularyRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('findAll', () => { + it('should proxy the call to dataservice.findAll', () => { + scheduler.schedule(() => service.findAll()); + scheduler.flush(); + + expect((service as any).dataService.findAll).toHaveBeenCalled(); + }); + + it('should return a RemoteData>', () => { + const result = service.findAll(); + const expected = cold('a|', { + a: paginatedListRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('searchByMetadataAndCollection', () => { + it('should proxy the call to dataservice.findByHref', () => { + const options = new FindListOptions(); + options.searchParams = [ + new RequestParam('metadata', metadata), + new RequestParam('collection', collectionUUID) + ]; + scheduler.schedule(() => service.searchByMetadataAndCollection(metadata, collectionUUID).subscribe()); + scheduler.flush(); + + expect((service as any).dataService.findByHref).toHaveBeenCalledWith(searchRequestURL); + }); + + it('should return a RemoteData for the search', () => { + const result = service.searchByMetadataAndCollection(metadata, collectionUUID); + const expected = cold('a|', { + a: vocabularyRD + }); + expect(result).toBeObservable(expected); + }); + + }); + + describe('getVocabularyEntries', () => { + + beforeEach(() => { + requestService = getMockRequestService(getRequestEntry$(true)); + rdbService = getMockRemoteDataBuildService(); + spyOn(rdbService, 'toRemoteDataObservable').and.callThrough(); + service = initTestService(); + }); + + it('should configure a new VocabularyEntriesRequest', () => { + const expected = new VocabularyEntriesRequest(requestService.generateRequestId(), entriesRequestURL); + + scheduler.schedule(() => service.getVocabularyEntries(vocabularyId, metadata, collectionUUID).subscribe()); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + + it('should call RemoteDataBuildService to create the RemoteData Observable', () => { + service.getVocabularyEntries(vocabularyId, metadata, collectionUUID); + + expect(rdbService.toRemoteDataObservable).toHaveBeenCalled(); + + }); + + }); + +}); diff --git a/src/app/core/submission/vocabularies/vocabulary.service.ts b/src/app/core/submission/vocabularies/vocabulary.service.ts new file mode 100644 index 0000000000..cfb795c942 --- /dev/null +++ b/src/app/core/submission/vocabularies/vocabulary.service.ts @@ -0,0 +1,194 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; + +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { distinctUntilChanged, first, flatMap, map } from 'rxjs/operators'; + +import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; +import { dataService } from '../../cache/builders/build-decorators'; +import { DataService } from '../../data/data.service'; +import { RequestService } from '../../data/request.service'; +import { FindListOptions, RestRequest, VocabularyEntriesRequest } from '../../data/request.models'; +import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { RemoteData } from '../../data/remote-data'; +import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { CoreState } from '../../core.reducers'; +import { ObjectCacheService } from '../../cache/object-cache.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { ChangeAnalyzer } from '../../data/change-analyzer'; +import { DefaultChangeAnalyzer } from '../../data/default-change-analyzer.service'; +import { PaginatedList } from '../../data/paginated-list'; +import { RequestParam } from '../../cache/models/request-param.model'; +import { Vocabulary } from './models/vocabulary.model'; +import { VOCABULARY } from './models/vocabularies.resource-type'; +import { VocabularyEntry } from './models/vocabulary-entry.model'; +import { hasValue, isNotEmptyOperator } from '../../../shared/empty.util'; +import { configureRequest, filterSuccessfulResponses, getRequestFromRequestHref } from '../../shared/operators'; +import { GenericSuccessResponse } from '../../cache/response.models'; + +/* tslint:disable:max-classes-per-file */ + +/** + * A private DataService implementation to delegate specific methods to. + */ +class DataServiceImpl extends DataService { + protected linkPath = 'vocabularies'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: ChangeAnalyzer) { + super(); + } + +} + +/** + * A service responsible for fetching/sending data from/to the REST API on the vocabularies endpoint + */ +@Injectable({ + providedIn: 'root' +}) +@dataService(VOCABULARY) +export class VocabularyService { + protected searchByMetadataAndCollectionMethod = 'byMetadataAndCollection'; + private dataService: DataServiceImpl; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); + } + + /** + * Returns an observable of {@link RemoteData} of a {@link Vocabulary}, based on an href, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the {@link Vocabulary} + * @param href The url of {@link Vocabulary} we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @return {Observable>} + * Return an observable that emits vocabulary object + */ + findByHref(href: string, ...linksToFollow: Array>): Observable> { + return this.dataService.findByHref(href, ...linksToFollow); + } + + /** + * Returns an observable of {@link RemoteData} of a {@link Vocabulary}, based on its ID, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the object + * @param id ID of {@link Vocabulary} we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @return {Observable>} + * Return an observable that emits vocabulary object + */ + findById(id: string, ...linksToFollow: Array>): Observable> { + return this.dataService.findById(id, ...linksToFollow); + } + + /** + * Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded + * info should be added to the objects + * + * @param options Find list options object + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @return {Observable>>} + * Return an observable that emits object list + */ + findAll(options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + return this.dataService.findAll(options, ...linksToFollow); + } + + /** + * Return the {@link VocabularyEntry} list for a given {@link Vocabulary} + * + * @param id The vocabulary id to retrieve the entries for + * @param metadata The metadata name + * @param collectionUUID The collection UUID + * @param options The {@link FindListOptions} for the request + * @return {Observable>>} + * Return an observable that emits object list + */ + getVocabularyEntries(id: string, metadata: string, collectionUUID: string, options: FindListOptions = {}): Observable>> { + options = Object.assign({}, options, { + searchParams: [ + new RequestParam('metadata', metadata), + new RequestParam('collection', collectionUUID) + ] + }); + + return this.dataService.getFindAllHref(options, `${id}/entries`).pipe( + isNotEmptyOperator(), + distinctUntilChanged(), + getVocabularyEntriesFor(this.requestService, this.rdbService) + ); + } + + /** + * Return the controlled {@link Vocabulary} configured for the specified metadata and collection if any. + * + * @param metadata The metadata name + * @param collectionUUID The collection UUID + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @return {Observable>>} + * Return an observable that emits object list + */ + searchByMetadataAndCollection(metadata: string, collectionUUID: string, ...linksToFollow: Array>): Observable> { + const options = new FindListOptions(); + options.searchParams = [ + new RequestParam('metadata', metadata), + new RequestParam('collection', collectionUUID) + ]; + + return this.dataService.getSearchByHref(this.searchByMetadataAndCollectionMethod, options).pipe( + first((href: string) => hasValue(href)), + flatMap((href: string) => this.dataService.findByHref(href)) + ) + } + +} + +/** + * Operator for turning a href into a PaginatedList of VocabularyEntry + * @param requestService + * @param rdb + */ +export const getVocabularyEntriesFor = (requestService: RequestService, rdb: RemoteDataBuildService) => + (source: Observable): Observable>> => + source.pipe( + map((href: string) => new VocabularyEntriesRequest(requestService.generateRequestId(), href)), + configureRequest(requestService), + toRDPaginatedVocabularyEntries(requestService, rdb) + ); + +/** + * Operator for turning a RestRequest into a PaginatedList of VocabularyEntry + * @param requestService + * @param rdb + */ +export const toRDPaginatedVocabularyEntries = (requestService: RequestService, rdb: RemoteDataBuildService) => + (source: Observable): Observable>> => { + const href$ = source.pipe(map((request: RestRequest) => request.href)); + + const requestEntry$ = href$.pipe(getRequestFromRequestHref(requestService)); + + const payload$ = requestEntry$.pipe( + filterSuccessfulResponses(), + map((response: GenericSuccessResponse) => new PaginatedList(response.pageInfo, response.payload)), + map((list: PaginatedList) => Object.assign(list, { + page: list.page ? list.page.map((entry: VocabularyEntry) => Object.assign(new VocabularyEntry(), entry)) : list.page + })), + distinctUntilChanged() + ); + + return rdb.toRemoteDataObservable(requestEntry$, payload$); + }; From c7cf11f4214a42d0c6994967f2d97aee0dfb96db Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 24 Jun 2020 12:52:16 +0200 Subject: [PATCH 03/62] [CST-3088] Improved VocabularyFindOptions --- .../models/vocabulary-find-options.model.ts | 45 +++++++++++++++++++ ...y-entries-response-parsing.service.spec.ts | 2 +- .../vocabularies/vocabulary.service.spec.ts | 10 +++-- .../vocabularies/vocabulary.service.ts | 27 +++-------- 4 files changed, 58 insertions(+), 26 deletions(-) create mode 100644 src/app/core/submission/vocabularies/models/vocabulary-find-options.model.ts diff --git a/src/app/core/submission/vocabularies/models/vocabulary-find-options.model.ts b/src/app/core/submission/vocabularies/models/vocabulary-find-options.model.ts new file mode 100644 index 0000000000..18309ed1fe --- /dev/null +++ b/src/app/core/submission/vocabularies/models/vocabulary-find-options.model.ts @@ -0,0 +1,45 @@ +import { SortOptions } from '../../../cache/models/sort-options.model'; +import { FindListOptions } from '../../../data/request.models'; +import { RequestParam } from '../../../cache/models/request-param.model'; +import { isNotEmpty } from '../../../../shared/empty.util'; + +/** + * Representing properties used to build a vocabulary find request + */ +export class VocabularyFindOptions extends FindListOptions { + + constructor(public collection: string = '', + public name: string = '', + public metadata: string = '', + public query: string = '', + public elementsPerPage?: number, + public currentPage?: number, + public sort?: SortOptions, + public filter?: string, + public exact?: string, + public entryID?: string, + ) { + super(); + + const searchParams = []; + if (isNotEmpty(metadata)) { + searchParams.push(new RequestParam('metadata', metadata)) + } + if (isNotEmpty(collection)) { + searchParams.push(new RequestParam('collection', collection)) + } + if (isNotEmpty(query)) { + searchParams.push(new RequestParam('query', query)) + } + if (isNotEmpty(filter)) { + searchParams.push(new RequestParam('filter', filter)) + } + if (isNotEmpty(exact)) { + searchParams.push(new RequestParam('exact', exact)) + } + if (isNotEmpty(entryID)) { + searchParams.push(new RequestParam('entryID', entryID)) + } + this.searchParams = searchParams; + } +} diff --git a/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.spec.ts b/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.spec.ts index 592ed234a7..9f28ccea17 100644 --- a/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.spec.ts +++ b/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.spec.ts @@ -4,7 +4,7 @@ import { DSpaceRESTV2Response } from '../../dspace-rest-v2/dspace-rest-v2-respon import { VocabularyEntriesResponseParsingService } from './vocabulary-entries-response-parsing.service'; import { VocabularyEntriesRequest } from '../../data/request.models'; -fdescribe('VocabularyEntriesResponseParsingService', () => { +describe('VocabularyEntriesResponseParsingService', () => { let service: VocabularyEntriesResponseParsingService; const metadata = 'dc.type'; const collectionUUID = '8b39g7ya-5a4b-438b-851f-be1d5b4a1c5a'; diff --git a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts index 2596e368b5..5323c29f30 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts @@ -19,6 +19,7 @@ import { RestResponse } from '../../cache/response.models'; import { VocabularyService } from './vocabulary.service'; import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; +import { VocabularyFindOptions } from './models/vocabulary-find-options.model'; describe('VocabularyService', () => { let scheduler: TestScheduler; @@ -95,6 +96,7 @@ describe('VocabularyService', () => { const vocabularyId = 'types'; const metadata = 'dc.type'; const collectionUUID = '8b39g7ya-5a4b-438b-851f-be1d5b4a1c5a'; + const vocabularyOptions = new VocabularyFindOptions(collectionUUID, vocabularyId, metadata); const searchRequestURL = `https://rest.api/rest/api/submission/vocabularies/search/byMetadataAndCollection?metadata=${metadata}&collection=${collectionUUID}`; const entriesRequestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}/entries?metadata=${metadata}&collection=${collectionUUID}`; @@ -223,14 +225,14 @@ describe('VocabularyService', () => { new RequestParam('metadata', metadata), new RequestParam('collection', collectionUUID) ]; - scheduler.schedule(() => service.searchByMetadataAndCollection(metadata, collectionUUID).subscribe()); + scheduler.schedule(() => service.searchByMetadataAndCollection(vocabularyOptions).subscribe()); scheduler.flush(); expect((service as any).dataService.findByHref).toHaveBeenCalledWith(searchRequestURL); }); it('should return a RemoteData for the search', () => { - const result = service.searchByMetadataAndCollection(metadata, collectionUUID); + const result = service.searchByMetadataAndCollection(vocabularyOptions); const expected = cold('a|', { a: vocabularyRD }); @@ -251,14 +253,14 @@ describe('VocabularyService', () => { it('should configure a new VocabularyEntriesRequest', () => { const expected = new VocabularyEntriesRequest(requestService.generateRequestId(), entriesRequestURL); - scheduler.schedule(() => service.getVocabularyEntries(vocabularyId, metadata, collectionUUID).subscribe()); + scheduler.schedule(() => service.getVocabularyEntries(vocabularyOptions).subscribe()); scheduler.flush(); expect(requestService.configure).toHaveBeenCalledWith(expected); }); it('should call RemoteDataBuildService to create the RemoteData Observable', () => { - service.getVocabularyEntries(vocabularyId, metadata, collectionUUID); + service.getVocabularyEntries(vocabularyOptions); expect(rdbService.toRemoteDataObservable).toHaveBeenCalled(); diff --git a/src/app/core/submission/vocabularies/vocabulary.service.ts b/src/app/core/submission/vocabularies/vocabulary.service.ts index cfb795c942..094f87544f 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.ts @@ -19,13 +19,13 @@ import { NotificationsService } from '../../../shared/notifications/notification import { ChangeAnalyzer } from '../../data/change-analyzer'; import { DefaultChangeAnalyzer } from '../../data/default-change-analyzer.service'; import { PaginatedList } from '../../data/paginated-list'; -import { RequestParam } from '../../cache/models/request-param.model'; import { Vocabulary } from './models/vocabulary.model'; import { VOCABULARY } from './models/vocabularies.resource-type'; import { VocabularyEntry } from './models/vocabulary-entry.model'; import { hasValue, isNotEmptyOperator } from '../../../shared/empty.util'; import { configureRequest, filterSuccessfulResponses, getRequestFromRequestHref } from '../../shared/operators'; import { GenericSuccessResponse } from '../../cache/response.models'; +import { VocabularyFindOptions } from './models/vocabulary-find-options.model'; /* tslint:disable:max-classes-per-file */ @@ -111,22 +111,13 @@ export class VocabularyService { /** * Return the {@link VocabularyEntry} list for a given {@link Vocabulary} * - * @param id The vocabulary id to retrieve the entries for - * @param metadata The metadata name - * @param collectionUUID The collection UUID - * @param options The {@link FindListOptions} for the request + * @param options The {@link VocabularyFindOptions} for the request * @return {Observable>>} * Return an observable that emits object list */ - getVocabularyEntries(id: string, metadata: string, collectionUUID: string, options: FindListOptions = {}): Observable>> { - options = Object.assign({}, options, { - searchParams: [ - new RequestParam('metadata', metadata), - new RequestParam('collection', collectionUUID) - ] - }); + getVocabularyEntries(options: VocabularyFindOptions): Observable>> { - return this.dataService.getFindAllHref(options, `${id}/entries`).pipe( + return this.dataService.getFindAllHref(options, `${options.name}/entries`).pipe( isNotEmptyOperator(), distinctUntilChanged(), getVocabularyEntriesFor(this.requestService, this.rdbService) @@ -136,18 +127,12 @@ export class VocabularyService { /** * Return the controlled {@link Vocabulary} configured for the specified metadata and collection if any. * - * @param metadata The metadata name - * @param collectionUUID The collection UUID + * @param options The {@link VocabularyFindOptions} for the request * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved * @return {Observable>>} * Return an observable that emits object list */ - searchByMetadataAndCollection(metadata: string, collectionUUID: string, ...linksToFollow: Array>): Observable> { - const options = new FindListOptions(); - options.searchParams = [ - new RequestParam('metadata', metadata), - new RequestParam('collection', collectionUUID) - ]; + searchByMetadataAndCollection(options: VocabularyFindOptions, ...linksToFollow: Array>): Observable> { return this.dataService.getSearchByHref(this.searchByMetadataAndCollectionMethod, options).pipe( first((href: string) => hasValue(href)), From 3117916d5b127da82e1740d8c8529dd7d549f37e Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 24 Jun 2020 19:00:13 +0200 Subject: [PATCH 04/62] [CST-3088] Added vocabularyEntryDetails's methods to VocabularyService --- .../models/vocabularies.resource-type.ts | 1 + .../models/vocabulary-entry-detail.model.ts | 33 ++ .../models/vocabulary-entry.model.ts | 4 +- .../models/vocabulary-options.model.ts | 35 ++ .../vocabularies/vocabulary.service.spec.ts | 419 ++++++++++++------ .../vocabularies/vocabulary.service.ts | 91 +++- 6 files changed, 433 insertions(+), 150 deletions(-) create mode 100644 src/app/core/submission/vocabularies/models/vocabulary-entry-detail.model.ts create mode 100644 src/app/core/submission/vocabularies/models/vocabulary-options.model.ts diff --git a/src/app/core/submission/vocabularies/models/vocabularies.resource-type.ts b/src/app/core/submission/vocabularies/models/vocabularies.resource-type.ts index 4d49abd823..5902fe4e17 100644 --- a/src/app/core/submission/vocabularies/models/vocabularies.resource-type.ts +++ b/src/app/core/submission/vocabularies/models/vocabularies.resource-type.ts @@ -9,3 +9,4 @@ import { ResourceType } from '../../../shared/resource-type'; export const VOCABULARY = new ResourceType('vocabulary'); export const VOCABULARY_ENTRY = new ResourceType('vocabularyEntry'); +export const VOCABULARY_ENTRY_DETAIL = new ResourceType('vocabularyEntryDetail'); diff --git a/src/app/core/submission/vocabularies/models/vocabulary-entry-detail.model.ts b/src/app/core/submission/vocabularies/models/vocabulary-entry-detail.model.ts new file mode 100644 index 0000000000..e8d0fc835a --- /dev/null +++ b/src/app/core/submission/vocabularies/models/vocabulary-entry-detail.model.ts @@ -0,0 +1,33 @@ +import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; + +import { HALLink } from '../../../shared/hal-link.model'; +import { VOCABULARY_ENTRY_DETAIL } from './vocabularies.resource-type'; +import { typedObject } from '../../../cache/builders/build-decorators'; +import { VocabularyEntry } from './vocabulary-entry.model'; + +/** + * Model class for a VocabularyEntryDetail + */ +@typedObject +@inheritSerialization(VocabularyEntry) +export class VocabularyEntryDetail extends VocabularyEntry { + static type = VOCABULARY_ENTRY_DETAIL; + + /** + * In an hierarchical vocabulary representing if entry is selectable as value + */ + @autoserialize + selectable: boolean; + + /** + * The {@link HALLink}s for this ExternalSourceEntry + */ + @deserialize + _links: { + self: HALLink; + vocabulary: HALLink; + parent: HALLink; + children + }; + +} diff --git a/src/app/core/submission/vocabularies/models/vocabulary-entry.model.ts b/src/app/core/submission/vocabularies/models/vocabulary-entry.model.ts index 5fe951c8aa..5ef61eba2e 100644 --- a/src/app/core/submission/vocabularies/models/vocabulary-entry.model.ts +++ b/src/app/core/submission/vocabularies/models/vocabulary-entry.model.ts @@ -11,7 +11,7 @@ import { ListableObject } from '../../../../shared/object-collection/shared/list import { GenericConstructor } from '../../../shared/generic-constructor'; /** - * Model class for a Vocabulary + * Model class for a VocabularyEntry */ @typedObject export class VocabularyEntry extends ListableObject { @@ -54,7 +54,7 @@ export class VocabularyEntry extends ListableObject { @deserialize _links: { self: HALLink; - vocabularyEntryDetail: HALLink; + vocabularyEntryDetail?: HALLink; }; /** diff --git a/src/app/core/submission/vocabularies/models/vocabulary-options.model.ts b/src/app/core/submission/vocabularies/models/vocabulary-options.model.ts new file mode 100644 index 0000000000..c28d8504bd --- /dev/null +++ b/src/app/core/submission/vocabularies/models/vocabulary-options.model.ts @@ -0,0 +1,35 @@ +/** + * Representing vocabulary properties + */ +export class VocabularyOptions { + + /** + * The name of the vocabulary + */ + name: string; + + /** + * The metadata field name (e.g. "dc.type") for which the vocabulary is used: + */ + metadata: string; + + /** + * The uuid of the collection where the item is being submitted + */ + scope: string; + + /** + * A boolean representing if value is closely related to a vocabulary entry or not + */ + closed: boolean; + + constructor(name: string, + metadata: string, + scope: string, + closed: boolean = false) { + this.name = name; + this.metadata = metadata; + this.scope = scope; + this.closed = closed; + } +} diff --git a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts index 5323c29f30..da496899d1 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts @@ -9,7 +9,7 @@ import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.s import { ObjectCacheService } from '../../cache/object-cache.service'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { RequestService } from '../../data/request.service'; -import { FindListOptions, VocabularyEntriesRequest } from '../../data/request.models'; +import { VocabularyEntriesRequest } from '../../data/request.models'; import { RequestParam } from '../../cache/models/request-param.model'; import { PageInfo } from '../../shared/page-info.model'; import { PaginatedList } from '../../data/paginated-list'; @@ -48,7 +48,7 @@ describe('VocabularyService', () => { } }; - const anotherVocabulary: any = { + const hierarchicalVocabulary: any = { id: 'srsc', name: 'srsc', scrollable: false, @@ -90,8 +90,55 @@ describe('VocabularyService', () => { } } }; + + const vocabularyEntryDetail: any = { + authority: 'authorityId1', + display: 'testValue1', + value: 'testValue1', + otherInformation: { + id: 'VR131402', + parent: 'Research Subject Categories::SOCIAL SCIENCES::Social sciences::Social work', + hasChildren: 'true', + note: 'Familjeforskning' + }, + type: 'vocabularyEntryDetail', + _links: { + self: { + href: 'https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:VR131402' + }, + parent: { + href: 'https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:parent' + }, + children: { + href: 'https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:children' + } + } + }; + + const anotherVocabularyEntryDetail: any = { + authority: 'authorityId1', + display: 'children', + value: 'children', + otherInformation: { + id: 'VR131402', + parent: 'Research Subject Categories::SOCIAL SCIENCES::Social sciences::Social work', + hasChildren: 'false', + note: 'Familjeforskning' + }, + type: 'vocabularyEntryDetail', + _links: { + self: { + href: 'https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:children' + }, + parent: { + href: 'https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:testValue1' + } + } + }; + const endpointURL = `https://rest.api/rest/api/submission/vocabularies`; const requestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}`; + const entryDetailRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:SCB110/${hierarchicalVocabulary.id}:testValue`; const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; const vocabularyId = 'types'; const metadata = 'dc.type'; @@ -101,10 +148,14 @@ describe('VocabularyService', () => { const entriesRequestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}/entries?metadata=${metadata}&collection=${collectionUUID}`; const pageInfo = new PageInfo(); - const array = [vocabulary, anotherVocabulary]; + const array = [vocabulary, hierarchicalVocabulary]; + const arrayEntries = [vocabularyEntryDetail, anotherVocabularyEntryDetail]; const paginatedList = new PaginatedList(pageInfo, array); + const entriesPaginatedList = new PaginatedList(pageInfo, arrayEntries); const vocabularyRD = createSuccessfulRemoteDataObject(vocabulary); + const vocabularyEntryDetailRD = createSuccessfulRemoteDataObject(vocabularyEntryDetail); const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); + const entriesPaginatedListRD = createSuccessfulRemoteDataObject(entriesPaginatedList); const getRequestEntry$ = (successful: boolean) => { return observableOf({ response: { isSuccessful: successful, payload: vocabulary } as any @@ -114,6 +165,7 @@ describe('VocabularyService', () => { const notificationsService = {} as NotificationsService; const http = {} as HttpClient; const comparator = {} as any; + const comparatorEntry = {} as any; function initTestService() { return new VocabularyService( @@ -123,149 +175,248 @@ describe('VocabularyService', () => { halService, notificationsService, http, - comparator + comparator, + comparatorEntry ); } - beforeEach(() => { - scheduler = getTestScheduler(); - - halService = jasmine.createSpyObj('halService', { - getEndpoint: cold('a', { a: endpointURL }) - }); - - responseCacheEntry = new RequestEntry(); - responseCacheEntry.response = new RestResponse(true, 200, 'Success'); - - requestService = jasmine.createSpyObj('requestService', { - generateRequestId: requestUUID, - configure: true, - removeByHrefSubstring: {}, - getByHref: observableOf(responseCacheEntry), - getByUUID: observableOf(responseCacheEntry), - }); - rdbService = jasmine.createSpyObj('rdbService', { - buildSingle: hot('a|', { - a: vocabularyRD - }), - buildList: hot('a|', { - a: paginatedListRD - }), - }); - - service = initTestService(); - - spyOn((service as any).dataService, 'findById').and.callThrough(); - spyOn((service as any).dataService, 'findAll').and.callThrough(); - spyOn((service as any).dataService, 'findByHref').and.callThrough(); - spyOn((service as any).dataService, 'searchBy').and.callThrough(); - spyOn((service as any).dataService, 'getSearchByHref').and.returnValue(observableOf(searchRequestURL)); - spyOn((service as any).dataService, 'getFindAllHref').and.returnValue(observableOf(entriesRequestURL)); - }); - - afterEach(() => { - service = null; - }); - - describe('findById', () => { - it('should proxy the call to dataservice.findById', () => { - scheduler.schedule(() => service.findById(vocabularyId)); - scheduler.flush(); - - expect((service as any).dataService.findById).toHaveBeenCalledWith(vocabularyId); - }); - - it('should return a RemoteData for the object with the given id', () => { - const result = service.findById(vocabularyId); - const expected = cold('a|', { - a: vocabularyRD - }); - expect(result).toBeObservable(expected); - }); - }); - - describe('findByHref', () => { - it('should proxy the call to dataservice.findByHref', () => { - scheduler.schedule(() => service.findByHref(requestURL)); - scheduler.flush(); - - expect((service as any).dataService.findByHref).toHaveBeenCalledWith(requestURL); - }); - - it('should return a RemoteData for the object with the given URL', () => { - const result = service.findByHref(requestURL); - const expected = cold('a|', { - a: vocabularyRD - }); - expect(result).toBeObservable(expected); - }); - }); - - describe('findAll', () => { - it('should proxy the call to dataservice.findAll', () => { - scheduler.schedule(() => service.findAll()); - scheduler.flush(); - - expect((service as any).dataService.findAll).toHaveBeenCalled(); - }); - - it('should return a RemoteData>', () => { - const result = service.findAll(); - const expected = cold('a|', { - a: paginatedListRD - }); - expect(result).toBeObservable(expected); - }); - }); - - describe('searchByMetadataAndCollection', () => { - it('should proxy the call to dataservice.findByHref', () => { - const options = new FindListOptions(); - options.searchParams = [ - new RequestParam('metadata', metadata), - new RequestParam('collection', collectionUUID) - ]; - scheduler.schedule(() => service.searchByMetadataAndCollection(vocabularyOptions).subscribe()); - scheduler.flush(); - - expect((service as any).dataService.findByHref).toHaveBeenCalledWith(searchRequestURL); - }); - - it('should return a RemoteData for the search', () => { - const result = service.searchByMetadataAndCollection(vocabularyOptions); - const expected = cold('a|', { - a: vocabularyRD - }); - expect(result).toBeObservable(expected); - }); - - }); - - describe('getVocabularyEntries', () => { - + describe('vocabularies endpoint', () => { beforeEach(() => { - requestService = getMockRequestService(getRequestEntry$(true)); - rdbService = getMockRemoteDataBuildService(); - spyOn(rdbService, 'toRemoteDataObservable').and.callThrough(); + scheduler = getTestScheduler(); + + halService = jasmine.createSpyObj('halService', { + getEndpoint: cold('a', { a: endpointURL }) + }); + + responseCacheEntry = new RequestEntry(); + responseCacheEntry.response = new RestResponse(true, 200, 'Success'); + + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: requestUUID, + configure: true, + removeByHrefSubstring: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: observableOf(responseCacheEntry), + }); + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: hot('a|', { + a: vocabularyRD + }), + buildList: hot('a|', { + a: paginatedListRD + }), + }); + service = initTestService(); + + spyOn((service as any).vocabularyDataService, 'findById').and.callThrough(); + spyOn((service as any).vocabularyDataService, 'findAll').and.callThrough(); + spyOn((service as any).vocabularyDataService, 'findByHref').and.callThrough(); + spyOn((service as any).vocabularyDataService, 'searchBy').and.callThrough(); + spyOn((service as any).vocabularyDataService, 'getSearchByHref').and.returnValue(observableOf(searchRequestURL)); + spyOn((service as any).vocabularyDataService, 'getFindAllHref').and.returnValue(observableOf(entriesRequestURL)); }); - it('should configure a new VocabularyEntriesRequest', () => { - const expected = new VocabularyEntriesRequest(requestService.generateRequestId(), entriesRequestURL); - - scheduler.schedule(() => service.getVocabularyEntries(vocabularyOptions).subscribe()); - scheduler.flush(); - - expect(requestService.configure).toHaveBeenCalledWith(expected); + afterEach(() => { + service = null; }); - it('should call RemoteDataBuildService to create the RemoteData Observable', () => { - service.getVocabularyEntries(vocabularyOptions); + describe('findVocabularyById', () => { + it('should proxy the call to vocabularyDataService.findVocabularyById', () => { + scheduler.schedule(() => service.findVocabularyById(vocabularyId)); + scheduler.flush(); - expect(rdbService.toRemoteDataObservable).toHaveBeenCalled(); + expect((service as any).vocabularyDataService.findById).toHaveBeenCalledWith(vocabularyId); + }); + + it('should return a RemoteData for the object with the given id', () => { + const result = service.findVocabularyById(vocabularyId); + const expected = cold('a|', { + a: vocabularyRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('findVocabularyByHref', () => { + it('should proxy the call to vocabularyDataService.findVocabularyByHref', () => { + scheduler.schedule(() => service.findVocabularyByHref(requestURL)); + scheduler.flush(); + + expect((service as any).vocabularyDataService.findByHref).toHaveBeenCalledWith(requestURL); + }); + + it('should return a RemoteData for the object with the given URL', () => { + const result = service.findVocabularyByHref(requestURL); + const expected = cold('a|', { + a: vocabularyRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('findAllVocabularies', () => { + it('should proxy the call to vocabularyDataService.findAllVocabularies', () => { + scheduler.schedule(() => service.findAllVocabularies()); + scheduler.flush(); + + expect((service as any).vocabularyDataService.findAll).toHaveBeenCalled(); + }); + + it('should return a RemoteData>', () => { + const result = service.findAllVocabularies(); + const expected = cold('a|', { + a: paginatedListRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('searchVocabularyByMetadataAndCollection', () => { + it('should proxy the call to vocabularyDataService.findVocabularyByHref', () => { + const options = new VocabularyFindOptions(); + options.searchParams = [ + new RequestParam('metadata', metadata), + new RequestParam('collection', collectionUUID) + ]; + scheduler.schedule(() => service.searchVocabularyByMetadataAndCollection(vocabularyOptions).subscribe()); + scheduler.flush(); + + expect((service as any).vocabularyDataService.findByHref).toHaveBeenCalledWith(searchRequestURL); + }); + + it('should return a RemoteData for the search', () => { + const result = service.searchVocabularyByMetadataAndCollection(vocabularyOptions); + const expected = cold('a|', { + a: vocabularyRD + }); + expect(result).toBeObservable(expected); + }); + + }); + + describe('getVocabularyEntries', () => { + + beforeEach(() => { + requestService = getMockRequestService(getRequestEntry$(true)); + rdbService = getMockRemoteDataBuildService(); + spyOn(rdbService, 'toRemoteDataObservable').and.callThrough(); + service = initTestService(); + }); + + it('should configure a new VocabularyEntriesRequest', () => { + const expected = new VocabularyEntriesRequest(requestService.generateRequestId(), entriesRequestURL); + + scheduler.schedule(() => service.getVocabularyEntries(vocabularyOptions).subscribe()); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + + it('should call RemoteDataBuildService to create the RemoteData Observable', () => { + service.getVocabularyEntries(vocabularyOptions); + + expect(rdbService.toRemoteDataObservable).toHaveBeenCalled(); + + }); + + }); + }); + + describe('vocabularyEntryDetails endpoint', () => { + beforeEach(() => { + scheduler = getTestScheduler(); + + halService = jasmine.createSpyObj('halService', { + getEndpoint: cold('a', { a: endpointURL }) + }); + + responseCacheEntry = new RequestEntry(); + responseCacheEntry.response = new RestResponse(true, 200, 'Success'); + + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: requestUUID, + configure: true, + removeByHrefSubstring: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: observableOf(responseCacheEntry), + }); + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: hot('a|', { + a: vocabularyEntryDetailRD + }), + buildList: hot('a|', { + a: entriesPaginatedListRD + }), + }); + + service = initTestService(); + + 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, 'searchBy').and.callThrough(); + spyOn((service as any).vocabularyEntryDetailDataService, 'getSearchByHref').and.returnValue(observableOf(searchRequestURL)); + spyOn((service as any).vocabularyEntryDetailDataService, 'getFindAllHref').and.returnValue(observableOf(entriesRequestURL)); + }); + + afterEach(() => { + service = null; + }); + + describe('findEntryDetailByHref', () => { + it('should proxy the call to vocabularyDataService.findVocabularyByHref', () => { + scheduler.schedule(() => service.findEntryDetailByHref(entryDetailRequestURL)); + scheduler.flush(); + + expect((service as any).vocabularyEntryDetailDataService.findByHref).toHaveBeenCalledWith(entryDetailRequestURL); + }); + + it('should return a RemoteData for the object with the given URL', () => { + const result = service.findEntryDetailByHref(entryDetailRequestURL); + const expected = cold('a|', { + a: vocabularyEntryDetailRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('findEntryDetailByValue', () => { + it('should proxy the call to vocabularyDataService.findVocabularyById', () => { + scheduler.schedule(() => service.findEntryDetailByValue('testValue', hierarchicalVocabulary.id)); + scheduler.flush(); + const expectedId = `${hierarchicalVocabulary.id}:testValue` + expect((service as any).vocabularyEntryDetailDataService.findById).toHaveBeenCalledWith(expectedId); + }); + + it('should return a RemoteData for the object with the given id', () => { + const result = service.findEntryDetailByValue('testValue', hierarchicalVocabulary.id); + const expected = cold('a|', { + a: vocabularyEntryDetailRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('searchByGroup', () => { + it('should proxy the call to vocabularyEntryDetailDataService.searchBy', () => { + const options = new VocabularyFindOptions(); + options.searchParams.push(new RequestParam('vocabulary', 'srsc')); + scheduler.schedule(() => service.searchTopEntries('srsc', new VocabularyFindOptions())); + scheduler.flush(); + + expect((service as any).vocabularyEntryDetailDataService.searchBy).toHaveBeenCalledWith((service as any).searchTopMethod, options); + }); + + it('should return a RemoteData) for the search', () => { + const result = service.searchTopEntries('srsc', new VocabularyFindOptions()); + const expected = cold('a|', { + a: entriesPaginatedListRD + }); + expect(result).toBeObservable(expected); + }); }); }); - }); diff --git a/src/app/core/submission/vocabularies/vocabulary.service.ts b/src/app/core/submission/vocabularies/vocabulary.service.ts index 094f87544f..13929525d4 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.ts @@ -26,13 +26,15 @@ import { hasValue, isNotEmptyOperator } from '../../../shared/empty.util'; import { configureRequest, filterSuccessfulResponses, getRequestFromRequestHref } from '../../shared/operators'; import { GenericSuccessResponse } from '../../cache/response.models'; import { VocabularyFindOptions } from './models/vocabulary-find-options.model'; +import { VocabularyEntryDetail } from './models/vocabulary-entry-detail.model'; +import { RequestParam } from '../../cache/models/request-param.model'; /* tslint:disable:max-classes-per-file */ /** * A private DataService implementation to delegate specific methods to. */ -class DataServiceImpl extends DataService { +class VocabularyDataServiceImpl extends DataService { protected linkPath = 'vocabularies'; constructor( @@ -49,6 +51,26 @@ class DataServiceImpl extends DataService { } +/** + * A private DataService implementation to delegate specific methods to. + */ +class VocabularyEntryDetailDataServiceImpl extends DataService { + protected linkPath = 'vocabularyEntryDetails'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: ChangeAnalyzer) { + super(); + } + +} + /** * A service responsible for fetching/sending data from/to the REST API on the vocabularies endpoint */ @@ -58,7 +80,9 @@ class DataServiceImpl extends DataService { @dataService(VOCABULARY) export class VocabularyService { protected searchByMetadataAndCollectionMethod = 'byMetadataAndCollection'; - private dataService: DataServiceImpl; + protected searchTopMethod = 'top'; + private vocabularyDataService: VocabularyDataServiceImpl; + private vocabularyEntryDetailDataService: VocabularyEntryDetailDataServiceImpl; constructor( protected requestService: RequestService, @@ -67,8 +91,10 @@ export class VocabularyService { protected halService: HALEndpointService, protected notificationsService: NotificationsService, protected http: HttpClient, - protected comparator: DefaultChangeAnalyzer) { - this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator); + protected comparatorVocabulary: DefaultChangeAnalyzer, + protected comparatorEntry: DefaultChangeAnalyzer) { + this.vocabularyDataService = new VocabularyDataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparatorVocabulary); + this.vocabularyEntryDetailDataService = new VocabularyEntryDetailDataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparatorEntry); } /** @@ -79,8 +105,8 @@ export class VocabularyService { * @return {Observable>} * Return an observable that emits vocabulary object */ - findByHref(href: string, ...linksToFollow: Array>): Observable> { - return this.dataService.findByHref(href, ...linksToFollow); + findVocabularyByHref(href: string, ...linksToFollow: Array>): Observable> { + return this.vocabularyDataService.findByHref(href, ...linksToFollow); } /** @@ -91,8 +117,8 @@ export class VocabularyService { * @return {Observable>} * Return an observable that emits vocabulary object */ - findById(id: string, ...linksToFollow: Array>): Observable> { - return this.dataService.findById(id, ...linksToFollow); + findVocabularyById(id: string, ...linksToFollow: Array>): Observable> { + return this.vocabularyDataService.findById(id, ...linksToFollow); } /** @@ -104,8 +130,8 @@ export class VocabularyService { * @return {Observable>>} * Return an observable that emits object list */ - findAll(options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { - return this.dataService.findAll(options, ...linksToFollow); + findAllVocabularies(options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + return this.vocabularyDataService.findAll(options, ...linksToFollow); } /** @@ -117,7 +143,7 @@ export class VocabularyService { */ getVocabularyEntries(options: VocabularyFindOptions): Observable>> { - return this.dataService.getFindAllHref(options, `${options.name}/entries`).pipe( + return this.vocabularyDataService.getFindAllHref(options, `${options.name}/entries`).pipe( isNotEmptyOperator(), distinctUntilChanged(), getVocabularyEntriesFor(this.requestService, this.rdbService) @@ -132,14 +158,51 @@ export class VocabularyService { * @return {Observable>>} * Return an observable that emits object list */ - searchByMetadataAndCollection(options: VocabularyFindOptions, ...linksToFollow: Array>): Observable> { + searchVocabularyByMetadataAndCollection(options: VocabularyFindOptions, ...linksToFollow: Array>): Observable> { - return this.dataService.getSearchByHref(this.searchByMetadataAndCollectionMethod, options).pipe( + return this.vocabularyDataService.getSearchByHref(this.searchByMetadataAndCollectionMethod, options).pipe( first((href: string) => hasValue(href)), - flatMap((href: string) => this.dataService.findByHref(href)) + flatMap((href: string) => this.vocabularyDataService.findByHref(href)) ) } + /** + * Returns an observable of {@link RemoteData} of a {@link VocabularyEntryDetail}, based on an href, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the {@link VocabularyEntryDetail} + * @param href The url of {@link VocabularyEntryDetail} we want to retrieve + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @return {Observable>} + * Return an observable that emits vocabulary object + */ + findEntryDetailByHref(href: string, ...linksToFollow: Array>): Observable> { + return this.vocabularyEntryDetailDataService.findByHref(href, ...linksToFollow); + } + + /** + * Returns an observable of {@link RemoteData} of a {@link VocabularyEntryDetail}, based on its ID, with a list of {@link FollowLinkConfig}, + * to automatically resolve {@link HALLink}s of the object + * @param value The entry value for which to provide detailed information. + * @param name The name of {@link Vocabulary} to which the entry belongs + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @return {Observable>} + * Return an observable that emits VocabularyEntryDetail object + */ + findEntryDetailByValue(value: string, name: string, ...linksToFollow: Array>): Observable> { + const id = `${name}:${value}`; + return this.vocabularyEntryDetailDataService.findById(id, ...linksToFollow); + } + + /** + * Return the top level {@link VocabularyEntryDetail} list for a given hierarchical vocabulary + * + * @param name The name of hierarchical {@link Vocabulary} to which the entries belongs + * @param options The {@link VocabularyFindOptions} for the request + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + searchTopEntries(name: string, options: VocabularyFindOptions, ...linksToFollow: Array>): Observable>> { + options.searchParams.push(new RequestParam('vocabulary', name)); + return this.vocabularyEntryDetailDataService.searchBy(this.searchTopMethod, options, ...linksToFollow) + } } /** From 63cca76b49a5e5ed21cb7064904b01043910a189 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 25 Jun 2020 15:21:44 +0200 Subject: [PATCH 05/62] [CST-3088] Replace use of AuthorityService with new VocabularyService --- src/app/core/core.module.ts | 12 +- .../builder/json-patch-operations-builder.ts | 8 +- .../models => shared}/confidence-type.ts | 0 .../vocabularies/vocabulary.service.ts | 4 +- .../authority-confidence-state.directive.ts | 11 +- src/app/shared/chips/chips.component.spec.ts | 2 +- .../shared/chips/models/chips-item.model.ts | 2 +- src/app/shared/chips/models/chips.model.ts | 4 +- ...c-form-control-container.component.spec.ts | 10 +- .../date-picker/date-picker.component.spec.ts | 10 +- .../date-picker/date-picker.component.ts | 6 +- .../models/ds-dynamic-input.model.ts | 12 +- .../list/dynamic-list-checkbox-group.model.ts | 30 +- .../list/dynamic-list-radio-group.model.ts | 10 +- .../list/dynamic-list.component.spec.ts | 84 ++--- .../models/list/dynamic-list.component.ts | 59 ++-- .../lookup/dynamic-lookup.component.spec.ts | 71 ++-- .../models/lookup/dynamic-lookup.component.ts | 312 +++++++++--------- .../dynamic-relation-group.component.spec.ts | 17 +- .../dynamic-relation-group.components.ts | 52 ++- ...amic-scrollable-dropdown.component.spec.ts | 38 +-- .../dynamic-scrollable-dropdown.component.ts | 68 ++-- .../dynamic-scrollable-dropdown.model.ts | 6 +- .../models/tag/dynamic-tag.component.spec.ts | 33 +- .../models/tag/dynamic-tag.component.ts | 65 ++-- .../dynamic-typeahead.component.spec.ts | 38 +-- .../typeahead/dynamic-typeahead.component.ts | 61 ++-- .../form/builder/form-builder.service.spec.ts | 18 +- .../models/form-field-metadata-value.model.ts | 10 +- .../form/builder/models/form-field.model.ts | 75 ++++- .../builder/parsers/date-field-parser.spec.ts | 2 +- .../parsers/disabled-field-parser.spec.ts | 2 +- .../parsers/dropdown-field-parser.spec.ts | 6 +- .../builder/parsers/dropdown-field-parser.ts | 6 +- .../form/builder/parsers/field-parser.ts | 113 +++---- .../builder/parsers/list-field-parser.spec.ts | 4 +- .../form/builder/parsers/list-field-parser.ts | 10 +- .../parsers/lookup-field-parser.spec.ts | 4 +- .../builder/parsers/lookup-field-parser.ts | 4 +- .../parsers/lookup-name-field-parser.spec.ts | 4 +- .../parsers/lookup-name-field-parser.ts | 4 +- .../builder/parsers/name-field-parser.spec.ts | 2 +- .../parsers/onebox-field-parser.spec.ts | 4 +- .../builder/parsers/onebox-field-parser.ts | 4 +- .../form/builder/parsers/parser-options.ts | 2 +- .../relation-group-field-parser.spec.ts | 2 +- .../parsers/relation-group-field-parser.ts | 2 +- .../form/builder/parsers/row-parser.spec.ts | 16 +- .../shared/form/builder/parsers/row-parser.ts | 6 +- .../parsers/series-field-parser.spec.ts | 2 +- .../builder/parsers/tag-field-parser.spec.ts | 4 +- .../form/builder/parsers/tag-field-parser.ts | 6 +- .../parsers/textarea-field-parser.spec.ts | 2 +- src/app/shared/mocks/form-models.mock.ts | 37 ++- src/app/shared/mocks/submission.mock.ts | 4 +- .../shared/testing/authority-service.stub.ts | 21 -- .../shared/testing/vocabulary-service.stub.ts | 29 ++ .../section-form-operations.service.spec.ts | 6 +- .../form/section-form-operations.service.ts | 10 +- 59 files changed, 740 insertions(+), 706 deletions(-) rename src/app/core/{integration/models => shared}/confidence-type.ts (100%) delete mode 100644 src/app/shared/testing/authority-service.stub.ts create mode 100644 src/app/shared/testing/vocabulary-service.stub.ts diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index a92b52b339..704f6c9e62 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -86,9 +86,6 @@ import { EPersonDataService } from './eperson/eperson-data.service'; import { EpersonResponseParsingService } from './eperson/eperson-response-parsing.service'; import { EPerson } from './eperson/models/eperson.model'; import { Group } from './eperson/models/group.model'; -import { AuthorityService } from './integration/authority.service'; -import { IntegrationResponseParsingService } from './integration/integration-response-parsing.service'; -import { AuthorityValue } from './integration/models/authority.value'; import { JsonPatchOperationsBuilder } from './json-patch/builder/json-patch-operations-builder'; import { MetadataField } from './metadata/metadata-field.model'; import { MetadataSchema } from './metadata/metadata-schema.model'; @@ -148,6 +145,8 @@ import { WorkflowAction } from './tasks/models/workflow-action-object.model'; import { VocabularyEntry } from './submission/vocabularies/models/vocabulary-entry.model'; 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'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -224,8 +223,6 @@ const PROVIDERS = [ SubmissionResponseParsingService, SubmissionJsonPatchOperationsService, JsonPatchOperationsBuilder, - AuthorityService, - IntegrationResponseParsingService, MetadataschemaParsingService, MetadatafieldParsingService, UploaderService, @@ -276,6 +273,7 @@ const PROVIDERS = [ NotificationsService, FilteredDiscoveryPageResponseParsingService, { provide: NativeWindowService, useFactory: NativeWindowFactory }, + VocabularyService, VocabularyEntriesResponseParsingService ]; @@ -305,7 +303,6 @@ export const models = SubmissionSectionModel, SubmissionUploadsModel, AuthStatus, - AuthorityValue, BrowseEntry, BrowseDefinition, ClaimedTask, @@ -320,7 +317,8 @@ export const models = VersionHistory, WorkflowAction, Vocabulary, - VocabularyEntry + VocabularyEntry, + VocabularyEntryDetail ]; @NgModule({ diff --git a/src/app/core/json-patch/builder/json-patch-operations-builder.ts b/src/app/core/json-patch/builder/json-patch-operations-builder.ts index c45183b4ef..36c82db283 100644 --- a/src/app/core/json-patch/builder/json-patch-operations-builder.ts +++ b/src/app/core/json-patch/builder/json-patch-operations-builder.ts @@ -9,7 +9,7 @@ import { JsonPatchOperationPathObject } from './json-patch-operation-path-combin import { Injectable } from '@angular/core'; import { isEmpty, isNotEmpty } from '../../../shared/empty.util'; import { dateToISOFormat } from '../../../shared/date.util'; -import { AuthorityValue } from '../../integration/models/authority.value'; +import { VocabularyEntry } from '../../submission/vocabularies/models/vocabulary-entry.model'; import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; import { FormFieldLanguageValueObject } from '../../../shared/form/builder/models/form-field-language-value.model'; @@ -106,7 +106,7 @@ export class JsonPatchOperationsBuilder { operationValue = value; } else if (value instanceof Date) { operationValue = new FormFieldMetadataValueObject(dateToISOFormat(value)); - } else if (value instanceof AuthorityValue) { + } else if (value instanceof VocabularyEntry) { operationValue = this.prepareAuthorityValue(value); } else if (value instanceof FormFieldLanguageValueObject) { operationValue = new FormFieldMetadataValueObject(value.value, value.language); @@ -127,8 +127,8 @@ export class JsonPatchOperationsBuilder { protected prepareAuthorityValue(value: any) { let operationValue: any = null; - if (isNotEmpty(value.id)) { - operationValue = new FormFieldMetadataValueObject(value.value, value.language, value.id); + if (isNotEmpty(value.authority)) { + operationValue = new FormFieldMetadataValueObject(value.value, value.language, value.authority); } else { operationValue = new FormFieldMetadataValueObject(value.value, value.language); } diff --git a/src/app/core/integration/models/confidence-type.ts b/src/app/core/shared/confidence-type.ts similarity index 100% rename from src/app/core/integration/models/confidence-type.ts rename to src/app/core/shared/confidence-type.ts diff --git a/src/app/core/submission/vocabularies/vocabulary.service.ts b/src/app/core/submission/vocabularies/vocabulary.service.ts index 13929525d4..bd5c5b6c48 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.ts @@ -74,9 +74,7 @@ class VocabularyEntryDetailDataServiceImpl extends DataService; @@ -102,7 +102,7 @@ export class Chips { private getChipsIcons(item) { const icons = []; - if (typeof item === 'string' || item instanceof FormFieldMetadataValueObject || item instanceof AuthorityValue) { + if (typeof item === 'string' || item instanceof FormFieldMetadataValueObject || item instanceof VocabularyEntry) { return icons; } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts index 4dee6905d2..171ad69f64 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts @@ -44,7 +44,7 @@ import { SharedModule } from '../../../shared.module'; import { DynamicDsDatePickerModel } from './models/date-picker/date-picker.model'; import { DynamicRelationGroupModel } from './models/relation-group/dynamic-relation-group.model'; import { DynamicListCheckboxGroupModel } from './models/list/dynamic-list-checkbox-group.model'; -import { AuthorityOptions } from '../../../../core/integration/models/authority-options.model'; +import { VocabularyOptions } from '../../../../core/submission/vocabularies/models/vocabulary-options.model'; import { DynamicListRadioGroupModel } from './models/list/dynamic-list-radio-group.model'; import { DynamicLookupModel } from './models/lookup/dynamic-lookup.model'; import { DynamicScrollableDropdownModel } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; @@ -74,7 +74,7 @@ import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; describe('DsDynamicFormControlContainerComponent test suite', () => { - const authorityOptions: AuthorityOptions = { + const vocabularyOptions: VocabularyOptions = { closed: false, metadata: 'list', name: 'type_programme', @@ -104,7 +104,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { new DynamicTypeaheadModel({ id: 'typeahead', metadataFields: [], repeatable: false, submissionId: '1234' }), new DynamicScrollableDropdownModel({ id: 'scrollableDropdown', - authorityOptions: authorityOptions, + vocabularyOptions: vocabularyOptions, metadataFields: [], repeatable: false, submissionId: '1234' @@ -112,12 +112,12 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { new DynamicTagModel({ id: 'tag', metadataFields: [], repeatable: false, submissionId: '1234' }), new DynamicListCheckboxGroupModel({ id: 'checkboxList', - authorityOptions: authorityOptions, + vocabularyOptions: vocabularyOptions, repeatable: true }), new DynamicListRadioGroupModel({ id: 'radioList', - authorityOptions: authorityOptions, + vocabularyOptions: vocabularyOptions, repeatable: false }), new DynamicRelationGroupModel({ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts index 78c2d5d217..e08116e5fa 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts @@ -8,10 +8,6 @@ import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dyna import { DsDatePickerComponent } from './date-picker.component'; import { DynamicDsDatePickerModel } from './date-picker.model'; -import { FormBuilderService } from '../../../form-builder.service'; - -import { FormComponent } from '../../../../form.component'; -import { FormService } from '../../../../form.service'; import { createTestComponent } from '../../../../../testing/utils.test'; export const DATE_TEST_GROUP = new FormGroup({ @@ -20,7 +16,7 @@ export const DATE_TEST_GROUP = new FormGroup({ export const DATE_TEST_MODEL_CONFIG = { disabled: false, - errorMessages: {required: 'You must enter at least the year.'}, + errorMessages: { required: 'You must enter at least the year.' }, id: 'date', label: 'Date', name: 'date', @@ -52,8 +48,8 @@ describe('DsDatePickerComponent test suite', () => { providers: [ ChangeDetectorRef, DsDatePickerComponent, - {provide: DynamicFormLayoutService, useValue: {}}, - {provide: DynamicFormValidationService, useValue: {}} + { provide: DynamicFormLayoutService, useValue: {} }, + { provide: DynamicFormValidationService, useValue: {} } ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts index 2e22f314ed..8523c60cf0 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts @@ -20,10 +20,6 @@ export class DsDatePickerComponent extends DynamicFormControlComponent implement @Input() bindId = true; @Input() group: FormGroup; @Input() model: DynamicDsDatePickerModel; - // @Input() - // minDate; - // @Input() - // maxDate; @Output() selected = new EventEmitter(); @Output() remove = new EventEmitter(); @@ -65,7 +61,7 @@ export class DsDatePickerComponent extends DynamicFormControlComponent implement this.initialMonth = now.getMonth() + 1; this.initialDay = now.getDate(); - if (this.model.value && this.model.value !== null) { + if (this.model && this.model.value !== null) { const values = this.model.value.toString().split(DS_DATE_PICKER_SEPARATOR); if (values.length > 0) { this.initialYear = parseInt(values[0], 10); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts index 3827df7be6..b723f7aa40 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts @@ -2,13 +2,13 @@ import { DynamicFormControlLayout, DynamicInputModel, DynamicInputModelConfig, s import { Subject } from 'rxjs'; import { LanguageCode } from '../../models/form-field-language-value.model'; -import { AuthorityOptions } from '../../../../../core/integration/models/authority-options.model'; +import { VocabularyOptions } from '../../../../../core/submission/vocabularies/models/vocabulary-options.model'; import { hasValue } from '../../../../empty.util'; import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model'; import { RelationshipOptions } from '../../models/relationship-options.model'; export interface DsDynamicInputModelConfig extends DynamicInputModelConfig { - authorityOptions?: AuthorityOptions; + vocabularyOptions?: VocabularyOptions; languageCodes?: LanguageCode[]; language?: string; value?: any; @@ -20,7 +20,7 @@ export interface DsDynamicInputModelConfig extends DynamicInputModelConfig { export class DsDynamicInputModel extends DynamicInputModel { - @serializable() authorityOptions: AuthorityOptions; + @serializable() vocabularyOptions: VocabularyOptions; @serializable() private _languageCodes: LanguageCode[]; @serializable() private _language: string; @serializable() languageUpdates: Subject; @@ -58,11 +58,11 @@ export class DsDynamicInputModel extends DynamicInputModel { this.language = lang; }); - this.authorityOptions = config.authorityOptions; + this.vocabularyOptions = config.vocabularyOptions; } get hasAuthority(): boolean { - return this.authorityOptions && hasValue(this.authorityOptions.name); + return this.vocabularyOptions && hasValue(this.vocabularyOptions.name); } get hasLanguages(): boolean { @@ -83,7 +83,7 @@ export class DsDynamicInputModel extends DynamicInputModel { set languageCodes(languageCodes: LanguageCode[]) { this._languageCodes = languageCodes; - if (!this.language || this.language === null || this.language === '') { + if (!this.language || this.language === '') { this.language = this.languageCodes ? this.languageCodes[0].code : null; } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts index f6b58c1504..fab371483c 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts @@ -1,16 +1,17 @@ import { Subject } from 'rxjs'; - import { - DynamicCheckboxGroupModel, DynamicFormControlLayout, + DynamicCheckboxGroupModel, + DynamicFormControlLayout, DynamicFormGroupModelConfig, serializable } from '@ng-dynamic-forms/core'; -import { AuthorityValue } from '../../../../../../core/integration/models/authority.value'; -import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model'; + +import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; +import { VocabularyOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-options.model'; import { hasValue } from '../../../../../empty.util'; export interface DynamicListCheckboxGroupModelConfig extends DynamicFormGroupModelConfig { - authorityOptions: AuthorityOptions; + vocabularyOptions: VocabularyOptions; groupLength?: number; repeatable: boolean; value?: any; @@ -18,43 +19,44 @@ export interface DynamicListCheckboxGroupModelConfig extends DynamicFormGroupMod export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel { - @serializable() authorityOptions: AuthorityOptions; + @serializable() vocabularyOptions: VocabularyOptions; @serializable() repeatable: boolean; @serializable() groupLength: number; - @serializable() _value: AuthorityValue[]; isListGroup = true; valueUpdates: Subject; constructor(config: DynamicListCheckboxGroupModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); - this.authorityOptions = config.authorityOptions; + this.vocabularyOptions = config.vocabularyOptions; this.groupLength = config.groupLength || 5; this._value = []; this.repeatable = config.repeatable; this.valueUpdates = new Subject(); - this.valueUpdates.subscribe((value: AuthorityValue | AuthorityValue[]) => this.value = value); + this.valueUpdates.subscribe((value: VocabularyEntry | VocabularyEntry[]) => this.value = value); this.valueUpdates.next(config.value); } - get hasAuthority(): boolean { - return this.authorityOptions && hasValue(this.authorityOptions.name); - } + @serializable() _value: VocabularyEntry[]; get value() { return this._value; } - set value(value: AuthorityValue | AuthorityValue[]) { + set value(value: VocabularyEntry | VocabularyEntry[]) { if (value) { if (Array.isArray(value)) { this._value = value; } else { // _value is non extendible so assign it a new array - const newValue = (this.value as AuthorityValue[]).concat([value]); + const newValue = (this.value as VocabularyEntry[]).concat([value]); this._value = newValue } } } + + get hasAuthority(): boolean { + return this.vocabularyOptions && hasValue(this.vocabularyOptions.name); + } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model.ts index 287c10f3fe..a4a0fb7749 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model.ts @@ -4,11 +4,11 @@ import { DynamicRadioGroupModelConfig, serializable } from '@ng-dynamic-forms/core'; -import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model'; +import { VocabularyOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-options.model'; import { hasValue } from '../../../../../empty.util'; export interface DynamicListModelConfig extends DynamicRadioGroupModelConfig { - authorityOptions: AuthorityOptions; + vocabularyOptions: VocabularyOptions; groupLength?: number; repeatable: boolean; value?: any; @@ -16,7 +16,7 @@ export interface DynamicListModelConfig extends DynamicRadioGroupModelConfig { - @serializable() authorityOptions: AuthorityOptions; + @serializable() vocabularyOptions: VocabularyOptions; @serializable() repeatable: boolean; @serializable() groupLength: number; isListGroup = true; @@ -24,13 +24,13 @@ export class DynamicListRadioGroupModel extends DynamicRadioGroupModel { constructor(config: DynamicListModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); - this.authorityOptions = config.authorityOptions; + this.vocabularyOptions = config.vocabularyOptions; this.groupLength = config.groupLength || 5; this.repeatable = config.repeatable; this.valueUpdates.next(config.value); } get hasAuthority(): boolean { - return this.authorityOptions && hasValue(this.authorityOptions.name); + return this.vocabularyOptions && hasValue(this.vocabularyOptions.name); } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.spec.ts index 63ae56e59a..b35ee10e8a 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.spec.ts @@ -2,25 +2,25 @@ import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { async, ComponentFixture, inject, TestBed, } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; - -import { DsDynamicListComponent } from './dynamic-list.component'; -import { DynamicListCheckboxGroupModel } from './dynamic-list-checkbox-group.model'; -import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model'; -import { FormBuilderService } from '../../../form-builder.service'; +import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap'; import { DynamicFormControlLayout, DynamicFormLayoutService, DynamicFormsCoreModule, DynamicFormValidationService } from '@ng-dynamic-forms/core'; -import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap'; -import { AuthorityService } from '../../../../../../core/integration/authority.service'; -import { AuthorityServiceStub } from '../../../../../testing/authority-service.stub'; + +import { DsDynamicListComponent } from './dynamic-list.component'; +import { DynamicListCheckboxGroupModel } from './dynamic-list-checkbox-group.model'; +import { VocabularyOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-options.model'; +import { FormBuilderService } from '../../../form-builder.service'; +import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; +import { VocabularyServiceStub } from '../../../../../testing/vocabulary-service.stub'; import { DynamicListRadioGroupModel } from './dynamic-list-radio-group.model'; -import { By } from '@angular/platform-browser'; -import { AuthorityValue } from '../../../../../../core/integration/models/authority.value'; +import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; import { createTestComponent } from '../../../../../testing/utils.test'; export const LAYOUT_TEST = { @@ -35,12 +35,12 @@ export const LIST_TEST_GROUP = new FormGroup({ }); export const LIST_CHECKBOX_TEST_MODEL_CONFIG = { - authorityOptions: { + vocabularyOptions: { closed: false, metadata: 'listCheckbox', name: 'type_programme', scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' - } as AuthorityOptions, + } as VocabularyOptions, disabled: false, id: 'listCheckbox', label: 'Programme', @@ -52,12 +52,12 @@ export const LIST_CHECKBOX_TEST_MODEL_CONFIG = { }; export const LIST_RADIO_TEST_MODEL_CONFIG = { - authorityOptions: { + vocabularyOptions: { closed: false, metadata: 'listRadio', name: 'type_programme', scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' - } as AuthorityOptions, + } as VocabularyOptions, disabled: false, id: 'listRadio', label: 'Programme', @@ -77,7 +77,7 @@ describe('DsDynamicListComponent test suite', () => { let html; let modelValue; - const authorityServiceStub = new AuthorityServiceStub(); + const vocabularyServiceStub = new VocabularyServiceStub(); // async beforeEach beforeEach(async(() => { @@ -99,9 +99,9 @@ describe('DsDynamicListComponent test suite', () => { DsDynamicListComponent, DynamicFormValidationService, FormBuilderService, - {provide: AuthorityService, useValue: authorityServiceStub}, - {provide: DynamicFormLayoutService, useValue: {}}, - {provide: DynamicFormValidationService, useValue: {}} + { provide: VocabularyService, useValue: vocabularyServiceStub }, + { provide: DynamicFormLayoutService, useValue: {} }, + { provide: DynamicFormValidationService, useValue: {} } ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }); @@ -147,20 +147,16 @@ describe('DsDynamicListComponent test suite', () => { }); it('should init component properly', () => { - const results$ = authorityServiceStub.getEntriesByName({} as any); - - results$.subscribe((results) => { - expect((listComp as any).optionsList).toEqual(results.payload); - expect(listComp.items.length).toBe(1); - expect(listComp.items[0].length).toBe(2); - }) + expect((listComp as any).optionsList).toEqual(vocabularyServiceStub.getList()); + expect(listComp.items.length).toBe(1); + expect(listComp.items[0].length).toBe(2); }); it('should set model value properly when a checkbox option is selected', () => { const de = listFixture.debugElement.queryAll(By.css('div.custom-checkbox')); const items = de[0].queryAll(By.css('input.custom-control-input')); const item = items[0]; - modelValue = [Object.assign(new AuthorityValue(), {id: 1, display: 'one', value: 1})]; + modelValue = [Object.assign(new VocabularyEntry(), { authority: 1, display: 'one', value: 1 })]; item.nativeElement.click(); @@ -187,7 +183,7 @@ describe('DsDynamicListComponent test suite', () => { listComp = listFixture.componentInstance; // FormComponent test instance listComp.group = LIST_TEST_GROUP; listComp.model = new DynamicListCheckboxGroupModel(LIST_CHECKBOX_TEST_MODEL_CONFIG, LAYOUT_TEST); - modelValue = [Object.assign(new AuthorityValue(), {id: 1, display: 'one', value: 1})]; + modelValue = [Object.assign(new VocabularyEntry(), { authority: 1, display: 'one', value: 1 })]; listComp.model.value = modelValue; listFixture.detectChanges(); }); @@ -198,13 +194,9 @@ describe('DsDynamicListComponent test suite', () => { }); it('should init component properly', () => { - const results$ = authorityServiceStub.getEntriesByName({} as any); - - results$.subscribe((results) => { - expect((listComp as any).optionsList).toEqual(results.payload); - expect(listComp.model.value).toEqual(modelValue); - expect((listComp.model as DynamicListCheckboxGroupModel).group[0].value).toBeTruthy(); - }) + expect((listComp as any).optionsList).toEqual(vocabularyServiceStub.getList()); + expect(listComp.model.value).toEqual(modelValue); + expect((listComp.model as DynamicListCheckboxGroupModel).group[0].value).toBeTruthy(); }); it('should set model value properly when a checkbox option is deselected', () => { @@ -237,20 +229,16 @@ describe('DsDynamicListComponent test suite', () => { }); it('should init component properly', () => { - const results$ = authorityServiceStub.getEntriesByName({} as any); - - results$.subscribe((results) => { - expect((listComp as any).optionsList).toEqual(results.payload); - expect(listComp.items.length).toBe(1); - expect(listComp.items[0].length).toBe(2); - }) + expect((listComp as any).optionsList).toEqual(vocabularyServiceStub.getList()); + expect(listComp.items.length).toBe(1); + expect(listComp.items[0].length).toBe(2); }); it('should set model value when a radio option is selected', () => { const de = listFixture.debugElement.queryAll(By.css('div.custom-radio')); const items = de[0].queryAll(By.css('input.custom-control-input')); const item = items[0]; - modelValue = Object.assign(new AuthorityValue(), {id: 1, display: 'one', value: 1}); + modelValue = Object.assign(new VocabularyEntry(), { authority: 1, display: 'one', value: 1 }); item.nativeElement.click(); @@ -265,7 +253,7 @@ describe('DsDynamicListComponent test suite', () => { listComp = listFixture.componentInstance; // FormComponent test instance listComp.group = LIST_TEST_GROUP; listComp.model = new DynamicListRadioGroupModel(LIST_RADIO_TEST_MODEL_CONFIG, LAYOUT_TEST); - modelValue = Object.assign(new AuthorityValue(), {id: 1, display: 'one', value: 1}); + modelValue = Object.assign(new VocabularyEntry(), { authority: 1, display: 'one', value: 1 }); listComp.model.value = modelValue; listFixture.detectChanges(); }); @@ -276,13 +264,9 @@ describe('DsDynamicListComponent test suite', () => { }); it('should init component properly', () => { - const results$ = authorityServiceStub.getEntriesByName({} as any); - - results$.subscribe((results) => { - expect((listComp as any).optionsList).toEqual(results.payload); - expect(listComp.model.value).toEqual(modelValue); - expect((listComp.model as DynamicListRadioGroupModel).options[0].value).toBeTruthy(); - }) + expect((listComp as any).optionsList).toEqual(vocabularyServiceStub.getList()); + expect(listComp.model.value).toEqual(modelValue); + expect((listComp.model as DynamicListRadioGroupModel).options[0].value).toBeTruthy(); }); }); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts index d023f1c583..3c5a86f362 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts @@ -1,20 +1,23 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { FormGroup } from '@angular/forms'; + +import { + DynamicCheckboxModel, + DynamicFormControlComponent, + DynamicFormLayoutService, + DynamicFormValidationService +} from '@ng-dynamic-forms/core'; import { findKey } from 'lodash'; -import { AuthorityService } from '../../../../../../core/integration/authority.service'; -import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model'; +import { VocabularyFindOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-find-options.model'; import { hasValue, isNotEmpty } from '../../../../../empty.util'; import { DynamicListCheckboxGroupModel } from './dynamic-list-checkbox-group.model'; import { FormBuilderService } from '../../../form-builder.service'; -import { - DynamicCheckboxModel, - DynamicFormControlComponent, DynamicFormLayoutService, - DynamicFormValidationService -} from '@ng-dynamic-forms/core'; -import { AuthorityValue } from '../../../../../../core/integration/models/authority.value'; import { DynamicListRadioGroupModel } from './dynamic-list-radio-group.model'; -import { IntegrationData } from '../../../../../../core/integration/integration-data'; +import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; +import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators'; +import { PaginatedList } from '../../../../../../core/data/paginated-list'; +import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; export interface ListItem { id: string, @@ -39,10 +42,10 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen @Output() focus: EventEmitter = new EventEmitter(); public items: ListItem[][] = []; - protected optionsList: AuthorityValue[]; - protected searchOptions: IntegrationSearchOptions; + protected optionsList: VocabularyEntry[]; + protected searchOptions: VocabularyFindOptions; - constructor(private authorityService: AuthorityService, + constructor(private vocabularyService: VocabularyService, private cdr: ChangeDetectorRef, private formBuilderService: FormBuilderService, protected layoutService: DynamicFormLayoutService, @@ -54,10 +57,10 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen ngOnInit() { if (this.hasAuthorityOptions()) { // TODO Replace max elements 1000 with a paginated request when pagination bug is resolved - this.searchOptions = new IntegrationSearchOptions( - this.model.authorityOptions.scope, - this.model.authorityOptions.name, - this.model.authorityOptions.metadata, + this.searchOptions = new VocabularyFindOptions( + this.model.vocabularyOptions.scope, + this.model.vocabularyOptions.name, + this.model.vocabularyOptions.metadata, '', 1000, // Max elements 1);// Current Page @@ -77,13 +80,13 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen const target = event.target as any; if (this.model.repeatable) { // Target tabindex coincide with the array index of the value into the authority list - const authorityValue: AuthorityValue = this.optionsList[target.tabIndex]; + const entry: VocabularyEntry = this.optionsList[target.tabIndex]; if (target.checked) { - this.model.valueUpdates.next(authorityValue); + this.model.valueUpdates.next(entry); } else { const newValue = []; this.model.value - .filter((item) => item.value !== authorityValue.value) + .filter((item) => item.value !== entry.value) .forEach((item) => newValue.push(item)); this.model.valueUpdates.next(newValue); } @@ -94,16 +97,18 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen } protected setOptionsFromAuthority() { - if (this.model.authorityOptions.name && this.model.authorityOptions.name.length > 0) { + if (this.model.vocabularyOptions.name && this.model.vocabularyOptions.name.length > 0) { const listGroup = this.group.controls[this.model.id] as FormGroup; - this.authorityService.getEntriesByName(this.searchOptions).subscribe((authorities: IntegrationData) => { + this.vocabularyService.getVocabularyEntries(this.searchOptions).pipe( + getFirstSucceededRemoteDataPayload() + ).subscribe((entries: PaginatedList) => { let groupCounter = 0; let itemsPerGroup = 0; let tempList: ListItem[] = []; - this.optionsList = authorities.payload as AuthorityValue[]; + this.optionsList = entries.page; // Make a list of available options (checkbox/radio) and split in groups of 'model.groupLength' - (authorities.payload as AuthorityValue[]).forEach((option, key) => { - const value = option.id || option.value; + entries.page.forEach((option, key) => { + const value = option.authority || option.value; const checked: boolean = isNotEmpty(findKey( this.model.value, (v) => v.value === option.value)); @@ -138,8 +143,8 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen } protected hasAuthorityOptions() { - return (hasValue(this.model.authorityOptions.scope) - && hasValue(this.model.authorityOptions.name) - && hasValue(this.model.authorityOptions.metadata)); + return (hasValue(this.model.vocabularyOptions.scope) + && hasValue(this.model.vocabularyOptions.name) + && hasValue(this.model.vocabularyOptions.metadata)); } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts index c77aabfeed..ce45e113a0 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts @@ -2,33 +2,33 @@ import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { async, ComponentFixture, fakeAsync, inject, TestBed, tick, } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; - -import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model'; -import { DynamicFormLayoutService, DynamicFormsCoreModule, DynamicFormValidationService } from '@ng-dynamic-forms/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap'; -import { AuthorityService } from '../../../../../../core/integration/authority.service'; -import { AuthorityServiceStub } from '../../../../../testing/authority-service.stub'; +import { DynamicFormLayoutService, DynamicFormsCoreModule, DynamicFormValidationService } from '@ng-dynamic-forms/core'; + +import { VocabularyOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-options.model'; +import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; +import { VocabularyServiceStub } from '../../../../../testing/vocabulary-service.stub'; import { DsDynamicLookupComponent } from './dynamic-lookup.component'; import { DynamicLookupModel } from './dynamic-lookup.model'; -import { InfiniteScrollModule } from 'ngx-infinite-scroll'; -import { TranslateModule } from '@ngx-translate/core'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; -import { By } from '@angular/platform-browser'; -import { AuthorityValue } from '../../../../../../core/integration/models/authority.value'; +import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; import { createTestComponent } from '../../../../../testing/utils.test'; import { DynamicLookupNameModel } from './dynamic-lookup-name.model'; import { AuthorityConfidenceStateDirective } from '../../../../../authority-confidence/authority-confidence-state.directive'; import { ObjNgFor } from '../../../../../utils/object-ngfor.pipe'; let LOOKUP_TEST_MODEL_CONFIG = { - authorityOptions: { + vocabularyOptions: { closed: false, metadata: 'lookup', name: 'RPAuthority', scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' - } as AuthorityOptions, + } as VocabularyOptions, disabled: false, errorMessages: { required: 'Required field.' }, id: 'lookup', @@ -47,12 +47,12 @@ let LOOKUP_TEST_MODEL_CONFIG = { }; let LOOKUP_NAME_TEST_MODEL_CONFIG = { - authorityOptions: { + vocabularyOptions: { closed: false, metadata: 'lookup-name', name: 'RPAuthority', scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' - } as AuthorityOptions, + } as VocabularyOptions, disabled: false, errorMessages: { required: 'Required field.' }, id: 'lookupName', @@ -78,12 +78,12 @@ let LOOKUP_TEST_GROUP = new FormGroup({ describe('Dynamic Lookup component', () => { function init() { LOOKUP_TEST_MODEL_CONFIG = { - authorityOptions: { + vocabularyOptions: { closed: false, metadata: 'lookup', name: 'RPAuthority', scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' - } as AuthorityOptions, + } as VocabularyOptions, disabled: false, errorMessages: { required: 'Required field.' }, id: 'lookup', @@ -102,12 +102,12 @@ describe('Dynamic Lookup component', () => { }; LOOKUP_NAME_TEST_MODEL_CONFIG = { - authorityOptions: { + vocabularyOptions: { closed: false, metadata: 'lookup-name', name: 'RPAuthority', scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' - } as AuthorityOptions, + } as VocabularyOptions, disabled: false, errorMessages: { required: 'Required field.' }, id: 'lookupName', @@ -137,12 +137,11 @@ describe('Dynamic Lookup component', () => { let testFixture: ComponentFixture; let lookupFixture: ComponentFixture; let html; + let vocabularyServiceStub: VocabularyServiceStub; - let authorityServiceStub; // async beforeEach beforeEach(async(() => { - const authorityService = new AuthorityServiceStub(); - authorityServiceStub = authorityService; + vocabularyServiceStub = new VocabularyServiceStub(); TestBed.configureTestingModule({ imports: [ DynamicFormsCoreModule, @@ -162,7 +161,7 @@ describe('Dynamic Lookup component', () => { providers: [ ChangeDetectorRef, DsDynamicLookupComponent, - { provide: AuthorityService, useValue: authorityService }, + { provide: VocabularyService, useValue: vocabularyServiceStub }, { provide: DynamicFormLayoutService, useValue: {} }, { provide: DynamicFormValidationService, useValue: {} } ], @@ -247,7 +246,7 @@ describe('Dynamic Lookup component', () => { it('should return search results', fakeAsync(() => { const de = lookupFixture.debugElement.queryAll(By.css('button')); const btnEl = de[0].nativeElement; - const results$ = authorityServiceStub.getEntriesByName({} as any); + const results = vocabularyServiceStub.getList(); lookupComp.firstInputValue = 'test'; lookupFixture.detectChanges(); @@ -255,17 +254,15 @@ describe('Dynamic Lookup component', () => { btnEl.click(); tick(); lookupFixture.detectChanges(); - results$.subscribe((results) => { - expect(lookupComp.optionsList).toEqual(results.payload); - }); + expect(lookupComp.optionsList).toEqual(results); })); it('should select a results entry properly', fakeAsync(() => { let de = lookupFixture.debugElement.queryAll(By.css('button')); const btnEl = de[0].nativeElement; - const selectedValue = Object.assign(new AuthorityValue(), { - id: 1, + const selectedValue = Object.assign(new VocabularyEntry(), { + authority: 1, display: 'one', value: 1 }); @@ -284,7 +281,7 @@ describe('Dynamic Lookup component', () => { expect(lookupComp.change.emit).toHaveBeenCalled(); })); - it('should set model.value on input type when AuthorityOptions.closed is false', fakeAsync(() => { + it('should set model.value on input type when VocabularyOptions.closed is false', fakeAsync(() => { lookupComp.firstInputValue = 'test'; lookupFixture.detectChanges(); @@ -293,8 +290,8 @@ describe('Dynamic Lookup component', () => { })); - it('should not set model.value on input type when AuthorityOptions.closed is true', () => { - lookupComp.model.authorityOptions.closed = true; + it('should not set model.value on input type when VocabularyOptions.closed is true', () => { + lookupComp.model.vocabularyOptions.closed = true; lookupComp.firstInputValue = 'test'; lookupFixture.detectChanges(); @@ -389,26 +386,26 @@ describe('Dynamic Lookup component', () => { it('should select a results entry properly', fakeAsync(() => { const payload = [ - Object.assign(new AuthorityValue(), { - id: 1, + Object.assign(new VocabularyEntry(), { + authority: 1, display: 'Name, Lastname', value: 1 }), - Object.assign(new AuthorityValue(), { - id: 2, + Object.assign(new VocabularyEntry(), { + authority: 2, display: 'NameTwo, LastnameTwo', value: 2 }), ]; let de = lookupFixture.debugElement.queryAll(By.css('button')); const btnEl = de[0].nativeElement; - const selectedValue = Object.assign(new AuthorityValue(), { - id: 1, + const selectedValue = Object.assign(new VocabularyEntry(), { + authority: 1, display: 'Name, Lastname', value: 1 }); spyOn(lookupComp.change, 'emit'); - authorityServiceStub.setNewPayload(payload); + vocabularyServiceStub.setNewPayload(payload); lookupComp.firstInputValue = 'test'; lookupFixture.detectChanges(); btnEl.click(); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts index d5516df6d9..24422e891a 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts @@ -1,8 +1,7 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { FormGroup } from '@angular/forms'; -import { Subscription } from 'rxjs'; -import { of as observableOf } from 'rxjs'; +import { of as observableOf, Subscription } from 'rxjs'; import { catchError, distinctUntilChanged } from 'rxjs/operators'; import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'; import { @@ -11,16 +10,16 @@ import { DynamicFormValidationService } from '@ng-dynamic-forms/core'; -import { AuthorityService } from '../../../../../../core/integration/authority.service'; -import { DynamicLookupModel } from './dynamic-lookup.model'; -import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model'; +import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; +import { VocabularyFindOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-find-options.model'; import { hasValue, isEmpty, isNotEmpty, isNull, isUndefined } from '../../../../../empty.util'; -import { IntegrationData } from '../../../../../../core/integration/integration-data'; import { PageInfo } from '../../../../../../core/shared/page-info.model'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; -import { AuthorityValue } from '../../../../../../core/integration/models/authority.value'; +import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; import { DynamicLookupNameModel } from './dynamic-lookup-name.model'; -import { ConfidenceType } from '../../../../../../core/integration/models/confidence-type'; +import { ConfidenceType } from '../../../../../../core/shared/confidence-type'; +import { PaginatedList } from '../../../../../../core/data/paginated-list'; +import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators'; @Component({ selector: 'ds-dynamic-lookup', @@ -43,10 +42,10 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem public pageInfo: PageInfo; public optionsList: any; - protected searchOptions: IntegrationSearchOptions; + protected searchOptions: VocabularyFindOptions; protected subs: Subscription[] = []; - constructor(private authorityService: AuthorityService, + constructor(private vocabularyService: VocabularyService, private cdr: ChangeDetectorRef, protected layoutService: DynamicFormLayoutService, protected validationService: DynamicFormValidationService @@ -59,10 +58,10 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem }; ngOnInit() { - this.searchOptions = new IntegrationSearchOptions( - this.model.authorityOptions.scope, - this.model.authorityOptions.name, - this.model.authorityOptions.metadata, + this.searchOptions = new VocabularyFindOptions( + this.model.vocabularyOptions.scope, + this.model.vocabularyOptions.name, + this.model.vocabularyOptions.metadata, '', this.model.maxOptions, 1); @@ -79,6 +78,148 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem })); } + public formatItemForInput(item: any, field: number): string { + if (isUndefined(item) || isNull(item)) { + return ''; + } + return (typeof item === 'string') ? item : this.inputFormatter(item, field); + } + + public hasAuthorityValue() { + return hasValue(this.model.value) + && this.model.value.hasAuthority(); + } + + public hasEmptyValue() { + return isNotEmpty(this.getCurrentValue()); + } + + public clearFields() { + // Clear inputs whether there is no results and authority is closed + if (this.model.vocabularyOptions.closed) { + this.resetFields(); + } + } + + public isEditDisabled() { + return !this.hasAuthorityValue(); + } + + public isInputDisabled() { + return (this.model.vocabularyOptions.closed && this.hasAuthorityValue() && !this.editMode); + } + + public isLookupName() { + return (this.model instanceof DynamicLookupNameModel); + } + + public isSearchDisabled() { + return isEmpty(this.firstInputValue) || this.editMode; + } + + public onBlurEvent(event: Event) { + this.blur.emit(event); + } + + public onFocusEvent(event) { + this.focus.emit(event); + } + + public onChange(event) { + event.preventDefault(); + if (!this.model.vocabularyOptions.closed) { + if (isNotEmpty(this.getCurrentValue())) { + const currentValue = new FormFieldMetadataValueObject(this.getCurrentValue()); + if (!this.editMode) { + this.updateModel(currentValue); + } + } else { + this.remove(); + } + } + } + + public onScroll() { + if (!this.loading && this.pageInfo.currentPage <= this.pageInfo.totalPages) { + this.searchOptions.currentPage++; + this.search(); + } + } + + public onSelect(event) { + this.updateModel(event); + } + + public openChange(isOpened: boolean) { + if (!isOpened) { + if (this.model.vocabularyOptions.closed && !this.hasAuthorityValue()) { + this.setInputsValue(''); + } + } + } + + public remove() { + this.group.markAsPristine(); + this.model.valueUpdates.next(null); + this.change.emit(null); + } + + public saveChanges() { + if (isNotEmpty(this.getCurrentValue())) { + const newValue = Object.assign(new VocabularyEntry(), this.model.value, { + display: this.getCurrentValue(), + value: this.getCurrentValue() + }); + this.updateModel(newValue); + } else { + this.remove(); + } + this.switchEditMode(); + } + + public search() { + this.optionsList = null; + this.pageInfo = null; + + // Query + this.searchOptions.query = this.getCurrentValue(); + + this.loading = true; + this.subs.push(this.vocabularyService.getVocabularyEntries(this.searchOptions).pipe( + getFirstSucceededRemoteDataPayload(), + catchError(() => + observableOf(new PaginatedList( + new PageInfo(), + [] + )) + ), + distinctUntilChanged()) + .subscribe((list: PaginatedList) => { + console.log(list); + this.optionsList = list.page; + this.pageInfo = list.pageInfo; + this.loading = false; + this.cdr.detectChanges(); + })); + } + + public switchEditMode() { + this.editMode = !this.editMode; + } + + public whenClickOnConfidenceNotAccepted(sdRef: NgbDropdown, confidence: ConfidenceType) { + if (!this.model.readOnly) { + sdRef.open(); + this.search(); + } + } + + ngOnDestroy() { + this.subs + .filter((sub) => hasValue(sub)) + .forEach((sub) => sub.unsubscribe()); + } + protected getCurrentValue(): string { let result = ''; if (!this.isLookupName()) { @@ -106,7 +247,7 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem protected setInputsValue(value) { if (hasValue(value)) { let displayValue = value; - if (value instanceof FormFieldMetadataValueObject || value instanceof AuthorityValue) { + if (value instanceof FormFieldMetadataValueObject || value instanceof VocabularyEntry) { displayValue = value.display; } @@ -131,145 +272,4 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem this.optionsList = null; this.pageInfo = null; } - - public formatItemForInput(item: any, field: number): string { - if (isUndefined(item) || isNull(item)) { - return ''; - } - return (typeof item === 'string') ? item : this.inputFormatter(item, field); - } - - public hasAuthorityValue() { - return hasValue(this.model.value) - && this.model.value.hasAuthority(); - } - - public hasEmptyValue() { - return isNotEmpty(this.getCurrentValue()); - } - - public clearFields() { - // Clear inputs whether there is no results and authority is closed - if (this.model.authorityOptions.closed) { - this.resetFields(); - } - } - - public isEditDisabled() { - return !this.hasAuthorityValue(); - } - - public isInputDisabled() { - return (this.model.authorityOptions.closed && this.hasAuthorityValue() && !this.editMode); - } - - public isLookupName() { - return (this.model instanceof DynamicLookupNameModel); - } - - public isSearchDisabled() { - return isEmpty(this.firstInputValue) || this.editMode; - } - - public onBlurEvent(event: Event) { - this.blur.emit(event); - } - - public onFocusEvent(event) { - this.focus.emit(event); - } - - public onChange(event) { - event.preventDefault(); - if (!this.model.authorityOptions.closed) { - if (isNotEmpty(this.getCurrentValue())) { - const currentValue = new FormFieldMetadataValueObject(this.getCurrentValue()); - if (!this.editMode) { - this.updateModel(currentValue); - } - } else { - this.remove(); - } - } - } - - public onScroll() { - if (!this.loading && this.pageInfo.currentPage <= this.pageInfo.totalPages) { - this.searchOptions.currentPage++; - this.search(); - } - } - - public onSelect(event) { - this.updateModel(event); - } - - public openChange(isOpened: boolean) { - if (!isOpened) { - if (this.model.authorityOptions.closed && !this.hasAuthorityValue()) { - this.setInputsValue(''); - } - } - } - - public remove() { - this.group.markAsPristine(); - this.model.valueUpdates.next(null); - this.change.emit(null); - } - - public saveChanges() { - if (isNotEmpty(this.getCurrentValue())) { - const newValue = Object.assign(new AuthorityValue(), this.model.value, { - display: this.getCurrentValue(), - value: this.getCurrentValue() - }); - this.updateModel(newValue); - } else { - this.remove(); - } - this.switchEditMode(); - } - - public search() { - this.optionsList = null; - this.pageInfo = null; - - // Query - this.searchOptions.query = this.getCurrentValue(); - - this.loading = true; - this.subs.push(this.authorityService.getEntriesByName(this.searchOptions).pipe( - catchError(() => { - const emptyResult = new IntegrationData( - new PageInfo(), - [] - ); - return observableOf(emptyResult); - }), - distinctUntilChanged()) - .subscribe((object: IntegrationData) => { - this.optionsList = object.payload; - this.pageInfo = object.pageInfo; - this.loading = false; - this.cdr.detectChanges(); - })); - } - - public switchEditMode() { - this.editMode = !this.editMode; - } - - public whenClickOnConfidenceNotAccepted(sdRef: NgbDropdown, confidence: ConfidenceType) { - if (!this.model.readOnly) { - sdRef.open(); - this.search(); - } - } - - ngOnDestroy() { - this.subs - .filter((sub) => hasValue(sub)) - .forEach((sub) => sub.unsubscribe()); - } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts index bcddb52123..64cf658d47 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts @@ -2,9 +2,12 @@ import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { async, ComponentFixture, inject, TestBed, } from '@angular/core/testing'; import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { Store, StoreModule } from '@ngrx/store'; import { TranslateModule } from '@ngx-translate/core'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; import { DsDynamicRelationGroupComponent } from './dynamic-relation-group.components'; import { DynamicRelationGroupModel, DynamicRelationGroupModelConfig } from './dynamic-relation-group.model'; @@ -13,18 +16,14 @@ import { FormFieldModel } from '../../../models/form-field.model'; import { FormBuilderService } from '../../../form-builder.service'; import { FormService } from '../../../../form.service'; import { FormComponent } from '../../../../form.component'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { Chips } from '../../../../../chips/models/chips.model'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; import { DsDynamicInputModel } from '../ds-dynamic-input.model'; import { createTestComponent } from '../../../../../testing/utils.test'; -import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; -import { AuthorityService } from '../../../../../../core/integration/authority.service'; -import { AuthorityServiceStub } from '../../../../../testing/authority-service.stub'; -import { Store, StoreModule } from '@ngrx/store'; +import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; +import { VocabularyServiceStub } from '../../../../../testing/vocabulary-service.stub'; import { StoreMock } from '../../../../../testing/store.mock'; import { FormRowModel } from '../../../../../../core/config/models/config-submission-form.model'; -import { GlobalConfig } from '../../../../../../../config/global-config.interface'; import { storeModuleConfig } from '../../../../../../app.reducer'; export let FORM_GROUP_TEST_MODEL_CONFIG; @@ -47,7 +46,7 @@ function init() { mandatoryMessage: 'Required field!', repeatable: false, selectableMetadata: [{ - authority: 'RPAuthority', + controlledVocabulary: 'RPAuthority', closed: false, metadata: 'dc.contributor.author' }], @@ -61,7 +60,7 @@ function init() { mandatory: 'false', repeatable: false, selectableMetadata: [{ - authority: 'OUAuthority', + controlledVocabulary: 'OUAuthority', closed: false, metadata: 'local.contributor.affiliation' }] @@ -128,7 +127,7 @@ describe('DsDynamicRelationGroupComponent test suite', () => { FormBuilderService, FormComponent, FormService, - { provide: AuthorityService, useValue: new AuthorityServiceStub() }, + { provide: VocabularyService, useValue: new VocabularyServiceStub() }, { provide: Store, useClass: StoreMock } ], schemas: [CUSTOM_ELEMENTS_SCHEMA] diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts index 11085a1bc3..21606d7abc 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts @@ -1,14 +1,4 @@ -import { - ChangeDetectorRef, - Component, - EventEmitter, - Inject, - Input, - OnDestroy, - OnInit, - Output, - ViewChild -} from '@angular/core'; +import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { combineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; @@ -33,12 +23,12 @@ import { hasValue, isEmpty, isNotEmpty, isNotNull } from '../../../../../empty.u import { shrinkInOut } from '../../../../../animations/shrink'; import { ChipsItem } from '../../../../../chips/models/chips-item.model'; import { hasOnlyEmptyProperties } from '../../../../../object.util'; -import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model'; -import { AuthorityService } from '../../../../../../core/integration/authority.service'; -import { IntegrationData } from '../../../../../../core/integration/integration-data'; +import { VocabularyFindOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-find-options.model'; +import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; -import { AuthorityValue } from '../../../../../../core/integration/models/authority.value'; import { environment } from '../../../../../../../environments/environment'; +import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators'; +import { VocabularyEntryDetail } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; @Component({ selector: 'ds-dynamic-relation-group', @@ -64,9 +54,9 @@ export class DsDynamicRelationGroupComponent extends DynamicFormControlComponent private selectedChipItem: ChipsItem; private subs: Subscription[] = []; - @ViewChild('formRef', {static: false}) private formRef: FormComponent; + @ViewChild('formRef', { static: false }) private formRef: FormComponent; - constructor(private authorityService: AuthorityService, + constructor(private vocabularyService: VocabularyService, private formBuilderService: FormBuilderService, private formService: FormService, private cdr: ChangeDetectorRef, @@ -177,6 +167,12 @@ export class DsDynamicRelationGroupComponent extends DynamicFormControlComponent this.clear(); } + ngOnDestroy(): void { + this.subs + .filter((sub) => hasValue(sub)) + .forEach((sub) => sub.unsubscribe()); + } + private addToChips() { if (!this.formRef.formGroup.valid) { this.formService.validateAllFormFields(this.formRef.formGroup); @@ -235,20 +231,16 @@ export class DsDynamicRelationGroupComponent extends DynamicFormControlComponent if (isObject(valueObj[fieldName]) && valueObj[fieldName].hasAuthority() && isNotEmpty(valueObj[fieldName].authority)) { const fieldId = fieldName.replace(/\./g, '_'); const model = this.formBuilderService.findById(fieldId, this.formModel); - const searchOptions: IntegrationSearchOptions = new IntegrationSearchOptions( - (model as any).authorityOptions.scope, - (model as any).authorityOptions.name, - (model as any).authorityOptions.metadata, + return$ = this.vocabularyService.findEntryDetailByValue( valueObj[fieldName].authority, - (model as any).maxOptions, - 1); - - return$ = this.authorityService.getEntryByValue(searchOptions).pipe( - map((result: IntegrationData) => Object.assign( + (model as any).vocabularyOptions.name + ).pipe( + getFirstSucceededRemoteDataPayload(), + map((entryDetail: VocabularyEntryDetail) => Object.assign( new FormFieldMetadataValueObject(), valueObj[fieldName], { - otherInformation: (result.payload[0] as AuthorityValue).otherInformation + otherInformation: entryDetail.otherInformation }) )); } else { @@ -315,10 +307,4 @@ export class DsDynamicRelationGroupComponent extends DynamicFormControlComponent } } - ngOnDestroy(): void { - this.subs - .filter((sub) => hasValue(sub)) - .forEach((sub) => sub.unsubscribe()); - } - } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts index 6086444264..c06908bdc5 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts @@ -9,12 +9,12 @@ import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { DynamicFormLayoutService, DynamicFormsCoreModule, DynamicFormValidationService } from '@ng-dynamic-forms/core'; import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap'; -import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model'; -import { AuthorityService } from '../../../../../../core/integration/authority.service'; -import { AuthorityServiceStub } from '../../../../../testing/authority-service.stub'; +import { VocabularyOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-options.model'; +import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; +import { VocabularyServiceStub } from '../../../../../testing/vocabulary-service.stub'; import { DsDynamicScrollableDropdownComponent } from './dynamic-scrollable-dropdown.component'; import { DynamicScrollableDropdownModel } from './dynamic-scrollable-dropdown.model'; -import { AuthorityValue } from '../../../../../../core/integration/models/authority.value'; +import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; import { createTestComponent, hasClass } from '../../../../../testing/utils.test'; export const SD_TEST_GROUP = new FormGroup({ @@ -22,14 +22,14 @@ export const SD_TEST_GROUP = new FormGroup({ }); export const SD_TEST_MODEL_CONFIG = { - authorityOptions: { + vocabularyOptions: { closed: false, metadata: 'dropdown', name: 'common_iso_languages', scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' - } as AuthorityOptions, + } as VocabularyOptions, disabled: false, - errorMessages: {required: 'Required field.'}, + errorMessages: { required: 'Required field.' }, id: 'dropdown', label: 'Language', maxOptions: 10, @@ -52,7 +52,7 @@ describe('Dynamic Dynamic Scrollable Dropdown component', () => { let html; let modelValue; - const authorityServiceStub = new AuthorityServiceStub(); + const vocabularyServiceStub = new VocabularyServiceStub(); // async beforeEach beforeEach(async(() => { @@ -74,9 +74,9 @@ describe('Dynamic Dynamic Scrollable Dropdown component', () => { providers: [ ChangeDetectorRef, DsDynamicScrollableDropdownComponent, - {provide: AuthorityService, useValue: authorityServiceStub}, - {provide: DynamicFormLayoutService, useValue: {}}, - {provide: DynamicFormValidationService, useValue: {}} + { provide: VocabularyService, useValue: vocabularyServiceStub }, + { provide: DynamicFormLayoutService, useValue: {} }, + { provide: DynamicFormValidationService, useValue: {} } ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }); @@ -121,11 +121,8 @@ describe('Dynamic Dynamic Scrollable Dropdown component', () => { }); it('should init component properly', () => { - const results$ = authorityServiceStub.getEntriesByName({} as any); expect(scrollableDropdownComp.optionsList).toBeDefined(); - results$.subscribe((results) => { - expect(scrollableDropdownComp.optionsList).toEqual(results.payload); - }) + expect(scrollableDropdownComp.optionsList).toEqual(vocabularyServiceStub.getList()); }); it('should display dropdown menu entries', () => { @@ -154,7 +151,7 @@ describe('Dynamic Dynamic Scrollable Dropdown component', () => { })); it('should select a results entry properly', fakeAsync(() => { - const selectedValue = Object.assign(new AuthorityValue(), {id: 1, display: 'one', value: 1}); + const selectedValue = Object.assign(new VocabularyEntry(), { authority: 1, display: 'one', value: 1 }); let de: any = scrollableDropdownFixture.debugElement.query(By.css('button.ds-form-input-btn')); let btnEl = de.nativeElement; @@ -192,7 +189,7 @@ describe('Dynamic Dynamic Scrollable Dropdown component', () => { scrollableDropdownFixture = TestBed.createComponent(DsDynamicScrollableDropdownComponent); scrollableDropdownComp = scrollableDropdownFixture.componentInstance; // FormComponent test instance scrollableDropdownComp.group = SD_TEST_GROUP; - modelValue = Object.assign(new AuthorityValue(), {id: 1, display: 'one', value: 1}); + modelValue = Object.assign(new VocabularyEntry(), { authority: 1, display: 'one', value: 1 }); scrollableDropdownComp.model = new DynamicScrollableDropdownModel(SD_TEST_MODEL_CONFIG); scrollableDropdownComp.model.value = modelValue; scrollableDropdownFixture.detectChanges(); @@ -204,12 +201,9 @@ describe('Dynamic Dynamic Scrollable Dropdown component', () => { }); it('should init component properly', () => { - const results$ = authorityServiceStub.getEntriesByName({} as any); expect(scrollableDropdownComp.optionsList).toBeDefined(); - results$.subscribe((results) => { - expect(scrollableDropdownComp.optionsList).toEqual(results.payload); - expect(scrollableDropdownComp.model.value).toEqual(modelValue); - }) + expect(scrollableDropdownComp.optionsList).toEqual(vocabularyServiceStub.getList()); + expect(scrollableDropdownComp.model.value).toEqual(modelValue); }); }); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts index 5eda1372eb..f6442407c4 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts @@ -2,7 +2,7 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } fro import { FormGroup } from '@angular/forms'; import { Observable, of as observableOf } from 'rxjs'; -import { catchError, distinctUntilChanged, first, tap } from 'rxjs/operators'; +import { catchError, distinctUntilChanged, tap } from 'rxjs/operators'; import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'; import { DynamicFormControlComponent, @@ -10,13 +10,14 @@ import { DynamicFormValidationService } from '@ng-dynamic-forms/core'; -import { AuthorityValue } from '../../../../../../core/integration/models/authority.value'; +import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; import { DynamicScrollableDropdownModel } from './dynamic-scrollable-dropdown.model'; import { PageInfo } from '../../../../../../core/shared/page-info.model'; import { isNull, isUndefined } from '../../../../../empty.util'; -import { AuthorityService } from '../../../../../../core/integration/authority.service'; -import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model'; -import { IntegrationData } from '../../../../../../core/integration/integration-data'; +import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; +import { VocabularyFindOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-find-options.model'; +import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators'; +import { PaginatedList } from '../../../../../../core/data/paginated-list'; @Component({ selector: 'ds-dynamic-scrollable-dropdown', @@ -37,9 +38,9 @@ export class DsDynamicScrollableDropdownComponent extends DynamicFormControlComp public pageInfo: PageInfo; public optionsList: any; - protected searchOptions: IntegrationSearchOptions; + protected searchOptions: VocabularyFindOptions; - constructor(private authorityService: AuthorityService, + constructor(private vocabularyService: VocabularyService, private cdr: ChangeDetectorRef, protected layoutService: DynamicFormLayoutService, protected validationService: DynamicFormValidationService @@ -48,28 +49,26 @@ export class DsDynamicScrollableDropdownComponent extends DynamicFormControlComp } ngOnInit() { - this.searchOptions = new IntegrationSearchOptions( - this.model.authorityOptions.scope, - this.model.authorityOptions.name, - this.model.authorityOptions.metadata, + this.searchOptions = new VocabularyFindOptions( + this.model.vocabularyOptions.scope, + this.model.vocabularyOptions.name, + this.model.vocabularyOptions.metadata, '', this.model.maxOptions, 1); - this.authorityService.getEntriesByName(this.searchOptions).pipe( - catchError(() => { - const emptyResult = new IntegrationData( - new PageInfo(), - [] - ); - return observableOf(emptyResult); - }), - first()) - .subscribe((object: IntegrationData) => { - this.optionsList = object.payload; + this.vocabularyService.getVocabularyEntries(this.searchOptions).pipe( + getFirstSucceededRemoteDataPayload(), + catchError(() => observableOf(new PaginatedList( + new PageInfo(), + [] + )) + )) + .subscribe((list: PaginatedList) => { + this.optionsList = list.page; if (this.model.value) { this.setCurrentValue(this.model.value); } - this.pageInfo = object.pageInfo; + this.pageInfo = list.pageInfo; this.cdr.detectChanges(); }); @@ -80,7 +79,7 @@ export class DsDynamicScrollableDropdownComponent extends DynamicFormControlComp } - inputFormatter = (x: AuthorityValue): string => x.display || x.value; + inputFormatter = (x: VocabularyEntry): string => x.display || x.value; openDropdown(sdRef: NgbDropdown) { if (!this.model.readOnly) { @@ -92,18 +91,17 @@ export class DsDynamicScrollableDropdownComponent extends DynamicFormControlComp if (!this.loading && this.pageInfo.currentPage <= this.pageInfo.totalPages) { this.loading = true; this.searchOptions.currentPage++; - this.authorityService.getEntriesByName(this.searchOptions).pipe( - catchError(() => { - const emptyResult = new IntegrationData( - new PageInfo(), - [] - ); - return observableOf(emptyResult); - }), + this.vocabularyService.getVocabularyEntries(this.searchOptions).pipe( + getFirstSucceededRemoteDataPayload(), + catchError(() => observableOf(new PaginatedList( + new PageInfo(), + [] + )) + ), tap(() => this.loading = false)) - .subscribe((object: IntegrationData) => { - this.optionsList = this.optionsList.concat(object.payload); - this.pageInfo = object.pageInfo; + .subscribe((list: PaginatedList) => { + this.optionsList = this.optionsList.concat(list.page); + this.pageInfo = list.pageInfo; this.cdr.detectChanges(); }) } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model.ts index 8ab497ec6e..a9974717e4 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model.ts @@ -1,11 +1,11 @@ import { AUTOCOMPLETE_OFF, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core'; import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model'; -import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model'; +import { VocabularyOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-options.model'; export const DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN = 'SCROLLABLE_DROPDOWN'; export interface DynamicScrollableDropdownModelConfig extends DsDynamicInputModelConfig { - authorityOptions: AuthorityOptions; + vocabularyOptions: VocabularyOptions; maxOptions?: number; value?: any; } @@ -20,7 +20,7 @@ export class DynamicScrollableDropdownModel extends DsDynamicInputModel { super(config, layout); this.autoComplete = AUTOCOMPLETE_OFF; - this.authorityOptions = config.authorityOptions; + this.vocabularyOptions = config.vocabularyOptions; this.maxOptions = config.maxOptions || 10; } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.spec.ts index 79d652623f..a85c215b5f 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.spec.ts @@ -12,15 +12,14 @@ import { import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap'; import { NgbModule, NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap'; -import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model'; -import { AuthorityService } from '../../../../../../core/integration/authority.service'; -import { AuthorityServiceStub } from '../../../../../testing/authority-service.stub'; +import { VocabularyOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-options.model'; +import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; +import { VocabularyServiceStub } from '../../../../../testing/vocabulary-service.stub'; import { DsDynamicTagComponent } from './dynamic-tag.component'; import { DynamicTagModel } from './dynamic-tag.model'; -import { GlobalConfig } from '../../../../../../../config/global-config.interface'; import { Chips } from '../../../../../chips/models/chips.model'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; -import { AuthorityValue } from '../../../../../../core/integration/models/authority.value'; +import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; import { createTestComponent } from '../../../../../testing/utils.test'; function createKeyUpEvent(key: number) { @@ -45,12 +44,12 @@ function init() { }); TAG_TEST_MODEL_CONFIG = { - authorityOptions: { + vocabularyOptions: { closed: false, metadata: 'tag', name: 'common_iso_languages', scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' - } as AuthorityOptions, + } as VocabularyOptions, disabled: false, id: 'tag', label: 'Keywords', @@ -75,7 +74,7 @@ describe('DsDynamicTagComponent test suite', () => { // async beforeEach beforeEach(async(() => { - const authorityServiceStub = new AuthorityServiceStub(); + const vocabularyServiceStub = new VocabularyServiceStub(); init(); TestBed.configureTestingModule({ imports: [ @@ -92,7 +91,7 @@ describe('DsDynamicTagComponent test suite', () => { providers: [ ChangeDetectorRef, DsDynamicTagComponent, - { provide: AuthorityService, useValue: authorityServiceStub }, + { provide: VocabularyService, useValue: vocabularyServiceStub }, { provide: DynamicFormLayoutService, useValue: {} }, { provide: DynamicFormValidationService, useValue: {} } ], @@ -124,7 +123,7 @@ describe('DsDynamicTagComponent test suite', () => { })); }); - describe('when authorityOptions are set', () => { + describe('when vocabularyOptions are set', () => { describe('and init model value is empty', () => { beforeEach(() => { @@ -148,20 +147,20 @@ describe('DsDynamicTagComponent test suite', () => { }); it('should search when 3+ characters typed', fakeAsync(() => { - spyOn((tagComp as any).authorityService, 'getEntriesByName').and.callThrough(); + spyOn((tagComp as any).vocabularyService, 'getVocabularyEntries').and.callThrough(); tagComp.search(observableOf('test')).subscribe(() => { - expect((tagComp as any).authorityService.getEntriesByName).toHaveBeenCalled(); + expect((tagComp as any).vocabularyService.getVocabularyEntries).toHaveBeenCalled(); }); })); it('should select a results entry properly', fakeAsync(() => { modelValue = [ - Object.assign(new AuthorityValue(), { id: 1, display: 'Name, Lastname', value: 1 }) + Object.assign(new VocabularyEntry(), { authority: 1, display: 'Name, Lastname', value: 1 }) ]; const event: NgbTypeaheadSelectItemEvent = { - item: Object.assign(new AuthorityValue(), { - id: 1, + item: Object.assign(new VocabularyEntry(), { + authority: 1, display: 'Name, Lastname', value: 1 }), @@ -239,7 +238,7 @@ describe('DsDynamicTagComponent test suite', () => { }); - describe('when authorityOptions are not set', () => { + describe('when vocabularyOptions are not set', () => { describe('and init model value is empty', () => { beforeEach(() => { @@ -247,7 +246,7 @@ describe('DsDynamicTagComponent test suite', () => { tagComp = tagFixture.componentInstance; // FormComponent test instance tagComp.group = TAG_TEST_GROUP; const config = TAG_TEST_MODEL_CONFIG; - config.authorityOptions = null; + config.vocabularyOptions = null; tagComp.model = new DynamicTagModel(config); tagFixture.detectChanges(); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts index 69a7a5a8c2..85d791da6d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnInit, Output, ViewChild } from '@angular/core'; +import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { @@ -6,17 +6,21 @@ import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; -import { of as observableOf, Observable } from 'rxjs'; -import { catchError, debounceTime, distinctUntilChanged, tap, switchMap, map, merge } from 'rxjs/operators'; +import { Observable, of as observableOf } from 'rxjs'; +import { catchError, debounceTime, distinctUntilChanged, map, merge, switchMap, tap } from 'rxjs/operators'; import { NgbTypeahead, NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap'; import { isEqual } from 'lodash'; -import { AuthorityService } from '../../../../../../core/integration/authority.service'; +import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; import { DynamicTagModel } from './dynamic-tag.model'; -import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model'; +import { VocabularyFindOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-find-options.model'; import { Chips } from '../../../../../chips/models/chips.model'; import { hasValue, isNotEmpty } from '../../../../../empty.util'; import { environment } from '../../../../../../../environments/environment'; +import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators'; +import { PaginatedList } from '../../../../../../core/data/paginated-list'; +import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; +import { PageInfo } from '../../../../../../core/shared/page-info.model'; @Component({ selector: 'ds-dynamic-tag', @@ -32,17 +36,25 @@ export class DsDynamicTagComponent extends DynamicFormControlComponent implement @Output() change: EventEmitter = new EventEmitter(); @Output() focus: EventEmitter = new EventEmitter(); - @ViewChild('instance', {static: false}) instance: NgbTypeahead; + @ViewChild('instance', { static: false }) instance: NgbTypeahead; chips: Chips; hasAuthority: boolean; searching = false; - searchOptions: IntegrationSearchOptions; + searchOptions: VocabularyFindOptions; searchFailed = false; hideSearchingWhenUnsubscribed = new Observable(() => () => this.changeSearchingStatus(false)); currentValue: any; + constructor(private vocabularyService: VocabularyService, + private cdr: ChangeDetectorRef, + protected layoutService: DynamicFormLayoutService, + protected validationService: DynamicFormValidationService + ) { + super(layoutService, validationService); + } + formatter = (x: { display: string }) => x.display; search = (text$: Observable) => @@ -52,44 +64,33 @@ export class DsDynamicTagComponent extends DynamicFormControlComponent implement tap(() => this.changeSearchingStatus(true)), switchMap((term) => { if (term === '' || term.length < this.model.minChars) { - return observableOf({list: []}); + return observableOf({ list: [] }); } else { this.searchOptions.query = term; - return this.authorityService.getEntriesByName(this.searchOptions).pipe( - map((authorities) => { - // @TODO Pagination for authority is not working, to refactor when it will be fixed - return { - list: authorities.payload, - pageInfo: authorities.pageInfo - }; - }), + return this.vocabularyService.getVocabularyEntries(this.searchOptions).pipe( + getFirstSucceededRemoteDataPayload(), tap(() => this.searchFailed = false), catchError(() => { this.searchFailed = true; - return observableOf({list: []}); + return observableOf(new PaginatedList( + new PageInfo(), + [] + )); })); } }), - map((results) => results.list), + map((list: PaginatedList) => list.page), tap(() => this.changeSearchingStatus(false)), merge(this.hideSearchingWhenUnsubscribed)); - constructor(private authorityService: AuthorityService, - private cdr: ChangeDetectorRef, - protected layoutService: DynamicFormLayoutService, - protected validationService: DynamicFormValidationService - ) { - super(layoutService, validationService); - } - ngOnInit() { - this.hasAuthority = this.model.authorityOptions && hasValue(this.model.authorityOptions.name); + this.hasAuthority = this.model.vocabularyOptions && hasValue(this.model.vocabularyOptions.name); if (this.hasAuthority) { - this.searchOptions = new IntegrationSearchOptions( - this.model.authorityOptions.scope, - this.model.authorityOptions.name, - this.model.authorityOptions.metadata); + this.searchOptions = new VocabularyFindOptions( + this.model.vocabularyOptions.scope, + this.model.vocabularyOptions.name, + this.model.vocabularyOptions.metadata); } this.chips = new Chips( @@ -166,7 +167,7 @@ export class DsDynamicTagComponent extends DynamicFormControlComponent implement } private addTagsToChips() { - if (hasValue(this.currentValue) && (!this.hasAuthority || !this.model.authorityOptions.closed)) { + if (hasValue(this.currentValue) && (!this.hasAuthority || !this.model.vocabularyOptions.closed)) { let res: string[] = []; res = this.currentValue.split(','); 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 f31f0eeff9..48456230bf 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 @@ -10,10 +10,9 @@ import { DynamicFormLayoutService, DynamicFormsCoreModule, DynamicFormValidation import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; -import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model'; -import { AuthorityService } from '../../../../../../core/integration/authority.service'; -import { AuthorityServiceStub } from '../../../../../testing/authority-service.stub'; -import { GlobalConfig } from '../../../../../../../config/global-config.interface'; +import { VocabularyOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-options.model'; +import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; +import { VocabularyServiceStub } from '../../../../../testing/vocabulary-service.stub'; import { DsDynamicTypeaheadComponent } from './dynamic-typeahead.component'; import { DynamicTypeaheadModel } from './dynamic-typeahead.model'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; @@ -31,12 +30,12 @@ function init() { }); TYPEAHEAD_TEST_MODEL_CONFIG = { - authorityOptions: { + vocabularyOptions: { closed: false, metadata: 'typeahead', name: 'EVENTAuthority', scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' - } as AuthorityOptions, + } as VocabularyOptions, disabled: false, id: 'typeahead', label: 'Conference', @@ -49,6 +48,7 @@ function init() { value: undefined }; } + describe('DsDynamicTypeaheadComponent test suite', () => { let testComp: TestComponent; @@ -59,7 +59,7 @@ describe('DsDynamicTypeaheadComponent test suite', () => { // async beforeEach beforeEach(async(() => { - const authorityServiceStub = new AuthorityServiceStub(); + const vocabularyServiceStub = new VocabularyServiceStub(); init(); TestBed.configureTestingModule({ imports: [ @@ -79,7 +79,7 @@ describe('DsDynamicTypeaheadComponent test suite', () => { providers: [ ChangeDetectorRef, DsDynamicTypeaheadComponent, - { provide: AuthorityService, useValue: authorityServiceStub }, + { provide: VocabularyService, useValue: vocabularyServiceStub }, { provide: DynamicFormLayoutService, useValue: {} }, { provide: DynamicFormValidationService, useValue: {} } ], @@ -134,17 +134,17 @@ describe('DsDynamicTypeaheadComponent test suite', () => { it('should search when 3+ characters typed', fakeAsync(() => { - spyOn((typeaheadComp as any).authorityService, 'getEntriesByName').and.callThrough(); + spyOn((typeaheadComp as any).vocabularyService, 'getVocabularyEntries').and.callThrough(); typeaheadComp.search(observableOf('test')).subscribe(); tick(300); typeaheadFixture.detectChanges(); - expect((typeaheadComp as any).authorityService.getEntriesByName).toHaveBeenCalled(); + expect((typeaheadComp as any).vocabularyService.getVocabularyEntries).toHaveBeenCalled(); })); - it('should set model.value on input type when AuthorityOptions.closed is false', () => { + it('should set model.value on input type when VocabularyOptions.closed is false', () => { const inputDe = typeaheadFixture.debugElement.query(By.css('input.form-control')); const inputElement = inputDe.nativeElement; @@ -155,8 +155,8 @@ describe('DsDynamicTypeaheadComponent test suite', () => { }); - it('should not set model.value on input type when AuthorityOptions.closed is true', () => { - typeaheadComp.model.authorityOptions.closed = true; + it('should not set model.value on input type when VocabularyOptions.closed is true', () => { + typeaheadComp.model.vocabularyOptions.closed = true; typeaheadFixture.detectChanges(); const inputDe = typeaheadFixture.debugElement.query(By.css('input.form-control')); const inputElement = inputDe.nativeElement; @@ -184,18 +184,18 @@ describe('DsDynamicTypeaheadComponent test suite', () => { expect(typeaheadComp.blur.emit).not.toHaveBeenCalled(); }); - it('should emit change Event onBlur when AuthorityOptions.closed is false and inputValue is changed', () => { + it('should emit change Event onBlur when VocabularyOptions.closed is false and inputValue is changed', () => { typeaheadComp.inputValue = 'test value'; typeaheadFixture.detectChanges(); spyOn(typeaheadComp.blur, 'emit'); spyOn(typeaheadComp.change, 'emit'); spyOn(typeaheadComp.instance, 'isPopupOpen').and.returnValue(false); - typeaheadComp.onBlur(new Event('blur', )); + typeaheadComp.onBlur(new Event('blur',)); expect(typeaheadComp.change.emit).toHaveBeenCalled(); expect(typeaheadComp.blur.emit).toHaveBeenCalled(); }); - it('should not emit change Event onBlur when AuthorityOptions.closed is false and inputValue is not changed', () => { + it('should not emit change Event onBlur when VocabularyOptions.closed is false and inputValue is not changed', () => { typeaheadComp.inputValue = 'test value'; typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG); (typeaheadComp.model as any).value = 'test value'; @@ -203,12 +203,12 @@ describe('DsDynamicTypeaheadComponent test suite', () => { spyOn(typeaheadComp.blur, 'emit'); spyOn(typeaheadComp.change, 'emit'); spyOn(typeaheadComp.instance, 'isPopupOpen').and.returnValue(false); - typeaheadComp.onBlur(new Event('blur', )); + typeaheadComp.onBlur(new Event('blur',)); expect(typeaheadComp.change.emit).not.toHaveBeenCalled(); expect(typeaheadComp.blur.emit).toHaveBeenCalled(); }); - it('should not emit change Event onBlur when AuthorityOptions.closed is false and inputValue is null', () => { + it('should not emit change Event onBlur when VocabularyOptions.closed is false and inputValue is null', () => { typeaheadComp.inputValue = null; typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG); (typeaheadComp.model as any).value = 'test value'; @@ -216,7 +216,7 @@ describe('DsDynamicTypeaheadComponent test suite', () => { spyOn(typeaheadComp.blur, 'emit'); spyOn(typeaheadComp.change, 'emit'); spyOn(typeaheadComp.instance, 'isPopupOpen').and.returnValue(false); - typeaheadComp.onBlur(new Event('blur', )); + typeaheadComp.onBlur(new Event('blur',)); expect(typeaheadComp.change.emit).not.toHaveBeenCalled(); expect(typeaheadComp.blur.emit).toHaveBeenCalled(); }); 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 791704a7ca..67030682a5 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 @@ -10,12 +10,16 @@ import { catchError, debounceTime, distinctUntilChanged, filter, map, merge, swi import { Observable, of as observableOf, Subject } from 'rxjs'; import { NgbTypeahead, NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap'; -import { AuthorityService } from '../../../../../../core/integration/authority.service'; +import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; import { DynamicTypeaheadModel } from './dynamic-typeahead.model'; -import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model'; +import { VocabularyFindOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-find-options.model'; import { isEmpty, isNotEmpty, isNotNull } from '../../../../../empty.util'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; -import { ConfidenceType } from '../../../../../../core/integration/models/confidence-type'; +import { ConfidenceType } from '../../../../../../core/shared/confidence-type'; +import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators'; +import { PaginatedList } from '../../../../../../core/data/paginated-list'; +import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; +import { PageInfo } from '../../../../../../core/shared/page-info.model'; @Component({ selector: 'ds-dynamic-typeahead', @@ -31,16 +35,24 @@ export class DsDynamicTypeaheadComponent extends DynamicFormControlComponent imp @Output() change: EventEmitter = new EventEmitter(); @Output() focus: EventEmitter = new EventEmitter(); - @ViewChild('instance', {static: false}) instance: NgbTypeahead; + @ViewChild('instance', { static: false }) instance: NgbTypeahead; searching = false; - searchOptions: IntegrationSearchOptions; + searchOptions: VocabularyFindOptions; searchFailed = false; hideSearchingWhenUnsubscribed$ = new Observable(() => () => this.changeSearchingStatus(false)); click$ = new Subject(); currentValue: any; inputValue: any; + constructor(private vocabularyService: VocabularyService, + private cdr: ChangeDetectorRef, + protected layoutService: DynamicFormLayoutService, + protected validationService: DynamicFormValidationService + ) { + super(layoutService, validationService); + } + formatter = (x: { display: string }) => { return (typeof x === 'object') ? x.display : x }; @@ -53,44 +65,33 @@ export class DsDynamicTypeaheadComponent extends DynamicFormControlComponent imp tap(() => this.changeSearchingStatus(true)), switchMap((term) => { if (term === '' || term.length < this.model.minChars) { - return observableOf({list: []}); + return observableOf({ list: [] }); } else { this.searchOptions.query = term; - return this.authorityService.getEntriesByName(this.searchOptions).pipe( - map((authorities) => { - // @TODO Pagination for authority is not working, to refactor when it will be fixed - return { - list: authorities.payload, - pageInfo: authorities.pageInfo - }; - }), + return this.vocabularyService.getVocabularyEntries(this.searchOptions).pipe( + getFirstSucceededRemoteDataPayload(), tap(() => this.searchFailed = false), catchError(() => { this.searchFailed = true; - return observableOf({list: []}); + return observableOf(new PaginatedList( + new PageInfo(), + [] + )); })); } }), - map((results) => results.list), + map((list: PaginatedList) => list.page), tap(() => this.changeSearchingStatus(false)), merge(this.hideSearchingWhenUnsubscribed$) ) }; - constructor(private authorityService: AuthorityService, - private cdr: ChangeDetectorRef, - protected layoutService: DynamicFormLayoutService, - protected validationService: DynamicFormValidationService - ) { - super(layoutService, validationService); - } - ngOnInit() { this.currentValue = this.model.value; - this.searchOptions = new IntegrationSearchOptions( - this.model.authorityOptions.scope, - this.model.authorityOptions.name, - this.model.authorityOptions.metadata); + this.searchOptions = new VocabularyFindOptions( + this.model.vocabularyOptions.scope, + this.model.vocabularyOptions.name, + this.model.vocabularyOptions.metadata); this.group.get(this.model.id).valueChanges.pipe( filter((value) => this.currentValue !== value)) .subscribe((value) => { @@ -104,14 +105,14 @@ export class DsDynamicTypeaheadComponent extends DynamicFormControlComponent imp } onInput(event) { - if (!this.model.authorityOptions.closed && isNotEmpty(event.target.value)) { + if (!this.model.vocabularyOptions.closed && isNotEmpty(event.target.value)) { this.inputValue = new FormFieldMetadataValueObject(event.target.value); } } onBlur(event: Event) { if (!this.instance.isPopupOpen()) { - if (!this.model.authorityOptions.closed && isNotEmpty(this.inputValue)) { + if (!this.model.vocabularyOptions.closed && isNotEmpty(this.inputValue)) { if (isNotNull(this.inputValue) && this.model.value !== this.inputValue) { this.model.valueUpdates.next(this.inputValue); this.change.emit(this.inputValue); diff --git a/src/app/shared/form/builder/form-builder.service.spec.ts b/src/app/shared/form/builder/form-builder.service.spec.ts index 972abb68b5..434fe6a2e1 100644 --- a/src/app/shared/form/builder/form-builder.service.spec.ts +++ b/src/app/shared/form/builder/form-builder.service.spec.ts @@ -36,7 +36,7 @@ import { DynamicLookupModel } from './ds-dynamic-form-ui/models/lookup/dynamic-l import { DynamicDsDatePickerModel } from './ds-dynamic-form-ui/models/date-picker/date-picker.model'; import { DynamicTypeaheadModel } from './ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.model'; import { DynamicListRadioGroupModel } from './ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model'; -import { AuthorityOptions } from '../../../core/integration/models/authority-options.model'; +import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model'; import { FormFieldModel } from './models/form-field.model'; import { SubmissionFormsModel @@ -78,7 +78,7 @@ describe('FormBuilderService test suite', () => { ] }); - const authorityOptions: AuthorityOptions = { + const vocabularyOptions: VocabularyOptions = { closed: false, metadata: 'list', name: 'type_programme', @@ -197,13 +197,13 @@ describe('FormBuilderService test suite', () => { new DynamicTypeaheadModel({id: 'testTypeahead', repeatable: false, metadataFields: [], submissionId: '1234'}), - new DynamicScrollableDropdownModel({id: 'testScrollableDropdown', authorityOptions: authorityOptions, repeatable: false, metadataFields: [], submissionId: '1234'}), + new DynamicScrollableDropdownModel({id: 'testScrollableDropdown', vocabularyOptions: vocabularyOptions, repeatable: false, metadataFields: [], submissionId: '1234'}), new DynamicTagModel({id: 'testTag', repeatable: false, metadataFields: [], submissionId: '1234'}), - new DynamicListCheckboxGroupModel({id: 'testCheckboxList', authorityOptions: authorityOptions, repeatable: true}), + new DynamicListCheckboxGroupModel({id: 'testCheckboxList', vocabularyOptions: vocabularyOptions, repeatable: true}), - new DynamicListRadioGroupModel({id: 'testRadioList', authorityOptions: authorityOptions, repeatable: false}), + new DynamicListRadioGroupModel({id: 'testRadioList', vocabularyOptions: vocabularyOptions, repeatable: false}), new DynamicRelationGroupModel({ submissionId, @@ -218,7 +218,7 @@ describe('FormBuilderService test suite', () => { mandatoryMessage: 'Required field!', repeatable: false, selectableMetadata: [{ - authority: 'RPAuthority', + controlledVocabulary: 'RPAuthority', closed: false, metadata: 'dc.contributor.author' }], @@ -232,7 +232,7 @@ describe('FormBuilderService test suite', () => { mandatory: 'false', repeatable: false, selectableMetadata: [{ - authority: 'OUAuthority', + controlledVocabulary: 'OUAuthority', closed: false, metadata: 'local.contributor.affiliation' }] @@ -284,7 +284,7 @@ describe('FormBuilderService test suite', () => { selectableMetadata: [ { metadata: 'journal', - authority: 'JOURNALAuthority', + controlledVocabulary: 'JOURNALAuthority', closed: false } ], @@ -364,7 +364,7 @@ describe('FormBuilderService test suite', () => { selectableMetadata: [ { metadata: 'conference', - authority: 'EVENTAuthority', + controlledVocabulary: 'EVENTAuthority', closed: false } ], 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 45489e3618..ff0afe97fd 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 @@ -1,5 +1,5 @@ import { isEmpty, isNotEmpty, isNotNull } from '../../../empty.util'; -import { ConfidenceType } from '../../../../core/integration/models/confidence-type'; +import { ConfidenceType } from '../../../../core/shared/confidence-type'; import { PLACEHOLDER_PARENT_METADATA } from '../ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; import { MetadataValueInterface } from '../../../../core/shared/metadata.models'; @@ -7,15 +7,16 @@ export interface OtherInformation { [name: string]: string } +/** + * A class representing a specific input-form field's value + */ export class FormFieldMetadataValueObject implements MetadataValueInterface { - metadata?: string; value: any; display: string; language: any; authority: string; confidence: ConfidenceType; place: number; - closed: boolean; label: string; otherInformation: OtherInformation; @@ -42,9 +43,6 @@ 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/models/form-field.model.ts b/src/app/shared/form/builder/models/form-field.model.ts index 718b3f4f0d..95ee980aeb 100644 --- a/src/app/shared/form/builder/models/form-field.model.ts +++ b/src/app/shared/form/builder/models/form-field.model.ts @@ -1,50 +1,121 @@ import { autoserialize } from 'cerialize'; + import { LanguageCode } from './form-field-language-value.model'; -import { FormFieldMetadataValueObject } from './form-field-metadata-value.model'; import { RelationshipOptions } from './relationship-options.model'; import { FormRowModel } from '../../../../core/config/models/config-submission-form.model'; +/** + * Representing SelectableMetadata properties + */ +export interface SelectableMetadata { + /** + * The key of the metadata field to use to store the input + */ + metadata: string; + + /** + * The label of the metadata field to use to store the input + */ + label: string; + + /** + * The name of the controlled vocabulary used to retrieve value for the input see controlled vocabularies + */ + controlledVocabulary: string; + + /** + * A boolean representing if value is closely related to the controlled vocabulary entry or not + */ + closed: boolean; +} + +/** + * A class representing a specific input-form field + */ export class FormFieldModel { + /** + * The hints for this metadata field to display on form + */ @autoserialize hints: string; + /** + * The label for this metadata field to display on form + */ @autoserialize label: string; + /** + * The languages available for this metadata field to display on form + */ @autoserialize languageCodes: LanguageCode[]; + /** + * The error message for this metadata field to display on form in case of field is required + */ @autoserialize mandatoryMessage: string; + /** + * Representing if this metadata field is mandatory or not + */ @autoserialize mandatory: string; + /** + * Representing if this metadata field is repeatable or not + */ @autoserialize repeatable: boolean; + /** + * Containing additional properties for this metadata field + */ @autoserialize input: { + /** + * Representing the type for this metadata field + */ type: string; + + /** + * Containing regex to use for field validation + */ regex?: string; }; + /** + * Representing additional vocabulary configuration for this metadata field + */ @autoserialize - selectableMetadata: FormFieldMetadataValueObject[]; + selectableMetadata: SelectableMetadata[]; + /** + * Representing additional relationship configuration for this metadata field + */ @autoserialize selectableRelationship: RelationshipOptions; @autoserialize rows: FormRowModel[]; + /** + * Representing the scope for this metadata field + */ @autoserialize scope: string; + /** + * Containing additional css classes for this metadata field to use on form + */ @autoserialize style: string; + /** + * Containing the value for this metadata field + */ @autoserialize value: any; } diff --git a/src/app/shared/form/builder/parsers/date-field-parser.spec.ts b/src/app/shared/form/builder/parsers/date-field-parser.spec.ts index efa4f3cdb5..b9adf3ed65 100644 --- a/src/app/shared/form/builder/parsers/date-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/date-field-parser.spec.ts @@ -12,7 +12,7 @@ describe('DateFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: null, - authorityUuid: null + collectionUUID: null }; beforeEach(() => { diff --git a/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts b/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts index c885b737c2..e7e64feb7b 100644 --- a/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts @@ -12,7 +12,7 @@ describe('DisabledFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: null, - authorityUuid: null + collectionUUID: null }; beforeEach(() => { diff --git a/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts b/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts index 8dbd68e05a..82d2aeac63 100644 --- a/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts @@ -11,7 +11,7 @@ describe('DropdownFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', - authorityUuid: null + collectionUUID: null }; beforeEach(() => { @@ -26,7 +26,7 @@ describe('DropdownFieldParser test suite', () => { selectableMetadata: [ { metadata: 'type', - authority: 'common_types_dataset', + controlledVocabulary: 'common_types_dataset', closed: false } ], @@ -50,7 +50,7 @@ describe('DropdownFieldParser test suite', () => { }); it('should throw when authority is not passed', () => { - field.selectableMetadata[0].authority = null; + field.selectableMetadata[0].controlledVocabulary = null; const parser = new DropdownFieldParser(submissionId, field, initFormValues, parserOptions); expect(() => parser.parse()) diff --git a/src/app/shared/form/builder/parsers/dropdown-field-parser.ts b/src/app/shared/form/builder/parsers/dropdown-field-parser.ts index 4816a2a073..7fb5fb206c 100644 --- a/src/app/shared/form/builder/parsers/dropdown-field-parser.ts +++ b/src/app/shared/form/builder/parsers/dropdown-field-parser.ts @@ -31,8 +31,8 @@ export class DropdownFieldParser extends FieldParser { const dropdownModelConfig: DynamicScrollableDropdownModelConfig = this.initModel(null, label); let layout: DynamicFormControlLayout; - if (isNotEmpty(this.configData.selectableMetadata[0].authority)) { - this.setAuthorityOptions(dropdownModelConfig, this.parserOptions.authorityUuid); + if (isNotEmpty(this.configData.selectableMetadata[0].controlledVocabulary)) { + this.setVocabularyOptions(dropdownModelConfig, this.parserOptions.collectionUUID); if (isNotEmpty(fieldValue)) { dropdownModelConfig.value = fieldValue; } @@ -47,7 +47,7 @@ export class DropdownFieldParser extends FieldParser { const dropdownModel = new DynamicScrollableDropdownModel(dropdownModelConfig, layout); return dropdownModel; } else { - throw Error(`Authority name is not available. Please check the form configuration file.`); + throw Error(`Controlled Vocabulary name is not available. Please check the form configuration file.`); } } } diff --git a/src/app/shared/form/builder/parsers/field-parser.ts b/src/app/shared/form/builder/parsers/field-parser.ts index f218d442e1..540c9c7b4e 100644 --- a/src/app/shared/form/builder/parsers/field-parser.ts +++ b/src/app/shared/form/builder/parsers/field-parser.ts @@ -1,19 +1,20 @@ import { Inject, InjectionToken } from '@angular/core'; -import { hasValue, isNotEmpty, isNotNull, isNotUndefined, isEmpty } from '../../../empty.util'; -import { FormFieldModel } from '../models/form-field.model'; import { uniqueId } from 'lodash'; +import { DynamicFormControlLayout } from '@ng-dynamic-forms/core'; + +import { hasValue, isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../../empty.util'; +import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { DynamicRowArrayModel, DynamicRowArrayModelConfig } from '../ds-dynamic-form-ui/models/ds-dynamic-row-array-model'; import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model'; -import { DynamicFormControlLayout } from '@ng-dynamic-forms/core'; import { setLayout } from './parser.utils'; -import { AuthorityOptions } from '../../../../core/integration/models/authority-options.model'; import { ParserOptions } from './parser-options'; import { RelationshipOptions } from '../models/relationship-options.model'; +import { VocabularyOptions } from '../../../../core/submission/vocabularies/models/vocabulary-options.model'; export const SUBMISSION_ID: InjectionToken = new InjectionToken('submissionId'); export const CONFIG_DATA: InjectionToken = new InjectionToken('configData'); @@ -49,7 +50,7 @@ export abstract class FieldParser { label: this.configData.label, initialCount: this.getInitArrayIndex(), notRepeatable: !this.configData.repeatable, - required: JSON.parse( this.configData.mandatory), + required: JSON.parse(this.configData.mandatory), groupFactory: () => { let model; if ((arrayCounter === 0)) { @@ -92,6 +93,52 @@ export abstract class FieldParser { } } + public setVocabularyOptions(controlModel, scope) { + if (isNotEmpty(this.configData.selectableMetadata) && isNotEmpty(this.configData.selectableMetadata[0].controlledVocabulary)) { + controlModel.vocabularyOptions = new VocabularyOptions( + this.configData.selectableMetadata[0].controlledVocabulary, + this.configData.selectableMetadata[0].metadata, + scope, + this.configData.selectableMetadata[0].closed + ) + } + } + + public setValues(modelConfig: DsDynamicInputModelConfig, fieldValue: any, forceValueAsObj: boolean = false, groupModel?: boolean) { + if (isNotEmpty(fieldValue)) { + if (groupModel) { + // Array, values is an array + modelConfig.value = this.getInitGroupValues(); + if (Array.isArray(modelConfig.value) && modelConfig.value.length > 0 && modelConfig.value[0].language) { + // Array Item has language, ex. AuthorityModel + modelConfig.language = modelConfig.value[0].language; + } + return; + } + + if (typeof fieldValue === 'object') { + modelConfig.language = fieldValue.language; + if (forceValueAsObj) { + modelConfig.value = fieldValue; + } else { + modelConfig.value = fieldValue.value; + } + } else { + if (forceValueAsObj) { + // If value isn't an instance of FormFieldMetadataValueObject instantiate it + modelConfig.value = new FormFieldMetadataValueObject(fieldValue); + } else { + if (typeof fieldValue === 'string') { + // Case only string + modelConfig.value = fieldValue; + } + } + } + } + + return modelConfig; + } + protected getInitValueCount(index = 0, fieldId?): number { const fieldIds = fieldId || this.getAllFieldIds(); if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length === 1 && this.initFormValues.hasOwnProperty(fieldIds[0])) { @@ -135,7 +182,7 @@ 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.metadata = id; // valueObj.value = this.initFormValues[id][innerIndex]; values.push(valueObj); } @@ -224,14 +271,6 @@ export abstract class FieldParser { if (this.configData.languageCodes && this.configData.languageCodes.length > 0) { (controlModel as DsDynamicInputModel).languageCodes = this.configData.languageCodes; } - /* (controlModel as DsDynamicInputModel).languageCodes = [{ - display: 'English', - code: 'en_US' - }, - { - display: 'Italian', - code: 'it_IT' - }];*/ return controlModel; } @@ -278,50 +317,4 @@ export abstract class FieldParser { } } - public setAuthorityOptions(controlModel, authorityUuid) { - if (isNotEmpty(this.configData.selectableMetadata) && isNotEmpty(this.configData.selectableMetadata[0].authority)) { - controlModel.authorityOptions = new AuthorityOptions( - this.configData.selectableMetadata[0].authority, - this.configData.selectableMetadata[0].metadata, - authorityUuid, - this.configData.selectableMetadata[0].closed - ) - } - } - - public setValues(modelConfig: DsDynamicInputModelConfig, fieldValue: any, forceValueAsObj: boolean = false, groupModel?: boolean) { - if (isNotEmpty(fieldValue)) { - if (groupModel) { - // Array, values is an array - modelConfig.value = this.getInitGroupValues(); - if (Array.isArray(modelConfig.value) && modelConfig.value.length > 0 && modelConfig.value[0].language) { - // Array Item has language, ex. AuthorityModel - modelConfig.language = modelConfig.value[0].language; - } - return; - } - - if (typeof fieldValue === 'object') { - modelConfig.language = fieldValue.language; - if (forceValueAsObj) { - modelConfig.value = fieldValue; - } else { - modelConfig.value = fieldValue.value; - } - } else { - if (forceValueAsObj) { - // If value isn't an instance of FormFieldMetadataValueObject instantiate it - modelConfig.value = new FormFieldMetadataValueObject(fieldValue); - } else { - if (typeof fieldValue === 'string') { - // Case only string - modelConfig.value = fieldValue; - } - } - } - } - - return modelConfig; - } - } diff --git a/src/app/shared/form/builder/parsers/list-field-parser.spec.ts b/src/app/shared/form/builder/parsers/list-field-parser.spec.ts index fab5ec3888..8a05b169fd 100644 --- a/src/app/shared/form/builder/parsers/list-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/list-field-parser.spec.ts @@ -13,7 +13,7 @@ describe('ListFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', - authorityUuid: null + collectionUUID: null }; beforeEach(() => { @@ -28,7 +28,7 @@ describe('ListFieldParser test suite', () => { selectableMetadata: [ { metadata: 'type', - authority: 'type_programme', + controlledVocabulary: 'type_programme', closed: false } ], diff --git a/src/app/shared/form/builder/parsers/list-field-parser.ts b/src/app/shared/form/builder/parsers/list-field-parser.ts index 273c9d1665..d3c158f4b8 100644 --- a/src/app/shared/form/builder/parsers/list-field-parser.ts +++ b/src/app/shared/form/builder/parsers/list-field-parser.ts @@ -1,19 +1,19 @@ import { FieldParser } from './field-parser'; import { isNotEmpty } from '../../../empty.util'; -import { IntegrationSearchOptions } from '../../../../core/integration/models/integration-options.model'; +import { VocabularyFindOptions } from '../../../../core/submission/vocabularies/models/vocabulary-find-options.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { DynamicListCheckboxGroupModel } from '../ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model'; import { DynamicListRadioGroupModel } from '../ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model'; export class ListFieldParser extends FieldParser { - searchOptions: IntegrationSearchOptions; + searchOptions: VocabularyFindOptions; public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any { const listModelConfig = this.initModel(null, label); listModelConfig.repeatable = this.configData.repeatable; - if (this.configData.selectableMetadata[0].authority - && this.configData.selectableMetadata[0].authority.length > 0) { + if (this.configData.selectableMetadata[0].controlledVocabulary + && this.configData.selectableMetadata[0].controlledVocabulary.length > 0) { if (isNotEmpty(this.getInitGroupValues())) { listModelConfig.value = []; @@ -26,7 +26,7 @@ export class ListFieldParser extends FieldParser { } }); } - this.setAuthorityOptions(listModelConfig, this.parserOptions.authorityUuid); + this.setVocabularyOptions(listModelConfig, this.parserOptions.collectionUUID); } let listModel; diff --git a/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts b/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts index 5e14e0c013..87cee9d950 100644 --- a/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts @@ -12,7 +12,7 @@ describe('LookupFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', - authorityUuid: null + collectionUUID: null }; beforeEach(() => { @@ -27,7 +27,7 @@ describe('LookupFieldParser test suite', () => { selectableMetadata: [ { metadata: 'journal', - authority: 'JOURNALAuthority', + controlledVocabulary: 'JOURNALAuthority', closed: false } ], diff --git a/src/app/shared/form/builder/parsers/lookup-field-parser.ts b/src/app/shared/form/builder/parsers/lookup-field-parser.ts index 9e9c434c4f..6e28194dad 100644 --- a/src/app/shared/form/builder/parsers/lookup-field-parser.ts +++ b/src/app/shared/form/builder/parsers/lookup-field-parser.ts @@ -5,10 +5,10 @@ import { FormFieldMetadataValueObject } from '../models/form-field-metadata-valu export class LookupFieldParser extends FieldParser { public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any { - if (this.configData.selectableMetadata[0].authority) { + if (this.configData.selectableMetadata[0].controlledVocabulary) { const lookupModelConfig: DynamicLookupModelConfig = this.initModel(null, label); - this.setAuthorityOptions(lookupModelConfig, this.parserOptions.authorityUuid); + this.setVocabularyOptions(lookupModelConfig, this.parserOptions.collectionUUID); this.setValues(lookupModelConfig, fieldValue, true); diff --git a/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts b/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts index adc1e90166..3d02b6952e 100644 --- a/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts @@ -12,7 +12,7 @@ describe('LookupNameFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', - authorityUuid: null + collectionUUID: null }; beforeEach(() => { @@ -27,7 +27,7 @@ describe('LookupNameFieldParser test suite', () => { selectableMetadata: [ { metadata: 'author', - authority: 'RPAuthority', + controlledVocabulary: 'RPAuthority', closed: false } ], diff --git a/src/app/shared/form/builder/parsers/lookup-name-field-parser.ts b/src/app/shared/form/builder/parsers/lookup-name-field-parser.ts index 684e06bcb6..26806742c8 100644 --- a/src/app/shared/form/builder/parsers/lookup-name-field-parser.ts +++ b/src/app/shared/form/builder/parsers/lookup-name-field-parser.ts @@ -8,10 +8,10 @@ import { FormFieldMetadataValueObject } from '../models/form-field-metadata-valu export class LookupNameFieldParser extends FieldParser { public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any { - if (this.configData.selectableMetadata[0].authority) { + if (this.configData.selectableMetadata[0].controlledVocabulary) { const lookupModelConfig: DynamicLookupNameModelConfig = this.initModel(null, label); - this.setAuthorityOptions(lookupModelConfig, this.parserOptions.authorityUuid); + this.setVocabularyOptions(lookupModelConfig, this.parserOptions.collectionUUID); this.setValues(lookupModelConfig, fieldValue, true); diff --git a/src/app/shared/form/builder/parsers/name-field-parser.spec.ts b/src/app/shared/form/builder/parsers/name-field-parser.spec.ts index 1b0c637030..363ff40926 100644 --- a/src/app/shared/form/builder/parsers/name-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/name-field-parser.spec.ts @@ -14,7 +14,7 @@ describe('NameFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', - authorityUuid: null + collectionUUID: null }; beforeEach(() => { diff --git a/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts b/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts index 4668b3017d..c2d1b6f565 100644 --- a/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts @@ -15,7 +15,7 @@ describe('OneboxFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', - authorityUuid: null + collectionUUID: null }; beforeEach(() => { @@ -28,7 +28,7 @@ describe('OneboxFieldParser test suite', () => { selectableMetadata: [ { metadata: 'title', - authority: 'EVENTAuthority', + controlledVocabulary: 'EVENTAuthority', closed: false } ], diff --git a/src/app/shared/form/builder/parsers/onebox-field-parser.ts b/src/app/shared/form/builder/parsers/onebox-field-parser.ts index d69c9d4677..598918ac2e 100644 --- a/src/app/shared/form/builder/parsers/onebox-field-parser.ts +++ b/src/app/shared/form/builder/parsers/onebox-field-parser.ts @@ -75,9 +75,9 @@ export class OneboxFieldParser extends FieldParser { inputSelectGroup.group.push(new DsDynamicInputModel(inputModelConfig, clsInput)); return new DynamicQualdropModel(inputSelectGroup, clsGroup); - } else if (this.configData.selectableMetadata[0].authority) { + } else if (this.configData.selectableMetadata[0].controlledVocabulary) { const typeaheadModelConfig: DsDynamicTypeaheadModelConfig = this.initModel(null, label); - this.setAuthorityOptions(typeaheadModelConfig, this.parserOptions.authorityUuid); + this.setVocabularyOptions(typeaheadModelConfig, this.parserOptions.collectionUUID); this.setValues(typeaheadModelConfig, fieldValue, true); return new DynamicTypeaheadModel(typeaheadModelConfig); diff --git a/src/app/shared/form/builder/parsers/parser-options.ts b/src/app/shared/form/builder/parsers/parser-options.ts index f96ce0f2f3..e8ac4b49cd 100644 --- a/src/app/shared/form/builder/parsers/parser-options.ts +++ b/src/app/shared/form/builder/parsers/parser-options.ts @@ -1,5 +1,5 @@ export interface ParserOptions { readOnly: boolean; submissionScope: string; - authorityUuid: string + collectionUUID: string } diff --git a/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts b/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts index 84f3df0365..111193a637 100644 --- a/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts @@ -12,7 +12,7 @@ describe('RelationGroupFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', - authorityUuid: 'WORKSPACE' + collectionUUID: 'WORKSPACE' }; beforeEach(() => { diff --git a/src/app/shared/form/builder/parsers/relation-group-field-parser.ts b/src/app/shared/form/builder/parsers/relation-group-field-parser.ts index 01699d9e78..99dfcf777a 100644 --- a/src/app/shared/form/builder/parsers/relation-group-field-parser.ts +++ b/src/app/shared/form/builder/parsers/relation-group-field-parser.ts @@ -16,7 +16,7 @@ export class RelationGroupFieldParser extends FieldParser { const modelConfiguration: DynamicRelationGroupModelConfig = this.initModel(null, label); modelConfiguration.submissionId = this.submissionId; - modelConfiguration.scopeUUID = this.parserOptions.authorityUuid; + modelConfiguration.scopeUUID = this.parserOptions.collectionUUID; modelConfiguration.submissionScope = this.parserOptions.submissionScope; if (this.configData && this.configData.rows && this.configData.rows.length > 0) { modelConfiguration.formConfiguration = this.configData.rows; diff --git a/src/app/shared/form/builder/parsers/row-parser.spec.ts b/src/app/shared/form/builder/parsers/row-parser.spec.ts index 435c6a6426..63f343e60d 100644 --- a/src/app/shared/form/builder/parsers/row-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/row-parser.spec.ts @@ -35,7 +35,7 @@ describe('RowParser test suite', () => { selectableMetadata: [ { metadata: 'journal', - authority: 'JOURNALAuthority', + controlledVocabulary: 'JOURNALAuthority', closed: false } ], @@ -83,7 +83,7 @@ describe('RowParser test suite', () => { selectableMetadata: [ { metadata: 'title', - authority: 'EVENTAuthority', + controlledVocabulary: 'EVENTAuthority', closed: false } ], @@ -103,7 +103,7 @@ describe('RowParser test suite', () => { selectableMetadata: [ { metadata: 'title', - authority: 'EVENTAuthority', + controlledVocabulary: 'EVENTAuthority', closed: false } ], @@ -119,7 +119,7 @@ describe('RowParser test suite', () => { selectableMetadata: [ { metadata: 'otherTitle', - authority: 'EVENTAuthority', + controlledVocabulary: 'EVENTAuthority', closed: false } ], @@ -141,7 +141,7 @@ describe('RowParser test suite', () => { selectableMetadata: [ { metadata: 'type', - authority: 'common_types_dataset', + controlledVocabulary: 'common_types_dataset', closed: false } ], @@ -176,7 +176,7 @@ describe('RowParser test suite', () => { selectableMetadata: [ { metadata: 'author', - authority: 'RPAuthority', + controlledVocabulary: 'RPAuthority', closed: false } ], @@ -198,7 +198,7 @@ describe('RowParser test suite', () => { selectableMetadata: [ { metadata: 'type', - authority: 'type_programme', + controlledVocabulary: 'type_programme', closed: false } ], @@ -241,7 +241,7 @@ describe('RowParser test suite', () => { selectableMetadata: [ { metadata: 'subject', - authority: 'JOURNALAuthority', + controlledVocabulary: 'JOURNALAuthority', closed: false } ], diff --git a/src/app/shared/form/builder/parsers/row-parser.ts b/src/app/shared/form/builder/parsers/row-parser.ts index 4938b9859e..a7ea023569 100644 --- a/src/app/shared/form/builder/parsers/row-parser.ts +++ b/src/app/shared/form/builder/parsers/row-parser.ts @@ -5,7 +5,7 @@ import { } from '@ng-dynamic-forms/core'; import { uniqueId } from 'lodash'; -import { IntegrationSearchOptions } from '../../../../core/integration/models/integration-options.model'; +import { VocabularyFindOptions } from '../../../../core/submission/vocabularies/models/vocabulary-find-options.model'; import { isEmpty } from '../../../empty.util'; import { DynamicRowGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-group-model'; import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from '../ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; @@ -48,7 +48,7 @@ export class RowParser { group: [], }; - const authorityOptions = new IntegrationSearchOptions(scopeUUID); + const vocabularyOptions = new VocabularyFindOptions(scopeUUID); const scopedFields: FormFieldModel[] = this.filterScopedFields(rowData.fields, submissionScope); @@ -58,7 +58,7 @@ export class RowParser { const parserOptions: ParserOptions = { readOnly: readOnly, submissionScope: submissionScope, - authorityUuid: authorityOptions.uuid + collectionUUID: vocabularyOptions.collection }; // Iterate over row's fields diff --git a/src/app/shared/form/builder/parsers/series-field-parser.spec.ts b/src/app/shared/form/builder/parsers/series-field-parser.spec.ts index ceb4e96320..ca58dfb50a 100644 --- a/src/app/shared/form/builder/parsers/series-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/series-field-parser.spec.ts @@ -12,7 +12,7 @@ describe('SeriesFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', - authorityUuid: null + collectionUUID: null }; beforeEach(() => { diff --git a/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts b/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts index 90449e62e5..7c63235f67 100644 --- a/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts @@ -12,7 +12,7 @@ describe('TagFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', - authorityUuid: null + collectionUUID: null }; beforeEach(() => { @@ -27,7 +27,7 @@ describe('TagFieldParser test suite', () => { selectableMetadata: [ { metadata: 'subject', - authority: 'JOURNALAuthority', + controlledVocabulary: 'JOURNALAuthority', closed: false } ], diff --git a/src/app/shared/form/builder/parsers/tag-field-parser.ts b/src/app/shared/form/builder/parsers/tag-field-parser.ts index c1c39feb2b..08685e0e35 100644 --- a/src/app/shared/form/builder/parsers/tag-field-parser.ts +++ b/src/app/shared/form/builder/parsers/tag-field-parser.ts @@ -6,9 +6,9 @@ export class TagFieldParser extends FieldParser { public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any { const tagModelConfig: DynamicTagModelConfig = this.initModel(null, label); - if (this.configData.selectableMetadata[0].authority - && this.configData.selectableMetadata[0].authority.length > 0) { - this.setAuthorityOptions(tagModelConfig, this.parserOptions.authorityUuid); + if (this.configData.selectableMetadata[0].controlledVocabulary + && this.configData.selectableMetadata[0].controlledVocabulary.length > 0) { + this.setVocabularyOptions(tagModelConfig, this.parserOptions.collectionUUID); } this.setValues(tagModelConfig, fieldValue, null, true); diff --git a/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts b/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts index 167f126cf2..84acc9f239 100644 --- a/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts @@ -12,7 +12,7 @@ describe('TextareaFieldParser test suite', () => { const parserOptions: ParserOptions = { readOnly: false, submissionScope: null, - authorityUuid: null + collectionUUID: null }; beforeEach(() => { diff --git a/src/app/shared/mocks/form-models.mock.ts b/src/app/shared/mocks/form-models.mock.ts index e4f9ec3131..1965f8d709 100644 --- a/src/app/shared/mocks/form-models.mock.ts +++ b/src/app/shared/mocks/form-models.mock.ts @@ -1,16 +1,19 @@ +import { DynamicSelectModel } from '@ng-dynamic-forms/core'; + import { DsDynamicInputModel } from '../form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model'; import { DynamicQualdropModel } from '../form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model'; -import { DynamicRowArrayModel, DynamicRowArrayModelConfig } from '../form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model'; -import { DynamicSelectModel } from '@ng-dynamic-forms/core'; +import { + DynamicRowArrayModel, + DynamicRowArrayModelConfig +} from '../form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model'; import { SubmissionScopeType } from '../../core/submission/submission-scope-type'; import { DynamicRelationGroupModel } from '../form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; import { FormFieldModel } from '../form/builder/models/form-field.model'; -import { AuthorityOptions } from '../../core/integration/models/authority-options.model'; -import { AuthorityValue } from '../../core/integration/models/authority.value'; +import { VocabularyOptions } from '../../core/submission/vocabularies/models/vocabulary-options.model'; +import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model'; import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model'; import { DynamicRowGroupModel } from '../form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-group-model'; import { FormRowModel } from '../../core/config/models/config-submission-form.model'; -import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model'; export const qualdropSelectConfig = { name: 'dc.identifier_QUALDROP_METADATA', @@ -87,7 +90,7 @@ export const MockRowArrayQualdropModel: DynamicRowArrayModel = new DynamicRowArr const mockFormRowModel = { fields: [ { - input: {type: 'lookup'}, + input: { type: 'lookup' }, label: 'Journal', mandatory: 'false', repeatable: false, @@ -95,14 +98,14 @@ const mockFormRowModel = { selectableMetadata: [ { metadata: 'journal', - authority: 'JOURNALAuthority', + controlledVocabulary: 'JOURNALAuthority', closed: false } ], languageCodes: [] } as FormFieldModel, { - input: {type: 'onebox'}, + input: { type: 'onebox' }, label: 'Issue', mandatory: 'false', repeatable: false, @@ -142,7 +145,7 @@ const relationGroupConfig = { export const MockRelationModel: DynamicRelationGroupModel = new DynamicRelationGroupModel(relationGroupConfig); export const inputWithLanguageAndAuthorityConfig = { - authorityOptions: new AuthorityOptions('testAuthority', 'testWithAuthority', 'scope'), + vocabularyOptions: new VocabularyOptions('testAuthority', 'testWithAuthority', 'scope'), languageCodes: [ { display: 'English', @@ -159,10 +162,10 @@ export const inputWithLanguageAndAuthorityConfig = { readOnly: false, disabled: false, repeatable: false, - value: { + value: { value: 'testWithLanguageAndAuthority', display: 'testWithLanguageAndAuthority', - id: 'testWithLanguageAndAuthority', + authority: 'testWithLanguageAndAuthority', }, submissionId: '1234', metadataFields: [] @@ -195,7 +198,7 @@ export const inputWithLanguageConfig = { export const mockInputWithLanguageModel = new DsDynamicInputModel(inputWithLanguageConfig); export const inputWithLanguageAndAuthorityArrayConfig = { - authorityOptions: new AuthorityOptions('testAuthority', 'testWithAuthority', 'scope'), + vocabularyOptions: new VocabularyOptions('testAuthority', 'testWithAuthority', 'scope'), languageCodes: [ { display: 'English', @@ -215,7 +218,7 @@ export const inputWithLanguageAndAuthorityArrayConfig = { value: [{ value: 'testLanguageAndAuthorityArray', display: 'testLanguageAndAuthorityArray', - id: 'testLanguageAndAuthorityArray', + authority: 'testLanguageAndAuthorityArray', }], submissionId: '1234', metadataFields: [] @@ -242,7 +245,11 @@ export const inputWithAuthorityValueConfig = { readOnly: false, disabled: false, repeatable: false, - value: Object.assign({}, new AuthorityValue(), { value: 'testWithAuthorityValue', id: 'testWithAuthorityValue', display: 'testWithAuthorityValue' }), + value: Object.assign({}, new VocabularyEntry(), { + value: 'testWithAuthorityValue', + authority: 'testWithAuthorityValue', + display: 'testWithAuthorityValue' + }), submissionId: '1234', metadataFields: [] }; @@ -255,7 +262,7 @@ export const inputWithObjectValueConfig = { readOnly: false, disabled: false, repeatable: false, - value: { value: 'testWithObjectValue', id: 'testWithObjectValue', display: 'testWithObjectValue' }, + value: { value: 'testWithObjectValue', authority: 'testWithObjectValue', display: 'testWithObjectValue' }, submissionId: '1234', metadataFields: [] }; diff --git a/src/app/shared/mocks/submission.mock.ts b/src/app/shared/mocks/submission.mock.ts index 082eec4c71..8965a17eef 100644 --- a/src/app/shared/mocks/submission.mock.ts +++ b/src/app/shared/mocks/submission.mock.ts @@ -1254,7 +1254,7 @@ export const mockUploadConfigResponse = { { metadata: 'dc.title', label: null, - authority: null, + controlledVocabulary: null, closed: null } ], @@ -1276,7 +1276,7 @@ export const mockUploadConfigResponse = { { metadata: 'dc.description', label: null, - authority: null, + controlledVocabulary: null, closed: null } ], diff --git a/src/app/shared/testing/authority-service.stub.ts b/src/app/shared/testing/authority-service.stub.ts deleted file mode 100644 index 3a5d31ab0d..0000000000 --- a/src/app/shared/testing/authority-service.stub.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {of as observableOf, Observable } from 'rxjs'; -import { IntegrationSearchOptions } from '../../core/integration/models/integration-options.model'; -import { IntegrationData } from '../../core/integration/integration-data'; -import { PageInfo } from '../../core/shared/page-info.model'; -import { AuthorityValue } from '../../core/integration/models/authority.value'; - -export class AuthorityServiceStub { - - private _payload = [ - Object.assign(new AuthorityValue(),{id: 1, display: 'one', value: 1}), - Object.assign(new AuthorityValue(),{id: 2, display: 'two', value: 2}), - ]; - - setNewPayload(payload) { - this._payload = payload; - } - - getEntriesByName(options: IntegrationSearchOptions) { - return observableOf(new IntegrationData(new PageInfo(), this._payload)); - } -} diff --git a/src/app/shared/testing/vocabulary-service.stub.ts b/src/app/shared/testing/vocabulary-service.stub.ts new file mode 100644 index 0000000000..15528653c2 --- /dev/null +++ b/src/app/shared/testing/vocabulary-service.stub.ts @@ -0,0 +1,29 @@ +import { Observable } from 'rxjs'; + +import { VocabularyFindOptions } from '../../core/submission/vocabularies/models/vocabulary-find-options.model'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; +import { RemoteData } from '../../core/data/remote-data'; + + +export class VocabularyServiceStub { + + private _payload = [ + Object.assign(new VocabularyEntry(),{authority: 1, display: 'one', value: 1}), + Object.assign(new VocabularyEntry(),{authority: 2, display: 'two', value: 2}), + ]; + + setNewPayload(payload) { + this._payload = payload; + } + + getList() { + return this._payload + } + + getVocabularyEntries(options: VocabularyFindOptions): Observable>> { + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), this._payload)); + } +} diff --git a/src/app/submission/sections/form/section-form-operations.service.spec.ts b/src/app/submission/sections/form/section-form-operations.service.spec.ts index de8e7da7f9..bc1c17ddbd 100644 --- a/src/app/submission/sections/form/section-form-operations.service.spec.ts +++ b/src/app/submission/sections/form/section-form-operations.service.spec.ts @@ -27,7 +27,7 @@ import { mockRowGroupModel } from '../../../shared/mocks/form-models.mock'; import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; -import { AuthorityValue } from '../../../core/integration/models/authority.value'; +import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.model'; describe('SectionFormOperationsService test suite', () => { let formBuilderService: any; @@ -365,7 +365,7 @@ describe('SectionFormOperationsService test suite', () => { event = Object.assign({}, dynamicFormControlChangeEvent, { model: mockInputWithLanguageAndAuthorityModel }); - expectedValue = Object.assign(new AuthorityValue(), mockInputWithLanguageAndAuthorityModel.value, {language: mockInputWithLanguageAndAuthorityModel.language}); + expectedValue = Object.assign(new VocabularyEntry(), mockInputWithLanguageAndAuthorityModel.value, {language: mockInputWithLanguageAndAuthorityModel.language}); expect(service.getFieldValueFromChangeEvent(event)).toEqual(expectedValue); @@ -373,7 +373,7 @@ describe('SectionFormOperationsService test suite', () => { model: mockInputWithLanguageAndAuthorityArrayModel }); expectedValue = [ - Object.assign(new AuthorityValue(), mockInputWithLanguageAndAuthorityArrayModel.value[0], + Object.assign(new VocabularyEntry(), mockInputWithLanguageAndAuthorityArrayModel.value[0], { language: mockInputWithLanguageAndAuthorityArrayModel.language } ) ]; diff --git a/src/app/submission/sections/form/section-form-operations.service.ts b/src/app/submission/sections/form/section-form-operations.service.ts index 2d6b1c5477..a28536a0a7 100644 --- a/src/app/submission/sections/form/section-form-operations.service.ts +++ b/src/app/submission/sections/form/section-form-operations.service.ts @@ -15,11 +15,12 @@ import { FormFieldPreviousValueObject } from '../../../shared/form/builder/model import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; import { FormFieldLanguageValueObject } from '../../../shared/form/builder/models/form-field-language-value.model'; import { DsDynamicInputModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model'; -import { AuthorityValue } from '../../../core/integration/models/authority.value'; +import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.model'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; import { DynamicQualdropModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model'; import { DynamicRelationGroupModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; +import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; /** * The service handling all form section operations @@ -221,18 +222,19 @@ export class SectionFormOperationsService { if ((event.model as DsDynamicInputModel).hasAuthority) { if (Array.isArray(value)) { value.forEach((authority, index) => { - authority = Object.assign(new AuthorityValue(), authority, { language }); + authority = Object.assign(new VocabularyEntry(), authority, { language }); value[index] = authority; }); fieldValue = value; } else { - fieldValue = Object.assign(new AuthorityValue(), value, { language }); + fieldValue = Object.assign(new VocabularyEntry(), value, { language }); } } else { // Language without Authority (input, textArea) fieldValue = new FormFieldMetadataValueObject(value, language); } - } else if (value instanceof FormFieldLanguageValueObject || value instanceof AuthorityValue || isObject(value)) { + } else if (value instanceof FormFieldLanguageValueObject || value instanceof VocabularyEntry + || value instanceof VocabularyEntryDetail || isObject(value)) { fieldValue = value; } else { fieldValue = new FormFieldMetadataValueObject(value); From f71fd5737a82be56dcbf8cd8929e47073a166224 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 25 Jun 2020 15:22:20 +0200 Subject: [PATCH 06/62] [CST-3088] Removed authority and integration services --- src/app/core/cache/response.models.ts | 14 +- src/app/core/data/request.models.ts | 38 ++- src/app/core/integration/authority.service.ts | 21 -- src/app/core/integration/integration-data.ts | 12 - ...tegration-response-parsing.service.spec.ts | 221 ------------------ .../integration-response-parsing.service.ts | 50 ---- .../integration/integration.service.spec.ts | 96 -------- .../core/integration/integration.service.ts | 121 ---------- .../models/authority-options.model.ts | 16 -- .../models/authority.resource-type.ts | 10 - .../integration/models/authority.value.ts | 92 -------- .../models/integration-options.model.ts | 14 -- .../integration/models/integration.model.ts | 22 -- 13 files changed, 16 insertions(+), 711 deletions(-) delete mode 100644 src/app/core/integration/authority.service.ts delete mode 100644 src/app/core/integration/integration-data.ts delete mode 100644 src/app/core/integration/integration-response-parsing.service.spec.ts delete mode 100644 src/app/core/integration/integration-response-parsing.service.ts delete mode 100644 src/app/core/integration/integration.service.spec.ts delete mode 100644 src/app/core/integration/integration.service.ts delete mode 100644 src/app/core/integration/models/authority-options.model.ts delete mode 100644 src/app/core/integration/models/authority.resource-type.ts delete mode 100644 src/app/core/integration/models/authority.value.ts delete mode 100644 src/app/core/integration/models/integration-options.model.ts delete mode 100644 src/app/core/integration/models/integration.model.ts diff --git a/src/app/core/cache/response.models.ts b/src/app/core/cache/response.models.ts index 3f46ecf647..00f1bffdeb 100644 --- a/src/app/core/cache/response.models.ts +++ b/src/app/core/cache/response.models.ts @@ -5,11 +5,9 @@ import { PageInfo } from '../shared/page-info.model'; import { ConfigObject } from '../config/models/config.model'; import { FacetValue } from '../../shared/search/facet-value.model'; import { SearchFilterConfig } from '../../shared/search/search-filter-config.model'; -import { IntegrationModel } from '../integration/models/integration.model'; import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model'; import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model'; import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model'; -import { PaginatedList } from '../data/paginated-list'; import { SubmissionObject } from '../submission/models/submission-object.model'; import { DSpaceObject } from '../shared/dspace-object.model'; import { MetadataSchema } from '../metadata/metadata-schema.model'; @@ -211,17 +209,6 @@ export class AuthStatusResponse extends RestResponse { } } -export class IntegrationSuccessResponse extends RestResponse { - constructor( - public dataDefinition: PaginatedList, - public statusCode: number, - public statusText: string, - public pageInfo?: PageInfo - ) { - super(true, statusCode, statusText); - } -} - export class PostPatchSuccessResponse extends RestResponse { constructor( public dataDefinition: any, @@ -302,4 +289,5 @@ export class ContentSourceSuccessResponse extends RestResponse { super(true, statusCode, statusText); } } + /* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index c9fe1fe0ce..a1f77d9cbb 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -9,7 +9,6 @@ import { ConfigResponseParsingService } from '../config/config-response-parsing. import { AuthResponseParsingService } from '../auth/auth-response-parsing.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { SubmissionResponseParsingService } from '../submission/submission-response-parsing.service'; -import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service'; import { RestRequestMethod } from './rest-request-method'; import { RequestParam } from '../cache/models/request-param.model'; import { EpersonResponseParsingService } from '../eperson/eperson-response-parsing.service'; @@ -26,13 +25,14 @@ import { VocabularyEntriesResponseParsingService } from '../submission/vocabular // uuid and handle requests have separate endpoints export enum IdentifierType { - UUID ='uuid', + UUID = 'uuid', HANDLE = 'handle' } export abstract class RestRequest { public responseMsToLive = 10 * 1000; public forceBypassCache = false; + constructor( public uuid: string, public href: string, @@ -42,13 +42,13 @@ export abstract class RestRequest { ) { } - getResponseParser(): GenericConstructor { - return DSOResponseParsingService; - } - get toCache(): boolean { return this.responseMsToLive > 0; } + + getResponseParser(): GenericConstructor { + return DSOResponseParsingService; + } } export class GetRequest extends RestRequest { @@ -59,7 +59,7 @@ export class GetRequest extends RestRequest { public href: string, public body?: any, public options?: HttpOptions - ) { + ) { super(uuid, href, RestRequestMethod.GET, body, options) } } @@ -70,7 +70,7 @@ export class PostRequest extends RestRequest { public href: string, public body?: any, public options?: HttpOptions - ) { + ) { super(uuid, href, RestRequestMethod.POST, body) } } @@ -81,7 +81,7 @@ export class PutRequest extends RestRequest { public href: string, public body?: any, public options?: HttpOptions - ) { + ) { super(uuid, href, RestRequestMethod.PUT, body) } } @@ -92,7 +92,7 @@ export class DeleteRequest extends RestRequest { public href: string, public body?: any, public options?: HttpOptions - ) { + ) { super(uuid, href, RestRequestMethod.DELETE, body) } } @@ -103,7 +103,7 @@ export class OptionsRequest extends RestRequest { public href: string, public body?: any, public options?: HttpOptions - ) { + ) { super(uuid, href, RestRequestMethod.OPTIONS, body) } } @@ -114,7 +114,7 @@ export class HeadRequest extends RestRequest { public href: string, public body?: any, public options?: HttpOptions - ) { + ) { super(uuid, href, RestRequestMethod.HEAD, body) } } @@ -127,7 +127,7 @@ export class PatchRequest extends RestRequest { public href: string, public body?: any, public options?: HttpOptions - ) { + ) { super(uuid, href, RestRequestMethod.PATCH, body) } } @@ -242,16 +242,6 @@ export class AuthGetRequest extends GetRequest { } } -export class IntegrationRequest extends GetRequest { - constructor(uuid: string, href: string) { - super(uuid, href); - } - - getResponseParser(): GenericConstructor { - return IntegrationResponseParsingService; - } -} - /** * Request to create a MetadataSchema */ @@ -309,6 +299,7 @@ export class UpdateMetadataFieldRequest extends PutRequest { */ export class SubmissionRequest extends GetRequest { forceBypassCache = true; + constructor(uuid: string, href: string) { super(uuid, href); } @@ -456,4 +447,5 @@ export class RequestError extends Error { statusCode: number; statusText: string; } + /* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/integration/authority.service.ts b/src/app/core/integration/authority.service.ts deleted file mode 100644 index f0a1759be6..0000000000 --- a/src/app/core/integration/authority.service.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Injectable } from '@angular/core'; - -import { RequestService } from '../data/request.service'; -import { IntegrationService } from './integration.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; - -@Injectable() -export class AuthorityService extends IntegrationService { - protected linkPath = 'authorities'; - protected entriesEndpoint = 'entries'; - protected entryValueEndpoint = 'entryValues'; - - constructor( - protected requestService: RequestService, - protected rdbService: RemoteDataBuildService, - protected halService: HALEndpointService) { - super(); - } - -} diff --git a/src/app/core/integration/integration-data.ts b/src/app/core/integration/integration-data.ts deleted file mode 100644 index b93ce36dad..0000000000 --- a/src/app/core/integration/integration-data.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { PageInfo } from '../shared/page-info.model'; -import { IntegrationModel } from './models/integration.model'; - -/** - * A class to represent the data retrieved by an Integration service - */ -export class IntegrationData { - constructor( - public pageInfo: PageInfo, - public payload: IntegrationModel[] - ) { } -} diff --git a/src/app/core/integration/integration-response-parsing.service.spec.ts b/src/app/core/integration/integration-response-parsing.service.spec.ts deleted file mode 100644 index b5cb8c4dc4..0000000000 --- a/src/app/core/integration/integration-response-parsing.service.spec.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { Store } from '@ngrx/store'; - -import { ObjectCacheService } from '../cache/object-cache.service'; -import { ErrorResponse, IntegrationSuccessResponse } from '../cache/response.models'; -import { CoreState } from '../core.reducers'; -import { PaginatedList } from '../data/paginated-list'; -import { IntegrationRequest } from '../data/request.models'; -import { PageInfo } from '../shared/page-info.model'; -import { IntegrationResponseParsingService } from './integration-response-parsing.service'; -import { AuthorityValue } from './models/authority.value'; - -describe('IntegrationResponseParsingService', () => { - let service: IntegrationResponseParsingService; - - const store = {} as Store; - const objectCacheService = new ObjectCacheService(store, undefined); - const name = 'type'; - const metadata = 'dc.type'; - const query = ''; - const uuid = 'd9d30c0c-69b7-4369-8397-ca67c888974d'; - const integrationEndpoint = 'https://rest.api/integration/authorities'; - const entriesEndpoint = `${integrationEndpoint}/${name}/entries?query=${query}&metadata=${metadata}&uuid=${uuid}`; - let validRequest; - - let validResponse; - - let invalidResponse1; - let invalidResponse2; - let pageInfo; - let definitions; - - function initVars() { - pageInfo = Object.assign(new PageInfo(), { - elementsPerPage: 5, - totalElements: 5, - totalPages: 1, - currentPage: 1, - _links: { - self: { href: 'https://rest.api/integration/authorities/type/entries' } - } - }); - definitions = new PaginatedList(pageInfo, [ - Object.assign(new AuthorityValue(), { - type: 'authority', - display: 'One', - id: 'One', - otherInformation: undefined, - value: 'One' - }), - Object.assign(new AuthorityValue(), { - type: 'authority', - display: 'Two', - id: 'Two', - otherInformation: undefined, - value: 'Two' - }), - Object.assign(new AuthorityValue(), { - type: 'authority', - display: 'Three', - id: 'Three', - otherInformation: undefined, - value: 'Three' - }), - Object.assign(new AuthorityValue(), { - type: 'authority', - display: 'Four', - id: 'Four', - otherInformation: undefined, - value: 'Four' - }), - Object.assign(new AuthorityValue(), { - type: 'authority', - display: 'Five', - id: 'Five', - otherInformation: undefined, - value: 'Five' - }) - ]); - validRequest = new IntegrationRequest('69f375b5-19f4-4453-8c7a-7dc5c55aafbb', entriesEndpoint); - - validResponse = { - payload: { - page: { - number: 0, - size: 5, - totalElements: 5, - totalPages: 1 - }, - _embedded: { - authorityEntries: [ - { - display: 'One', - id: 'One', - otherInformation: {}, - type: 'authority', - value: 'One' - }, - { - display: 'Two', - id: 'Two', - otherInformation: {}, - type: 'authority', - value: 'Two' - }, - { - display: 'Three', - id: 'Three', - otherInformation: {}, - type: 'authority', - value: 'Three' - }, - { - display: 'Four', - id: 'Four', - otherInformation: {}, - type: 'authority', - value: 'Four' - }, - { - display: 'Five', - id: 'Five', - otherInformation: {}, - type: 'authority', - value: 'Five' - }, - ], - - }, - _links: { - self: { href: 'https://rest.api/integration/authorities/type/entries' } - } - }, - statusCode: 200, - statusText: 'OK' - }; - - invalidResponse1 = { - payload: {}, - statusCode: 400, - statusText: 'Bad Request' - }; - - invalidResponse2 = { - payload: { - page: { - number: 0, - size: 5, - totalElements: 5, - totalPages: 1 - }, - _embedded: { - authorityEntries: [ - { - display: 'One', - id: 'One', - otherInformation: {}, - type: 'authority', - value: 'One' - }, - { - display: 'Two', - id: 'Two', - otherInformation: {}, - type: 'authority', - value: 'Two' - }, - { - display: 'Three', - id: 'Three', - otherInformation: {}, - type: 'authority', - value: 'Three' - }, - { - display: 'Four', - id: 'Four', - otherInformation: {}, - type: 'authority', - value: 'Four' - }, - { - display: 'Five', - id: 'Five', - otherInformation: {}, - type: 'authority', - value: 'Five' - }, - ], - - }, - _links: {} - }, - statusCode: 500, - statusText: 'Internal Server Error' - }; - } - beforeEach(() => { - initVars(); - service = new IntegrationResponseParsingService(objectCacheService); - }); - - describe('parse', () => { - it('should return a IntegrationSuccessResponse if data contains a valid endpoint response', () => { - const response = service.parse(validRequest, validResponse); - expect(response.constructor).toBe(IntegrationSuccessResponse); - }); - - it('should return an ErrorResponse if data contains an invalid config endpoint response', () => { - const response1 = service.parse(validRequest, invalidResponse1); - const response2 = service.parse(validRequest, invalidResponse2); - expect(response1.constructor).toBe(ErrorResponse); - expect(response2.constructor).toBe(ErrorResponse); - }); - - it('should return a IntegrationSuccessResponse with data definition', () => { - const response = service.parse(validRequest, validResponse); - expect((response as any).dataDefinition).toEqual(definitions); - }); - - }); -}); diff --git a/src/app/core/integration/integration-response-parsing.service.ts b/src/app/core/integration/integration-response-parsing.service.ts deleted file mode 100644 index 2719669bae..0000000000 --- a/src/app/core/integration/integration-response-parsing.service.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Inject, Injectable } from '@angular/core'; -import { RestRequest } from '../data/request.models'; -import { ResponseParsingService } from '../data/parsing.service'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { ErrorResponse, IntegrationSuccessResponse, RestResponse } from '../cache/response.models'; -import { isNotEmpty } from '../../shared/empty.util'; - -import { BaseResponseParsingService } from '../data/base-response-parsing.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { IntegrationModel } from './models/integration.model'; -import { AuthorityValue } from './models/authority.value'; -import { PaginatedList } from '../data/paginated-list'; - -@Injectable() -export class IntegrationResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { - - protected toCache = true; - - constructor( - protected objectCache: ObjectCacheService, - ) { - super(); - } - - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) { - const dataDefinition = this.process(data.payload, request); - return new IntegrationSuccessResponse(this.processResponse(dataDefinition), data.statusCode, data.statusText, this.processPageInfo(data.payload)); - } else { - return new ErrorResponse( - Object.assign( - new Error('Unexpected response from Integration endpoint'), - {statusCode: data.statusCode, statusText: data.statusText} - ) - ); - } - } - - protected processResponse(data: PaginatedList): any { - const returnList = Array.of(); - data.page.forEach((item, index) => { - if (item.type === AuthorityValue.type.value) { - data.page[index] = Object.assign(new AuthorityValue(), item); - } - }); - - return data; - } - -} diff --git a/src/app/core/integration/integration.service.spec.ts b/src/app/core/integration/integration.service.spec.ts deleted file mode 100644 index 148a5df7b8..0000000000 --- a/src/app/core/integration/integration.service.spec.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { cold, getTestScheduler } from 'jasmine-marbles'; -import { TestScheduler } from 'rxjs/testing'; -import { getMockRequestService } from '../../shared/mocks/request.service.mock'; - -import { RequestService } from '../data/request.service'; -import { IntegrationRequest } from '../data/request.models'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; -import { IntegrationService } from './integration.service'; -import { IntegrationSearchOptions } from './models/integration-options.model'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; - -const LINK_NAME = 'authorities'; -const ENTRIES = 'entries'; -const ENTRY_VALUE = 'entryValue'; - -class TestService extends IntegrationService { - protected linkPath = LINK_NAME; - protected entriesEndpoint = ENTRIES; - protected entryValueEndpoint = ENTRY_VALUE; - - constructor( - protected requestService: RequestService, - protected rdbService: RemoteDataBuildService, - protected halService: HALEndpointService) { - super(); - } -} - -describe('IntegrationService', () => { - let scheduler: TestScheduler; - let service: TestService; - let requestService: RequestService; - let rdbService: RemoteDataBuildService; - let halService: any; - let findOptions: IntegrationSearchOptions; - - const name = 'type'; - const metadata = 'dc.type'; - const query = ''; - const value = 'test'; - const uuid = 'd9d30c0c-69b7-4369-8397-ca67c888974d'; - const integrationEndpoint = 'https://rest.api/integration'; - const serviceEndpoint = `${integrationEndpoint}/${LINK_NAME}`; - const entriesEndpoint = `${serviceEndpoint}/${name}/entries?query=${query}&metadata=${metadata}&uuid=${uuid}`; - const entryValueEndpoint = `${serviceEndpoint}/${name}/entryValue/${value}?metadata=${metadata}`; - - findOptions = new IntegrationSearchOptions(uuid, name, metadata); - - function initTestService(): TestService { - return new TestService( - requestService, - rdbService, - halService - ); - } - - beforeEach(() => { - requestService = getMockRequestService(); - rdbService = getMockRemoteDataBuildService(); - scheduler = getTestScheduler(); - halService = new HALEndpointServiceStub(integrationEndpoint); - findOptions = new IntegrationSearchOptions(uuid, name, metadata, query); - service = initTestService(); - - }); - - describe('getEntriesByName', () => { - - it('should configure a new IntegrationRequest', () => { - const expected = new IntegrationRequest(requestService.generateRequestId(), entriesEndpoint); - scheduler.schedule(() => service.getEntriesByName(findOptions).subscribe()); - scheduler.flush(); - - expect(requestService.configure).toHaveBeenCalledWith(expected); - }); - }); - - describe('getEntryByValue', () => { - - it('should configure a new IntegrationRequest', () => { - findOptions = new IntegrationSearchOptions( - null, - name, - metadata, - value); - - const expected = new IntegrationRequest(requestService.generateRequestId(), entryValueEndpoint); - scheduler.schedule(() => service.getEntryByValue(findOptions).subscribe()); - scheduler.flush(); - - expect(requestService.configure).toHaveBeenCalledWith(expected); - }); - }); -}); diff --git a/src/app/core/integration/integration.service.ts b/src/app/core/integration/integration.service.ts deleted file mode 100644 index 5826f4646d..0000000000 --- a/src/app/core/integration/integration.service.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs'; -import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators'; -import { RequestService } from '../data/request.service'; -import { IntegrationSuccessResponse } from '../cache/response.models'; -import { GetRequest, IntegrationRequest } from '../data/request.models'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { IntegrationData } from './integration-data'; -import { IntegrationSearchOptions } from './models/integration-options.model'; -import { getResponseFromEntry } from '../shared/operators'; - -export abstract class IntegrationService { - protected request: IntegrationRequest; - protected abstract requestService: RequestService; - protected abstract linkPath: string; - protected abstract entriesEndpoint: string; - protected abstract entryValueEndpoint: string; - protected abstract halService: HALEndpointService; - - protected getData(request: GetRequest): Observable { - return this.requestService.getByHref(request.href).pipe( - getResponseFromEntry(), - mergeMap((response: IntegrationSuccessResponse) => { - if (response.isSuccessful && isNotEmpty(response)) { - return observableOf(new IntegrationData( - response.pageInfo, - (response.dataDefinition) ? response.dataDefinition.page : [] - )); - } else if (!response.isSuccessful) { - return observableThrowError(new Error(`Couldn't retrieve the integration data`)); - } - }), - distinctUntilChanged() - ); - } - - protected getEntriesHref(endpoint, options: IntegrationSearchOptions = new IntegrationSearchOptions()): string { - let result; - const args = []; - - if (hasValue(options.name)) { - result = `${endpoint}/${options.name}/${this.entriesEndpoint}`; - } else { - result = endpoint; - } - - if (hasValue(options.query)) { - args.push(`query=${options.query}`); - } - - if (hasValue(options.metadata)) { - args.push(`metadata=${options.metadata}`); - } - - if (hasValue(options.uuid)) { - args.push(`uuid=${options.uuid}`); - } - - if (hasValue(options.currentPage) && typeof options.currentPage === 'number') { - /* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */ - args.push(`page=${options.currentPage - 1}`); - } - - if (hasValue(options.elementsPerPage)) { - args.push(`size=${options.elementsPerPage}`); - } - - if (hasValue(options.sort)) { - args.push(`sort=${options.sort.field},${options.sort.direction}`); - } - - if (isNotEmpty(args)) { - result = `${result}?${args.join('&')}`; - } - return result; - } - - protected getEntryValueHref(endpoint, options: IntegrationSearchOptions = new IntegrationSearchOptions()): string { - let result; - const args = []; - - if (hasValue(options.name) && hasValue(options.query)) { - result = `${endpoint}/${options.name}/${this.entryValueEndpoint}/${options.query}`; - } else { - result = endpoint; - } - - if (hasValue(options.metadata)) { - args.push(`metadata=${options.metadata}`); - } - - if (isNotEmpty(args)) { - result = `${result}?${args.join('&')}`; - } - - return result; - } - - public getEntriesByName(options: IntegrationSearchOptions): Observable { - return this.halService.getEndpoint(this.linkPath).pipe( - map((endpoint: string) => this.getEntriesHref(endpoint, options)), - filter((href: string) => isNotEmpty(href)), - distinctUntilChanged(), - map((endpointURL: string) => new IntegrationRequest(this.requestService.generateRequestId(), endpointURL)), - tap((request: GetRequest) => this.requestService.configure(request)), - mergeMap((request: GetRequest) => this.getData(request)), - distinctUntilChanged()); - } - - public getEntryByValue(options: IntegrationSearchOptions): Observable { - return this.halService.getEndpoint(this.linkPath).pipe( - map((endpoint: string) => this.getEntryValueHref(endpoint, options)), - filter((href: string) => isNotEmpty(href)), - distinctUntilChanged(), - map((endpointURL: string) => new IntegrationRequest(this.requestService.generateRequestId(), endpointURL)), - tap((request: GetRequest) => this.requestService.configure(request)), - mergeMap((request: GetRequest) => this.getData(request)), - distinctUntilChanged()); - } - -} diff --git a/src/app/core/integration/models/authority-options.model.ts b/src/app/core/integration/models/authority-options.model.ts deleted file mode 100644 index 0b826f7f9c..0000000000 --- a/src/app/core/integration/models/authority-options.model.ts +++ /dev/null @@ -1,16 +0,0 @@ -export class AuthorityOptions { - name: string; - metadata: string; - scope: string; - closed: boolean; - - constructor(name: string, - metadata: string, - scope: string, - closed: boolean = false) { - this.name = name; - this.metadata = metadata; - this.scope = scope; - this.closed = closed; - } -} diff --git a/src/app/core/integration/models/authority.resource-type.ts b/src/app/core/integration/models/authority.resource-type.ts deleted file mode 100644 index ec87ddc85f..0000000000 --- a/src/app/core/integration/models/authority.resource-type.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ResourceType } from '../../shared/resource-type'; - -/** - * The resource type for AuthorityValue - * - * Needs to be in a separate file to prevent circular - * dependencies in webpack. - */ - -export const AUTHORITY_VALUE = new ResourceType('authority'); diff --git a/src/app/core/integration/models/authority.value.ts b/src/app/core/integration/models/authority.value.ts deleted file mode 100644 index 4e0183603b..0000000000 --- a/src/app/core/integration/models/authority.value.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; -import { isNotEmpty } from '../../../shared/empty.util'; -import { PLACEHOLDER_PARENT_METADATA } from '../../../shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; -import { OtherInformation } from '../../../shared/form/builder/models/form-field-metadata-value.model'; -import { typedObject } from '../../cache/builders/build-decorators'; -import { HALLink } from '../../shared/hal-link.model'; -import { MetadataValueInterface } from '../../shared/metadata.models'; -import { AUTHORITY_VALUE } from './authority.resource-type'; -import { IntegrationModel } from './integration.model'; - -/** - * Class representing an authority object - */ -@typedObject -@inheritSerialization(IntegrationModel) -export class AuthorityValue extends IntegrationModel implements MetadataValueInterface { - static type = AUTHORITY_VALUE; - - /** - * The identifier of this authority - */ - @autoserialize - id: string; - - /** - * The display value of this authority - */ - @autoserialize - display: string; - - /** - * The value of this authority - */ - @autoserialize - value: string; - - /** - * An object containing additional information related to this authority - */ - @autoserialize - otherInformation: OtherInformation; - - /** - * The language code of this authority value - */ - @autoserialize - language: string; - - /** - * The {@link HALLink}s for this AuthorityValue - */ - @deserialize - _links: { - self: HALLink, - }; - - /** - * This method checks if authority has an identifier value - * - * @return boolean - */ - hasAuthority(): boolean { - return isNotEmpty(this.id); - } - - /** - * This method checks if authority has a value - * - * @return boolean - */ - hasValue(): boolean { - return isNotEmpty(this.value); - } - - /** - * This method checks if authority has related information object - * - * @return boolean - */ - hasOtherInformation(): boolean { - return isNotEmpty(this.otherInformation); - } - - /** - * This method checks if authority has a placeholder as value - * - * @return boolean - */ - hasPlaceholder(): boolean { - return this.hasValue() && this.value === PLACEHOLDER_PARENT_METADATA; - } -} diff --git a/src/app/core/integration/models/integration-options.model.ts b/src/app/core/integration/models/integration-options.model.ts deleted file mode 100644 index 5f158bd47c..0000000000 --- a/src/app/core/integration/models/integration-options.model.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { SortOptions } from '../../cache/models/sort-options.model'; - -export class IntegrationSearchOptions { - - constructor(public uuid: string = '', - public name: string = '', - public metadata: string = '', - public query: string = '', - public elementsPerPage?: number, - public currentPage?: number, - public sort?: SortOptions) { - - } -} diff --git a/src/app/core/integration/models/integration.model.ts b/src/app/core/integration/models/integration.model.ts deleted file mode 100644 index d2f21a70c0..0000000000 --- a/src/app/core/integration/models/integration.model.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { autoserialize, deserialize } from 'cerialize'; -import { CacheableObject } from '../../cache/object-cache.reducer'; -import { HALLink } from '../../shared/hal-link.model'; - -export abstract class IntegrationModel implements CacheableObject { - - @autoserialize - self: string; - - @autoserialize - uuid: string; - - @autoserialize - public type: any; - - @deserialize - public _links: { - self: HALLink, - [name: string]: HALLink - } - -} From df26b85c5018374edb8c1c22c9a84376a1b6ac86 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 29 Jun 2020 22:15:02 +0200 Subject: [PATCH 07/62] [CST-3088] Create abstract response parsing for entries --- ...browse-entries-response-parsing.service.ts | 34 ++++----------- .../data/entries-response-parsing.service.ts | 42 +++++++++++++++++++ ...bulary-entries-response-parsing.service.ts | 11 ++++- 3 files changed, 59 insertions(+), 28 deletions(-) create mode 100644 src/app/core/data/entries-response-parsing.service.ts diff --git a/src/app/core/data/browse-entries-response-parsing.service.ts b/src/app/core/data/browse-entries-response-parsing.service.ts index 98385f0237..1009a07bca 100644 --- a/src/app/core/data/browse-entries-response-parsing.service.ts +++ b/src/app/core/data/browse-entries-response-parsing.service.ts @@ -1,40 +1,22 @@ -import { Inject, Injectable } from '@angular/core'; -import { isNotEmpty } from '../../shared/empty.util'; +import { Injectable } from '@angular/core'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { ErrorResponse, GenericSuccessResponse, RestResponse } from '../cache/response.models'; -import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; -import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; import { BrowseEntry } from '../shared/browse-entry.model'; -import { BaseResponseParsingService } from './base-response-parsing.service'; -import { ResponseParsingService } from './parsing.service'; -import { RestRequest } from './request.models'; +import { EntriesResponseParsingService } from './entries-response-parsing.service'; +import { GenericConstructor } from '../shared/generic-constructor'; @Injectable() -export class BrowseEntriesResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { +export class BrowseEntriesResponseParsingService extends EntriesResponseParsingService { protected toCache = false; constructor( protected objectCache: ObjectCacheService, - ) { super(); + ) { + super(objectCache); } - parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - if (isNotEmpty(data.payload)) { - let browseEntries = []; - if (isNotEmpty(data.payload._embedded) && Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) { - const serializer = new DSpaceSerializer(BrowseEntry); - browseEntries = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]); - } - return new GenericSuccessResponse(browseEntries, data.statusCode, data.statusText, this.processPageInfo(data.payload)); - } else { - return new ErrorResponse( - Object.assign( - new Error('Unexpected response from browse endpoint'), - { statusCode: data.statusCode, statusText: data.statusText } - ) - ); - } + getSerializerModel(): GenericConstructor { + return BrowseEntry; } } diff --git a/src/app/core/data/entries-response-parsing.service.ts b/src/app/core/data/entries-response-parsing.service.ts new file mode 100644 index 0000000000..2f49eb3871 --- /dev/null +++ b/src/app/core/data/entries-response-parsing.service.ts @@ -0,0 +1,42 @@ +import { isNotEmpty } from '../../shared/empty.util'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { ErrorResponse, GenericSuccessResponse, RestResponse } from '../cache/response.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { DSpaceSerializer } from '../dspace-rest-v2/dspace.serializer'; +import { BaseResponseParsingService } from './base-response-parsing.service'; +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; +import { CacheableObject } from '../cache/object-cache.reducer'; +import { GenericConstructor } from '../shared/generic-constructor'; + +export abstract class EntriesResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { + + protected toCache = false; + + constructor( + protected objectCache: ObjectCacheService, + ) { + super(); + } + + abstract getSerializerModel(): GenericConstructor; + + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + if (isNotEmpty(data.payload)) { + let browseEntries = []; + if (isNotEmpty(data.payload._embedded) && Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) { + const serializer = new DSpaceSerializer(this.getSerializerModel()); + browseEntries = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]); + } + return new GenericSuccessResponse(browseEntries, data.statusCode, data.statusText, this.processPageInfo(data.payload)); + } else { + return new ErrorResponse( + Object.assign( + new Error('Unexpected response from browse endpoint'), + { statusCode: data.statusCode, statusText: data.statusText } + ) + ); + } + } + +} diff --git a/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.ts b/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.ts index 680b949733..1e5b67b261 100644 --- a/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.ts +++ b/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.ts @@ -1,13 +1,15 @@ import { Injectable } from '@angular/core'; import { ObjectCacheService } from '../../cache/object-cache.service'; -import { BrowseEntriesResponseParsingService } from '../../data/browse-entries-response-parsing.service'; +import { VocabularyEntry } from './models/vocabulary-entry.model'; +import { EntriesResponseParsingService } from '../../data/entries-response-parsing.service'; +import { GenericConstructor } from '../../shared/generic-constructor'; /** * A service responsible for parsing data for a vocabulary entries response */ @Injectable() -export class VocabularyEntriesResponseParsingService extends BrowseEntriesResponseParsingService { +export class VocabularyEntriesResponseParsingService extends EntriesResponseParsingService { protected toCache = false; @@ -17,4 +19,9 @@ export class VocabularyEntriesResponseParsingService extends BrowseEntriesRespon super(objectCache); } + getSerializerModel(): GenericConstructor { + return VocabularyEntry; + } + + } From 4cc1a3eecdc2eab2f8fde5ac0cf55c0d51958824 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 29 Jun 2020 22:18:10 +0200 Subject: [PATCH 08/62] [CST-3088] Refactored VocabularyService to use VocabularyOptions and added more methods --- .../submission-response-parsing.service.ts | 6 +- .../models/vocabulary-find-options.model.ts | 15 +- .../vocabularies/vocabulary.service.spec.ts | 116 ++++++++++++-- .../vocabularies/vocabulary.service.ts | 143 ++++++++++++++++-- .../shared/testing/vocabulary-service.stub.ts | 22 ++- 5 files changed, 255 insertions(+), 47 deletions(-) diff --git a/src/app/core/submission/submission-response-parsing.service.ts b/src/app/core/submission/submission-response-parsing.service.ts index afabde831a..b740cde639 100644 --- a/src/app/core/submission/submission-response-parsing.service.ts +++ b/src/app/core/submission/submission-response-parsing.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { deepClone } from 'fast-json-patch'; import { DSOResponseParsingService } from '../data/dso-response-parsing.service'; @@ -113,7 +113,7 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService return new ErrorResponse( Object.assign( new Error('Unexpected response from server'), - {statusCode: data.statusCode, statusText: data.statusText} + { statusCode: data.statusCode, statusText: data.statusText } ) ); } @@ -133,7 +133,7 @@ export class SubmissionResponseParsingService extends BaseResponseParsingService processedList.forEach((item) => { - item = Object.assign({}, item); + // item = Object.assign({}, item); // In case data is an Instance of WorkspaceItem normalize field value of all the section of type form if (item instanceof WorkspaceItem || item instanceof WorkflowItem) { diff --git a/src/app/core/submission/vocabularies/models/vocabulary-find-options.model.ts b/src/app/core/submission/vocabularies/models/vocabulary-find-options.model.ts index 18309ed1fe..9dc12fad57 100644 --- a/src/app/core/submission/vocabularies/models/vocabulary-find-options.model.ts +++ b/src/app/core/submission/vocabularies/models/vocabulary-find-options.model.ts @@ -8,16 +8,15 @@ import { isNotEmpty } from '../../../../shared/empty.util'; */ export class VocabularyFindOptions extends FindListOptions { - constructor(public collection: string = '', - public name: string = '', - public metadata: string = '', + constructor(public collection, + public metadata, public query: string = '', + public filter?: string, + public exact?: boolean, + public entryID?: string, public elementsPerPage?: number, public currentPage?: number, - public sort?: SortOptions, - public filter?: string, - public exact?: string, - public entryID?: string, + public sort?: SortOptions ) { super(); @@ -35,7 +34,7 @@ export class VocabularyFindOptions extends FindListOptions { searchParams.push(new RequestParam('filter', filter)) } if (isNotEmpty(exact)) { - searchParams.push(new RequestParam('exact', exact)) + searchParams.push(new RequestParam('exact', exact.toString())) } if (isNotEmpty(entryID)) { searchParams.push(new RequestParam('entryID', entryID)) diff --git a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts index da496899d1..9e6d3b200e 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts @@ -9,7 +9,7 @@ import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.s import { ObjectCacheService } from '../../cache/object-cache.service'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { RequestService } from '../../data/request.service'; -import { VocabularyEntriesRequest } from '../../data/request.models'; +import { FindListOptions, VocabularyEntriesRequest } from '../../data/request.models'; import { RequestParam } from '../../cache/models/request-param.model'; import { PageInfo } from '../../shared/page-info.model'; import { PaginatedList } from '../../data/paginated-list'; @@ -19,7 +19,7 @@ import { RestResponse } from '../../cache/response.models'; import { VocabularyService } from './vocabulary.service'; import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; -import { VocabularyFindOptions } from './models/vocabulary-find-options.model'; +import { VocabularyOptions } from './models/vocabulary-options.model'; describe('VocabularyService', () => { let scheduler: TestScheduler; @@ -143,10 +143,18 @@ describe('VocabularyService', () => { const vocabularyId = 'types'; const metadata = 'dc.type'; const collectionUUID = '8b39g7ya-5a4b-438b-851f-be1d5b4a1c5a'; - const vocabularyOptions = new VocabularyFindOptions(collectionUUID, vocabularyId, metadata); + const entryID = 'dsfsfsdf-5a4b-438b-851f-be1d5b4a1c5a'; const searchRequestURL = `https://rest.api/rest/api/submission/vocabularies/search/byMetadataAndCollection?metadata=${metadata}&collection=${collectionUUID}`; const entriesRequestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}/entries?metadata=${metadata}&collection=${collectionUUID}`; - + const entriesByValueRequestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}/entries?metadata=${metadata}&collection=${collectionUUID}&filter=test&exact=false`; + const entryByValueRequestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}/entries?metadata=${metadata}&collection=${collectionUUID}&filter=test&exact=true`; + const entryByIDRequestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}/entries?metadata=${metadata}&collection=${collectionUUID}&entryID=${entryID}`; + const vocabularyOptions: VocabularyOptions = { + name: vocabularyId, + metadata: metadata, + scope: collectionUUID, + closed: false + } const pageInfo = new PageInfo(); const array = [vocabulary, hierarchicalVocabulary]; const arrayEntries = [vocabularyEntryDetail, anotherVocabularyEntryDetail]; @@ -274,11 +282,6 @@ describe('VocabularyService', () => { describe('searchVocabularyByMetadataAndCollection', () => { it('should proxy the call to vocabularyDataService.findVocabularyByHref', () => { - const options = new VocabularyFindOptions(); - options.searchParams = [ - new RequestParam('metadata', metadata), - new RequestParam('collection', collectionUUID) - ]; scheduler.schedule(() => service.searchVocabularyByMetadataAndCollection(vocabularyOptions).subscribe()); scheduler.flush(); @@ -307,20 +310,103 @@ describe('VocabularyService', () => { it('should configure a new VocabularyEntriesRequest', () => { const expected = new VocabularyEntriesRequest(requestService.generateRequestId(), entriesRequestURL); - scheduler.schedule(() => service.getVocabularyEntries(vocabularyOptions).subscribe()); + scheduler.schedule(() => service.getVocabularyEntries(vocabularyOptions, pageInfo).subscribe()); scheduler.flush(); expect(requestService.configure).toHaveBeenCalledWith(expected); }); it('should call RemoteDataBuildService to create the RemoteData Observable', () => { - service.getVocabularyEntries(vocabularyOptions); + service.getVocabularyEntries(vocabularyOptions, pageInfo); expect(rdbService.toRemoteDataObservable).toHaveBeenCalled(); }); }); + + describe('', () => { + + beforeEach(() => { + requestService = getMockRequestService(getRequestEntry$(true)); + rdbService = getMockRemoteDataBuildService(); + spyOn(rdbService, 'toRemoteDataObservable').and.callThrough(); + service = initTestService(); + }); + + describe('getVocabularyEntries', () => { + it('should configure a new VocabularyEntriesRequest', () => { + const expected = new VocabularyEntriesRequest(requestService.generateRequestId(), entriesRequestURL); + + scheduler.schedule(() => service.getVocabularyEntries(vocabularyOptions, pageInfo).subscribe()); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + + it('should call RemoteDataBuildService to create the RemoteData Observable', () => { + service.getVocabularyEntries(vocabularyOptions, pageInfo); + + expect(rdbService.toRemoteDataObservable).toHaveBeenCalled(); + + }); + }); + + describe('getVocabularyEntriesByValue', () => { + it('should configure a new VocabularyEntriesRequest', () => { + const expected = new VocabularyEntriesRequest(requestService.generateRequestId(), entriesByValueRequestURL); + + scheduler.schedule(() => service.getVocabularyEntriesByValue('test', false, vocabularyOptions, pageInfo).subscribe()); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + + it('should call RemoteDataBuildService to create the RemoteData Observable', () => { + service.getVocabularyEntriesByValue('test', false, vocabularyOptions, pageInfo); + + expect(rdbService.toRemoteDataObservable).toHaveBeenCalled(); + + }); + }); + + describe('getVocabularyEntryByValue', () => { + it('should configure a new VocabularyEntriesRequest', () => { + const expected = new VocabularyEntriesRequest(requestService.generateRequestId(), entryByValueRequestURL); + + scheduler.schedule(() => service.getVocabularyEntryByValue('test', vocabularyOptions).subscribe()); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + + it('should call RemoteDataBuildService to create the RemoteData Observable', () => { + service.getVocabularyEntryByValue('test', vocabularyOptions); + + expect(rdbService.toRemoteDataObservable).toHaveBeenCalled(); + + }); + }); + + describe('getVocabularyEntryByID', () => { + it('should configure a new VocabularyEntriesRequest', () => { + const expected = new VocabularyEntriesRequest(requestService.generateRequestId(), entryByIDRequestURL); + + scheduler.schedule(() => service.getVocabularyEntryByID(entryID, vocabularyOptions).subscribe()); + scheduler.flush(); + + expect(requestService.configure).toHaveBeenCalledWith(expected); + }); + + it('should call RemoteDataBuildService to create the RemoteData Observable', () => { + service.getVocabularyEntryByID('test', vocabularyOptions); + + expect(rdbService.toRemoteDataObservable).toHaveBeenCalled(); + + }); + }); + + }); }); describe('vocabularyEntryDetails endpoint', () => { @@ -400,16 +486,16 @@ describe('VocabularyService', () => { describe('searchByGroup', () => { it('should proxy the call to vocabularyEntryDetailDataService.searchBy', () => { - const options = new VocabularyFindOptions(); - options.searchParams.push(new RequestParam('vocabulary', 'srsc')); - scheduler.schedule(() => service.searchTopEntries('srsc', new VocabularyFindOptions())); + const options = new FindListOptions(); + options.searchParams = [new RequestParam('vocabulary', 'srsc')]; + scheduler.schedule(() => service.searchTopEntries('srsc', pageInfo)); scheduler.flush(); expect((service as any).vocabularyEntryDetailDataService.searchBy).toHaveBeenCalledWith((service as any).searchTopMethod, options); }); it('should return a RemoteData) for the search', () => { - const result = service.searchTopEntries('srsc', new VocabularyFindOptions()); + const result = service.searchTopEntries('srsc', pageInfo); const expected = cold('a|', { a: entriesPaginatedListRD }); diff --git a/src/app/core/submission/vocabularies/vocabulary.service.ts b/src/app/core/submission/vocabularies/vocabulary.service.ts index bd5c5b6c48..5a3f357881 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.ts @@ -22,12 +22,19 @@ import { PaginatedList } from '../../data/paginated-list'; import { Vocabulary } from './models/vocabulary.model'; import { VOCABULARY } from './models/vocabularies.resource-type'; import { VocabularyEntry } from './models/vocabulary-entry.model'; -import { hasValue, isNotEmptyOperator } from '../../../shared/empty.util'; -import { configureRequest, filterSuccessfulResponses, getRequestFromRequestHref } from '../../shared/operators'; +import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util'; +import { + configureRequest, + filterSuccessfulResponses, + getFirstSucceededRemoteListPayload, + getRequestFromRequestHref +} from '../../shared/operators'; import { GenericSuccessResponse } from '../../cache/response.models'; import { VocabularyFindOptions } from './models/vocabulary-find-options.model'; import { VocabularyEntryDetail } from './models/vocabulary-entry-detail.model'; import { RequestParam } from '../../cache/models/request-param.model'; +import { VocabularyOptions } from './models/vocabulary-options.model'; +import { PageInfo } from '../../shared/page-info.model'; /* tslint:disable:max-classes-per-file */ @@ -110,20 +117,20 @@ export class VocabularyService { /** * Returns an observable of {@link RemoteData} of a {@link Vocabulary}, based on its ID, with a list of {@link FollowLinkConfig}, * to automatically resolve {@link HALLink}s of the object - * @param id ID of {@link Vocabulary} we want to retrieve + * @param name The name of {@link Vocabulary} we want to retrieve * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved * @return {Observable>} * Return an observable that emits vocabulary object */ - findVocabularyById(id: string, ...linksToFollow: Array>): Observable> { - return this.vocabularyDataService.findById(id, ...linksToFollow); + findVocabularyById(name: string, ...linksToFollow: Array>): Observable> { + return this.vocabularyDataService.findById(name, ...linksToFollow); } /** * Returns {@link RemoteData} of all object with a list of {@link FollowLinkConfig}, to indicate which embedded * info should be added to the objects * - * @param options Find list options object + * @param options Find list options object * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved * @return {Observable>>} * Return an observable that emits object list @@ -135,28 +142,131 @@ export class VocabularyService { /** * Return the {@link VocabularyEntry} list for a given {@link Vocabulary} * - * @param options The {@link VocabularyFindOptions} for the request + * @param vocabularyOptions The {@link VocabularyOptions} for the request to which the entries belong + * @param pageInfo The {@link PageInfo} for the request * @return {Observable>>} * Return an observable that emits object list */ - getVocabularyEntries(options: VocabularyFindOptions): Observable>> { + getVocabularyEntries(vocabularyOptions: VocabularyOptions, pageInfo: PageInfo): Observable>> { - return this.vocabularyDataService.getFindAllHref(options, `${options.name}/entries`).pipe( + const options: VocabularyFindOptions = new VocabularyFindOptions( + vocabularyOptions.scope, + vocabularyOptions.metadata, + null, + null, + null, + null, + pageInfo.elementsPerPage, + pageInfo.currentPage + ); + + return this.vocabularyDataService.getFindAllHref(options, `${vocabularyOptions.name}/entries`).pipe( isNotEmptyOperator(), distinctUntilChanged(), getVocabularyEntriesFor(this.requestService, this.rdbService) ); } + /** + * Return the {@link VocabularyEntry} list for a given {@link Vocabulary} + * + * @param value The entry value to retrieve + * @param exact If true force the vocabulary to provide only entries that match exactly with the value + * @param vocabularyOptions The {@link VocabularyOptions} for the request to which the entries belong + * @param pageInfo The {@link PageInfo} for the request + * @return {Observable>>} + * Return an observable that emits object list + */ + getVocabularyEntriesByValue(value: string, exact: boolean, vocabularyOptions: VocabularyOptions, pageInfo: PageInfo): Observable>> { + const options: VocabularyFindOptions = new VocabularyFindOptions( + vocabularyOptions.scope, + vocabularyOptions.metadata, + null, + value, + exact, + null, + pageInfo.elementsPerPage, + pageInfo.currentPage + ); + + return this.vocabularyDataService.getFindAllHref(options, `${vocabularyOptions.name}/entries`).pipe( + isNotEmptyOperator(), + distinctUntilChanged(), + getVocabularyEntriesFor(this.requestService, this.rdbService) + ); + } + + /** + * Return the {@link VocabularyEntry} list for a given value + * + * @param value The entry value to retrieve + * @param vocabularyOptions The {@link VocabularyOptions} for the request to which the entry belongs + * @return {Observable>>} + * Return an observable that emits {@link VocabularyEntry} object + */ + getVocabularyEntryByValue(value: string, vocabularyOptions: VocabularyOptions): Observable { + + return this.getVocabularyEntriesByValue(value, true, vocabularyOptions, new PageInfo()).pipe( + getFirstSucceededRemoteListPayload(), + map((list: VocabularyEntry[]) => { + if (isNotEmpty(list)) { + return list[0] + } else { + return null; + } + }) + ); + } + + /** + * Return the {@link VocabularyEntry} list for a given ID + * + * @param ID The entry ID to retrieve + * @param vocabularyOptions The {@link VocabularyOptions} for the request to which the entry belongs + * @return {Observable>>} + * Return an observable that emits {@link VocabularyEntry} object + */ + getVocabularyEntryByID(ID: string, vocabularyOptions: VocabularyOptions): Observable { + const pageInfo = new PageInfo() + const options: VocabularyFindOptions = new VocabularyFindOptions( + vocabularyOptions.scope, + vocabularyOptions.metadata, + null, + null, + null, + ID, + pageInfo.elementsPerPage, + pageInfo.currentPage + ); + + return this.vocabularyDataService.getFindAllHref(options, `${vocabularyOptions.name}/entries`).pipe( + isNotEmptyOperator(), + distinctUntilChanged(), + getVocabularyEntriesFor(this.requestService, this.rdbService), + getFirstSucceededRemoteListPayload(), + map((list: VocabularyEntry[]) => { + if (isNotEmpty(list)) { + return list[0] + } else { + return null; + } + }) + ); + } + /** * Return the controlled {@link Vocabulary} configured for the specified metadata and collection if any. * - * @param options The {@link VocabularyFindOptions} for the request - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + * @param vocabularyOptions The {@link VocabularyOptions} for the request + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved * @return {Observable>>} * Return an observable that emits object list */ - searchVocabularyByMetadataAndCollection(options: VocabularyFindOptions, ...linksToFollow: Array>): Observable> { + searchVocabularyByMetadataAndCollection(vocabularyOptions: VocabularyOptions, ...linksToFollow: Array>): Observable> { + const options: VocabularyFindOptions = new VocabularyFindOptions( + vocabularyOptions.scope, + vocabularyOptions.metadata + ); return this.vocabularyDataService.getSearchByHref(this.searchByMetadataAndCollectionMethod, options).pipe( first((href: string) => hasValue(href)), @@ -193,12 +303,13 @@ export class VocabularyService { /** * Return the top level {@link VocabularyEntryDetail} list for a given hierarchical vocabulary * - * @param name The name of hierarchical {@link Vocabulary} to which the entries belongs - * @param options The {@link VocabularyFindOptions} for the request + * @param name The name of hierarchical {@link Vocabulary} to which the entries belongs + * @param pageInfo The {@link PageInfo} for the request * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - searchTopEntries(name: string, options: VocabularyFindOptions, ...linksToFollow: Array>): Observable>> { - options.searchParams.push(new RequestParam('vocabulary', name)); + searchTopEntries(name: string, pageInfo: PageInfo, ...linksToFollow: Array>): Observable>> { + const options = new FindListOptions(); + options.searchParams = [new RequestParam('vocabulary', name)]; return this.vocabularyEntryDetailDataService.searchBy(this.searchTopMethod, options, ...linksToFollow) } } diff --git a/src/app/shared/testing/vocabulary-service.stub.ts b/src/app/shared/testing/vocabulary-service.stub.ts index 15528653c2..fef9b72557 100644 --- a/src/app/shared/testing/vocabulary-service.stub.ts +++ b/src/app/shared/testing/vocabulary-service.stub.ts @@ -1,18 +1,18 @@ -import { Observable } from 'rxjs'; +import { Observable, of as observableOf } from 'rxjs'; -import { VocabularyFindOptions } from '../../core/submission/vocabularies/models/vocabulary-find-options.model'; import { PageInfo } from '../../core/shared/page-info.model'; import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model'; 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'; export class VocabularyServiceStub { private _payload = [ - Object.assign(new VocabularyEntry(),{authority: 1, display: 'one', value: 1}), - Object.assign(new VocabularyEntry(),{authority: 2, display: 'two', value: 2}), + Object.assign(new VocabularyEntry(), { authority: 1, display: 'one', value: 1 }), + Object.assign(new VocabularyEntry(), { authority: 2, display: 'two', value: 2 }), ]; setNewPayload(payload) { @@ -23,7 +23,19 @@ export class VocabularyServiceStub { return this._payload } - getVocabularyEntries(options: VocabularyFindOptions): Observable>> { + getVocabularyEntries(vocabularyOptions: VocabularyOptions, pageInfo: PageInfo): Observable>> { return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), this._payload)); } + + getVocabularyEntriesByValue(value: string, exact: boolean, vocabularyOptions: VocabularyOptions, pageInfo: PageInfo): Observable>> { + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), this._payload)); + } + + getVocabularyEntryByValue(value: string, vocabularyOptions: VocabularyOptions): Observable { + return observableOf(Object.assign(new VocabularyEntry(), { authority: 1, display: 'one', value: 1 })); + } + + getVocabularyEntryByID(id: string, vocabularyOptions: VocabularyOptions): Observable { + return observableOf(Object.assign(new VocabularyEntry(), { authority: 1, display: 'one', value: 1 })); + } } From 3225966600b03107f542390e2620361e226cfd08 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 29 Jun 2020 22:20:52 +0200 Subject: [PATCH 09/62] [CST-3088] Created abstract form component to handle vocabularies --- .../models/dynamic-vocabulary.component.ts | 119 ++++++++++ .../models/list/dynamic-list.component.ts | 20 +- .../lookup/dynamic-lookup.component.html | 8 +- .../lookup/dynamic-lookup.component.spec.ts | 110 +++++++++- .../models/lookup/dynamic-lookup.component.ts | 205 +++++++++++------- .../dynamic-relation-group.components.ts | 4 +- ...dynamic-scrollable-dropdown.component.html | 10 +- ...amic-scrollable-dropdown.component.spec.ts | 4 +- .../dynamic-scrollable-dropdown.component.ts | 131 ++++++----- .../models/tag/dynamic-tag.component.spec.ts | 8 +- .../models/tag/dynamic-tag.component.ts | 91 +++++--- .../dynamic-typeahead.component.spec.ts | 60 ++++- .../typeahead/dynamic-typeahead.component.ts | 108 ++++++--- .../form/builder/parsers/list-field-parser.ts | 2 - .../shared/form/builder/parsers/row-parser.ts | 19 +- 15 files changed, 649 insertions(+), 250 deletions(-) create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-vocabulary.component.ts 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 new file mode 100644 index 0000000000..bf09e237a5 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-vocabulary.component.ts @@ -0,0 +1,119 @@ +import { EventEmitter, Input, Output } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +import { + DynamicFormControlComponent, + DynamicFormLayoutService, + DynamicFormValidationService +} from '@ng-dynamic-forms/core'; +import { map } from 'rxjs/operators'; +import { Observable, of as observableOf } from 'rxjs'; + +import { VocabularyService } from '../../../../../core/submission/vocabularies/vocabulary.service'; +import { isNotEmpty } from '../../../../empty.util'; +import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model'; +import { VocabularyEntry } from '../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; +import { DsDynamicInputModel } from './ds-dynamic-input.model'; +import { PageInfo } from '../../../../../core/shared/page-info.model'; + +/** + * An abstract class to be extended by form components that handle vocabulary + */ +export abstract class DsDynamicVocabularyComponent extends DynamicFormControlComponent { + + @Input() abstract bindId = true; + @Input() abstract group: FormGroup; + @Input() abstract model: DsDynamicInputModel; + + @Output() abstract blur: EventEmitter = new EventEmitter(); + @Output() abstract change: EventEmitter = new EventEmitter(); + @Output() abstract focus: EventEmitter = new EventEmitter(); + + public abstract pageInfo: PageInfo; + + constructor(protected vocabularyService: VocabularyService, + protected layoutService: DynamicFormLayoutService, + protected validationService: DynamicFormValidationService + ) { + super(layoutService, validationService); + } + + /** + * Sets the current value with the given value. + * @param value The value to set. + * @param init Representing if is init value or not. + */ + public abstract setCurrentValue(value: any, init?: boolean); + + /** + * Retrieves the init form value from model + */ + getInitValueFromModel(): Observable { + let initValue$: Observable; + if (isNotEmpty(this.model.value) && (this.model.value instanceof FormFieldMetadataValueObject)) { + let initEntry$: Observable; + if (this.model.value.hasAuthority()) { + initEntry$ = this.vocabularyService.getVocabularyEntryByID(this.model.value.authority, this.model.vocabularyOptions) + } else { + initEntry$ = this.vocabularyService.getVocabularyEntryByValue(this.model.value.value, this.model.vocabularyOptions) + } + initValue$ = initEntry$.pipe(map((initEntry: VocabularyEntry) => { + if (isNotEmpty(initEntry)) { + return new FormFieldMetadataValueObject( + initEntry.value, + null, + initEntry.authority, + initEntry.display + ); + } else { + return this.model.value as any; + } + })); + } else { + initValue$ = observableOf(new FormFieldMetadataValueObject(this.model.value)); + } + return initValue$; + } + + + /** + * Emits a blur event containing a given value. + * @param event The value to emit. + */ + onBlur(event: Event) { + this.blur.emit(event); + } + + /** + * Emits a focus event containing a given value. + * @param event The value to emit. + */ + onFocus(event) { + this.focus.emit(event); + } + + /** + * Emits a change event and updates model value. + * @param updateValue + */ + dispatchUpdate(updateValue: any) { + this.model.valueUpdates.next(updateValue); + this.change.emit(updateValue); + } + + /** + * Update the page info object + * @param elementsPerPage + * @param currentPage + * @param totalElements + * @param totalPages + */ + protected updatePageInfo(elementsPerPage: number, currentPage: number, totalElements?: number, totalPages?: number) { + this.pageInfo = Object.assign(new PageInfo(), { + elementsPerPage: elementsPerPage, + currentPage: currentPage, + totalElements: totalElements, + totalPages: totalPages + }); + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts index 3c5a86f362..d7c492ccde 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts @@ -9,7 +9,6 @@ import { } from '@ng-dynamic-forms/core'; import { findKey } from 'lodash'; -import { VocabularyFindOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-find-options.model'; import { hasValue, isNotEmpty } from '../../../../../empty.util'; import { DynamicListCheckboxGroupModel } from './dynamic-list-checkbox-group.model'; import { FormBuilderService } from '../../../form-builder.service'; @@ -18,6 +17,7 @@ import { VocabularyService } from '../../../../../../core/submission/vocabularie import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators'; import { PaginatedList } from '../../../../../../core/data/paginated-list'; import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; +import { PageInfo } from '../../../../../../core/shared/page-info.model'; export interface ListItem { id: string, @@ -26,12 +26,14 @@ export interface ListItem { index: number } +/** + * Component representing a list input field + */ @Component({ selector: 'ds-dynamic-list', styleUrls: ['./dynamic-list.component.scss'], templateUrl: './dynamic-list.component.html' }) - export class DsDynamicListComponent extends DynamicFormControlComponent implements OnInit { @Input() bindId = true; @Input() group: FormGroup; @@ -43,7 +45,6 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen public items: ListItem[][] = []; protected optionsList: VocabularyEntry[]; - protected searchOptions: VocabularyFindOptions; constructor(private vocabularyService: VocabularyService, private cdr: ChangeDetectorRef, @@ -56,14 +57,6 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen ngOnInit() { if (this.hasAuthorityOptions()) { - // TODO Replace max elements 1000 with a paginated request when pagination bug is resolved - this.searchOptions = new VocabularyFindOptions( - this.model.vocabularyOptions.scope, - this.model.vocabularyOptions.name, - this.model.vocabularyOptions.metadata, - '', - 1000, // Max elements - 1);// Current Page this.setOptionsFromAuthority(); } } @@ -99,7 +92,10 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen protected setOptionsFromAuthority() { if (this.model.vocabularyOptions.name && this.model.vocabularyOptions.name.length > 0) { const listGroup = this.group.controls[this.model.id] as FormGroup; - this.vocabularyService.getVocabularyEntries(this.searchOptions).pipe( + const pageInfo: PageInfo = new PageInfo({ + elementsPerPage: Number.MAX_VALUE, currentPage: 1 + } as PageInfo); + this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, pageInfo).pipe( getFirstSucceededRemoteDataPayload() ).subscribe((entries: PaginatedList) => { let groupCounter = 0; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html index 254b15411d..b274197f13 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html @@ -21,8 +21,8 @@ [placeholder]="model.placeholder | translate" [readonly]="model.readOnly" (change)="onChange($event)" - (blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();" - (focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();" + (blur)="onBlur($event); $event.stopPropagation(); sdRef.close();" + (focus)="onFocus($event); $event.stopPropagation(); sdRef.close();" (click)="$event.stopPropagation(); $event.stopPropagation(); sdRef.close();"> @@ -40,8 +40,8 @@ [placeholder]="model.secondPlaceholder | translate" [readonly]="model.readOnly" (change)="onChange($event)" - (blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();" - (focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();" + (blur)="onBlur($event); $event.stopPropagation(); sdRef.close();" + (focus)="onFocus($event); $event.stopPropagation(); sdRef.close();" (click)="$event.stopPropagation(); sdRef.close();">
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts index ce45e113a0..66eca2393b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts @@ -4,6 +4,7 @@ import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angul import { async, ComponentFixture, fakeAsync, inject, TestBed, tick, } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { of as observableOf } from 'rxjs'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; @@ -309,7 +310,13 @@ describe('Dynamic Lookup component', () => { lookupComp = lookupFixture.componentInstance; // FormComponent test instance lookupComp.group = LOOKUP_TEST_GROUP; lookupComp.model = new DynamicLookupModel(LOOKUP_TEST_MODEL_CONFIG); - lookupComp.model.value = new FormFieldMetadataValueObject('test', null, 'test001'); + const entry = observableOf(Object.assign(new VocabularyEntry(), { + authority: null, + value: 'test', + display: 'testDisplay' + })); + spyOn((lookupComp as any).vocabularyService, 'getVocabularyEntryByValue').and.returnValue(entry); + (lookupComp.model as any).value = new FormFieldMetadataValueObject('test', null, null, 'testDisplay'); lookupFixture.detectChanges(); // spyOn(store, 'dispatch'); @@ -318,9 +325,52 @@ describe('Dynamic Lookup component', () => { lookupFixture.destroy(); lookupComp = null; }); - it('should init component properly', () => { - expect(lookupComp.firstInputValue).toBe('test'); + it('should init component properly', fakeAsync(() => { + tick(); + expect(lookupComp.firstInputValue).toBe('testDisplay'); + expect((lookupComp as any).vocabularyService.getVocabularyEntryByValue).toHaveBeenCalled(); + })); + + it('should have search button disabled on edit mode', () => { + lookupComp.editMode = true; + lookupFixture.detectChanges(); + + const de = lookupFixture.debugElement.queryAll(By.css('button')); + const searchBtnEl = de[0].nativeElement; + const saveBtnEl = de[1].nativeElement; + expect(searchBtnEl.disabled).toBe(true); + expect(saveBtnEl.disabled).toBe(false); + expect(saveBtnEl.textContent.trim()).toBe('form.save'); + }); + }); + describe('and init model value is not empty with authority', () => { + beforeEach(() => { + + lookupFixture = TestBed.createComponent(DsDynamicLookupComponent); + lookupComp = lookupFixture.componentInstance; // FormComponent test instance + lookupComp.group = LOOKUP_TEST_GROUP; + lookupComp.model = new DynamicLookupModel(LOOKUP_TEST_MODEL_CONFIG); + const entry = observableOf(Object.assign(new VocabularyEntry(), { + authority: 'test001', + value: 'test', + display: 'testDisplay' + })); + spyOn((lookupComp as any).vocabularyService, 'getVocabularyEntryByID').and.returnValue(entry); + lookupComp.model.value = new FormFieldMetadataValueObject('test', null, 'test001', 'testDisplay'); + lookupFixture.detectChanges(); + + // spyOn(store, 'dispatch'); + }); + afterEach(() => { + lookupFixture.destroy(); + lookupComp = null; + }); + it('should init component properly', fakeAsync(() => { + tick(); + expect(lookupComp.firstInputValue).toBe('testDisplay'); + expect((lookupComp as any).vocabularyService.getVocabularyEntryByID).toHaveBeenCalled(); + })); it('should have search button disabled on edit mode', () => { lookupComp.editMode = true; @@ -430,6 +480,13 @@ describe('Dynamic Lookup component', () => { lookupComp.group = LOOKUP_TEST_GROUP; lookupComp.model = new DynamicLookupNameModel(LOOKUP_NAME_TEST_MODEL_CONFIG); lookupComp.model.value = new FormFieldMetadataValueObject('Name, Lastname', null, 'test001'); + const entry = observableOf(Object.assign(new VocabularyEntry(), { + authority: null, + value: 'Name, Lastname', + display: 'Name, Lastname' + })); + spyOn((lookupComp as any).vocabularyService, 'getVocabularyEntryByValue').and.returnValue(entry); + (lookupComp.model as any).value = new FormFieldMetadataValueObject('Name, Lastname', null, null, 'Name, Lastname'); lookupFixture.detectChanges(); }); @@ -437,10 +494,55 @@ describe('Dynamic Lookup component', () => { lookupFixture.destroy(); lookupComp = null; }); - it('should init component properly', () => { + it('should init component properly', fakeAsync(() => { + tick(); expect(lookupComp.firstInputValue).toBe('Name'); expect(lookupComp.secondInputValue).toBe('Lastname'); + expect((lookupComp as any).vocabularyService.getVocabularyEntryByValue).toHaveBeenCalled(); + })); + + it('should have search button disabled on edit mode', () => { + lookupComp.editMode = true; + lookupFixture.detectChanges(); + + const de = lookupFixture.debugElement.queryAll(By.css('button')); + const searchBtnEl = de[0].nativeElement; + const saveBtnEl = de[1].nativeElement; + expect(searchBtnEl.disabled).toBe(true); + expect(saveBtnEl.disabled).toBe(false); + expect(saveBtnEl.textContent.trim()).toBe('form.save'); + }); + }); + + describe('and init model value is not empty with authority', () => { + beforeEach(() => { + + lookupFixture = TestBed.createComponent(DsDynamicLookupComponent); + lookupComp = lookupFixture.componentInstance; // FormComponent test instance + lookupComp.group = LOOKUP_TEST_GROUP; + lookupComp.model = new DynamicLookupNameModel(LOOKUP_NAME_TEST_MODEL_CONFIG); + lookupComp.model.value = new FormFieldMetadataValueObject('Name, Lastname', null, 'test001'); + const entry = observableOf(Object.assign(new VocabularyEntry(), { + authority: 'test001', + value: 'Name, Lastname', + display: 'Name, Lastname' + })); + spyOn((lookupComp as any).vocabularyService, 'getVocabularyEntryByID').and.returnValue(entry); + lookupComp.model.value = new FormFieldMetadataValueObject('Name, Lastname', null, 'test001', 'Name, Lastname'); + lookupFixture.detectChanges(); + + }); + afterEach(() => { + lookupFixture.destroy(); + lookupComp = null; + }); + it('should init component properly', fakeAsync(() => { + tick(); + expect(lookupComp.firstInputValue).toBe('Name'); + expect(lookupComp.secondInputValue).toBe('Lastname'); + expect((lookupComp as any).vocabularyService.getVocabularyEntryByID).toHaveBeenCalled(); + })); it('should have search button disabled on edit mode', () => { lookupComp.editMode = true; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts index 24422e891a..9ae4ad0737 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts @@ -4,15 +4,10 @@ import { FormGroup } from '@angular/forms'; import { of as observableOf, Subscription } from 'rxjs'; import { catchError, distinctUntilChanged } from 'rxjs/operators'; import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'; -import { - DynamicFormControlComponent, - DynamicFormLayoutService, - DynamicFormValidationService -} from '@ng-dynamic-forms/core'; +import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; -import { VocabularyFindOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-find-options.model'; -import { hasValue, isEmpty, isNotEmpty, isNull, isUndefined } from '../../../../../empty.util'; +import { hasValue, isEmpty, isNotEmpty } from '../../../../../empty.util'; import { PageInfo } from '../../../../../../core/shared/page-info.model'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; @@ -20,16 +15,21 @@ import { DynamicLookupNameModel } from './dynamic-lookup-name.model'; import { ConfidenceType } from '../../../../../../core/shared/confidence-type'; import { PaginatedList } from '../../../../../../core/data/paginated-list'; import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators'; +import { DsDynamicVocabularyComponent } from '../dynamic-vocabulary.component'; +import { DynamicLookupModel } from './dynamic-lookup.model'; +/** + * Component representing a lookup or lookup-name input field + */ @Component({ selector: 'ds-dynamic-lookup', styleUrls: ['./dynamic-lookup.component.scss'], templateUrl: './dynamic-lookup.component.html' }) -export class DsDynamicLookupComponent extends DynamicFormControlComponent implements OnDestroy, OnInit { +export class DsDynamicLookupComponent extends DsDynamicVocabularyComponent implements OnDestroy, OnInit { @Input() bindId = true; @Input() group: FormGroup; - @Input() model: any; + @Input() model: DynamicLookupModel | DynamicLookupNameModel; @Output() blur: EventEmitter = new EventEmitter(); @Output() change: EventEmitter = new EventEmitter(); @@ -42,89 +42,97 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem public pageInfo: PageInfo; public optionsList: any; - protected searchOptions: VocabularyFindOptions; protected subs: Subscription[] = []; - constructor(private vocabularyService: VocabularyService, + constructor(protected vocabularyService: VocabularyService, private cdr: ChangeDetectorRef, protected layoutService: DynamicFormLayoutService, protected validationService: DynamicFormValidationService ) { - super(layoutService, validationService); + super(vocabularyService, layoutService, validationService); } + /** + * Converts an item from the result list to a `string` to display in the `` field. + */ inputFormatter = (x: { display: string }, y: number) => { return y === 1 ? this.firstInputValue : this.secondInputValue; }; + /** + * Initialize the component, setting up the init form value + */ ngOnInit() { - this.searchOptions = new VocabularyFindOptions( - this.model.vocabularyOptions.scope, - this.model.vocabularyOptions.name, - this.model.vocabularyOptions.metadata, - '', - this.model.maxOptions, - 1); - - this.setInputsValue(this.model.value); + if (isNotEmpty(this.model.value)) { + this.setCurrentValue(this.model.value, true); + } this.subs.push(this.model.valueUpdates .subscribe((value) => { if (isEmpty(value)) { this.resetFields(); } else if (!this.editMode) { - this.setInputsValue(this.model.value); + this.setCurrentValue(this.model.value); } })); } - public formatItemForInput(item: any, field: number): string { - if (isUndefined(item) || isNull(item)) { - return ''; - } - return (typeof item === 'string') ? item : this.inputFormatter(item, field); - } - + /** + * Check if model value has an authority + */ public hasAuthorityValue() { return hasValue(this.model.value) && this.model.value.hasAuthority(); } + /** + * Check if current value has an authority + */ public hasEmptyValue() { return isNotEmpty(this.getCurrentValue()); } + /** + * Clear inputs whether there is no results and authority is closed + */ public clearFields() { - // Clear inputs whether there is no results and authority is closed if (this.model.vocabularyOptions.closed) { this.resetFields(); } } + /** + * Check if edit button is disabled + */ public isEditDisabled() { return !this.hasAuthorityValue(); } + /** + * Check if input is disabled + */ public isInputDisabled() { return (this.model.vocabularyOptions.closed && this.hasAuthorityValue() && !this.editMode); } + /** + * Check if model is instanceof DynamicLookupNameModel + */ public isLookupName() { return (this.model instanceof DynamicLookupNameModel); } + /** + * Check if search button is disabled + */ public isSearchDisabled() { return isEmpty(this.firstInputValue) || this.editMode; } - public onBlurEvent(event: Event) { - this.blur.emit(event); - } - - public onFocusEvent(event) { - this.focus.emit(event); - } - + /** + * Update model value with the typed text if vocabulary is not closed + * @param event the typed text + */ public onChange(event) { event.preventDefault(); if (!this.model.vocabularyOptions.closed) { @@ -139,31 +147,51 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem } } + /** + * Load more result entries + */ public onScroll() { if (!this.loading && this.pageInfo.currentPage <= this.pageInfo.totalPages) { - this.searchOptions.currentPage++; + this.updatePageInfo( + this.pageInfo.elementsPerPage, + this.pageInfo.currentPage + 1, + this.pageInfo.totalElements, + this.pageInfo.totalPages + ); this.search(); } } + /** + * Update model value with selected entry + * @param event the selected entry + */ public onSelect(event) { this.updateModel(event); } + /** + * Reset the current value when dropdown toggle + */ public openChange(isOpened: boolean) { if (!isOpened) { if (this.model.vocabularyOptions.closed && !this.hasAuthorityValue()) { - this.setInputsValue(''); + this.setCurrentValue(''); } } } + /** + * Reset the model value + */ public remove() { this.group.markAsPristine(); - this.model.valueUpdates.next(null); - this.change.emit(null); + this.dispatchUpdate(null) } + /** + * Saves all changes + */ public saveChanges() { if (isNotEmpty(this.getCurrentValue())) { const newValue = Object.assign(new VocabularyEntry(), this.model.value, { @@ -177,15 +205,21 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem this.switchEditMode(); } + /** + * Converts a stream of text values from the `` element to the stream of the array of items + * to display in the result list. + */ public search() { this.optionsList = null; - this.pageInfo = null; - - // Query - this.searchOptions.query = this.getCurrentValue(); - + this.updatePageInfo(this.model.maxOptions, 1); this.loading = true; - this.subs.push(this.vocabularyService.getVocabularyEntries(this.searchOptions).pipe( + + this.subs.push(this.vocabularyService.getVocabularyEntriesByValue( + this.getCurrentValue(), + false, + this.model.vocabularyOptions, + this.pageInfo + ).pipe( getFirstSucceededRemoteDataPayload(), catchError(() => observableOf(new PaginatedList( @@ -195,18 +229,28 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem ), distinctUntilChanged()) .subscribe((list: PaginatedList) => { - console.log(list); this.optionsList = list.page; - this.pageInfo = list.pageInfo; + this.updatePageInfo( + list.pageInfo.elementsPerPage, + list.pageInfo.currentPage, + list.pageInfo.totalElements, + list.pageInfo.totalPages + ); this.loading = false; this.cdr.detectChanges(); })); } + /** + * Changes the edit mode flag + */ public switchEditMode() { this.editMode = !this.editMode; } + /** + * Callback functions for whenClickOnConfidenceNotAccepted event + */ public whenClickOnConfidenceNotAccepted(sdRef: NgbDropdown, confidence: ConfidenceType) { if (!this.model.readOnly) { sdRef.open(); @@ -220,6 +264,38 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem .forEach((sub) => sub.unsubscribe()); } + /** + * Sets the current value with the given value. + * @param value The value to set. + * @param init Representing if is init value or not. + */ + public setCurrentValue(value: any, init = false) { + if (init) { + this.getInitValueFromModel() + .subscribe((value: FormFieldMetadataValueObject) => this.setDisplayInputValue(value.display)); + } else if (hasValue(value)) { + if (value instanceof FormFieldMetadataValueObject || value instanceof VocabularyEntry) { + this.setDisplayInputValue(value.display); + } + } + } + + protected setDisplayInputValue(displayValue: string) { + if (hasValue(displayValue)) { + if (this.isLookupName()) { + const values = displayValue.split((this.model as DynamicLookupNameModel).separator); + + this.firstInputValue = (values[0] || '').trim(); + this.secondInputValue = (values[1] || '').trim(); + } else { + this.firstInputValue = displayValue || ''; + } + } + } + + /** + * Gets the current text present in the input field(s) + */ protected getCurrentValue(): string { let result = ''; if (!this.isLookupName()) { @@ -237,6 +313,9 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem return result; } + /** + * Clear text present in the input field(s) + */ protected resetFields() { this.firstInputValue = ''; if (this.isLookupName()) { @@ -244,32 +323,12 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem } } - protected setInputsValue(value) { - if (hasValue(value)) { - let displayValue = value; - if (value instanceof FormFieldMetadataValueObject || value instanceof VocabularyEntry) { - displayValue = value.display; - } - - if (hasValue(displayValue)) { - if (this.isLookupName()) { - const values = displayValue.split((this.model as DynamicLookupNameModel).separator); - - this.firstInputValue = (values[0] || '').trim(); - this.secondInputValue = (values[1] || '').trim(); - } else { - this.firstInputValue = displayValue || ''; - } - } - } - } - protected updateModel(value) { this.group.markAsDirty(); - this.model.valueUpdates.next(value); - this.setInputsValue(value); - this.change.emit(value); + this.dispatchUpdate(value); + this.setCurrentValue(value); this.optionsList = null; this.pageInfo = null; } + } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts index 21606d7abc..51cf69d560 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts @@ -23,13 +23,15 @@ import { hasValue, isEmpty, isNotEmpty, isNotNull } from '../../../../../empty.u import { shrinkInOut } from '../../../../../animations/shrink'; import { ChipsItem } from '../../../../../chips/models/chips-item.model'; import { hasOnlyEmptyProperties } from '../../../../../object.util'; -import { VocabularyFindOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-find-options.model'; import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; import { environment } from '../../../../../../../environments/environment'; import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators'; import { VocabularyEntryDetail } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; +/** + * Component representing a group input field + */ @Component({ selector: 'ds-dynamic-relation-group', styleUrls: ['./dynamic-relation-group.component.scss'], diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html index cfe50def98..7a35287a99 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.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", From e8237f196f796e365eacb9a6b3945ca0cbcc3a14 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 2 Jul 2020 11:42:30 +0200 Subject: [PATCH 15/62] [CST-3088] added test for hierarchical vocabulary --- .../models/dynamic-vocabulary.component.ts | 2 +- .../dynamic-typeahead.component.spec.ts | 112 ++++++++++++++++-- .../typeahead/dynamic-typeahead.component.ts | 4 + 3 files changed, 106 insertions(+), 12 deletions(-) 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 14ae2128b5..5e6acf8581 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 @@ -64,7 +64,7 @@ export abstract class DsDynamicVocabularyComponent extends DynamicFormControlCom value: initEntry.value, authority: initEntry.authority, display: initEntry.display, - otherInformation: initEntry.otherInformation + otherInformation: initEntry.otherInformation || null }); } 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 72fc7fcba0..d837af5402 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 @@ -5,7 +5,7 @@ import { async, ComponentFixture, fakeAsync, inject, TestBed, tick, } from '@ang import { By } from '@angular/platform-browser'; import { of as observableOf } from 'rxjs'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { DynamicFormLayoutService, DynamicFormsCoreModule, DynamicFormValidationService } from '@ng-dynamic-forms/core'; import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; @@ -21,6 +21,10 @@ import { AuthorityConfidenceStateDirective } from '../../../../../authority-conf import { ObjNgFor } from '../../../../../utils/object-ngfor.pipe'; import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; import { createSuccessfulRemoteDataObject$ } from '../../../../../remote-data.utils'; +import { VocabularyTreeviewComponent } from '../../../../../vocabulary-treeview/vocabulary-treeview.component'; +import { CdkTreeModule } from '@angular/cdk/tree'; +import { TestScheduler } from 'rxjs/testing'; +import { getTestScheduler } from 'jasmine-marbles'; export let TYPEAHEAD_TEST_GROUP; @@ -51,14 +55,16 @@ function init() { }; } -fdescribe('DsDynamicTypeaheadComponent test suite', () => { +describe('DsDynamicTypeaheadComponent test suite', () => { + let scheduler: TestScheduler; let testComp: TestComponent; let typeaheadComp: DsDynamicTypeaheadComponent; let testFixture: ComponentFixture; let typeaheadFixture: ComponentFixture; - let service: any; + let vocabularyServiceStub: any; let html; + let modal; let vocabulary = { id: 'vocabulary', name: 'vocabulary', @@ -95,7 +101,15 @@ fdescribe('DsDynamicTypeaheadComponent test suite', () => { // async beforeEach beforeEach(async(() => { - const vocabularyServiceStub = new VocabularyServiceStub(); + vocabularyServiceStub = new VocabularyServiceStub(); + modal = jasmine.createSpyObj('modal', ['open', 'close', 'dismiss']); +/* jasmine.createSpyObj('modal', + { + open: jasmine.createSpy('open'), + close: jasmine.createSpy('close'), + dismiss: jasmine.createSpy('dismiss'), + } + );*/ init(); TestBed.configureTestingModule({ imports: [ @@ -104,20 +118,23 @@ fdescribe('DsDynamicTypeaheadComponent test suite', () => { FormsModule, NgbModule, ReactiveFormsModule, - TranslateModule.forRoot() + TranslateModule.forRoot(), + CdkTreeModule ], declarations: [ DsDynamicTypeaheadComponent, TestComponent, AuthorityConfidenceStateDirective, - ObjNgFor + ObjNgFor, + VocabularyTreeviewComponent ], // declare the test component providers: [ ChangeDetectorRef, DsDynamicTypeaheadComponent, { provide: VocabularyService, useValue: vocabularyServiceStub }, { provide: DynamicFormLayoutService, useValue: {} }, - { provide: DynamicFormValidationService, useValue: {} } + { provide: DynamicFormValidationService, useValue: {} }, + { provide: NgbModal, useValue: modal } ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }); @@ -135,6 +152,7 @@ fdescribe('DsDynamicTypeaheadComponent test suite', () => { (change)="onValueChange($event)" (focus)="onFocus($event)">`; + spyOn(vocabularyServiceStub, 'findVocabularyById').and.returnValue(createSuccessfulRemoteDataObject$(vocabulary)); testFixture = createTestComponent(html, TestComponent) as ComponentFixture; testComp = testFixture.componentInstance; }); @@ -143,12 +161,15 @@ fdescribe('DsDynamicTypeaheadComponent test suite', () => { testFixture.destroy(); }); it('should create DsDynamicTypeaheadComponent', inject([DsDynamicTypeaheadComponent], (app: DsDynamicTypeaheadComponent) => { - expect(app).toBeDefined(); })); }); - describe('not hiearchical', () => { + describe('Has not hierarchical vocabulary', () => { + beforeEach(() => { + spyOn(vocabularyServiceStub, 'findVocabularyById').and.returnValue(createSuccessfulRemoteDataObject$(vocabulary)); + }); + describe('when init model value is empty', () => { beforeEach(() => { @@ -156,8 +177,6 @@ fdescribe('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(); }); @@ -341,6 +360,77 @@ fdescribe('DsDynamicTypeaheadComponent test suite', () => { }); }); }); + + describe('Has hierarchical vocabulary', () => { + beforeEach(() => { + scheduler = getTestScheduler(); + spyOn(vocabularyServiceStub, 'findVocabularyById').and.returnValue(createSuccessfulRemoteDataObject$(hierarchicalVocabulary)); + }); + + describe('when init model value is empty', () => { + beforeEach(() => { + + typeaheadFixture = TestBed.createComponent(DsDynamicTypeaheadComponent); + typeaheadComp = typeaheadFixture.componentInstance; // FormComponent test instance + typeaheadComp.group = TYPEAHEAD_TEST_GROUP; + typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG); + typeaheadFixture.detectChanges(); + }); + + afterEach(() => { + typeaheadFixture.destroy(); + typeaheadComp = null; + }); + + it('should init component properly', () => { + expect(typeaheadComp.currentValue).not.toBeDefined(); + }); + + it('should open tree properly', () => { + scheduler.schedule(() => typeaheadComp.openTree(new Event('click'))); + scheduler.flush(); + + expect((typeaheadComp as any).modalService.open).toHaveBeenCalled(); + }); + }); + + describe('when init model value is not empty', () => { + beforeEach(() => { + typeaheadFixture = TestBed.createComponent(DsDynamicTypeaheadComponent); + typeaheadComp = typeaheadFixture.componentInstance; // FormComponent test instance + typeaheadComp.group = TYPEAHEAD_TEST_GROUP; + typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG); + const entry = observableOf(Object.assign(new VocabularyEntry(), { + authority: null, + value: 'test', + display: 'testDisplay' + })); + spyOn((typeaheadComp as any).vocabularyService, 'getVocabularyEntryByValue').and.returnValue(entry); + spyOn((typeaheadComp as any).vocabularyService, 'getVocabularyEntryByID').and.returnValue(entry); + (typeaheadComp.model as any).value = new FormFieldMetadataValueObject('test', null, null, 'testDisplay'); + typeaheadFixture.detectChanges(); + }); + + afterEach(() => { + typeaheadFixture.destroy(); + typeaheadComp = null; + }); + + it('should init component properly', fakeAsync(() => { + tick(); + expect(typeaheadComp.currentValue).toEqual(new FormFieldMetadataValueObject('test', null, null, 'testDisplay')); + expect((typeaheadComp as any).vocabularyService.getVocabularyEntryByValue).toHaveBeenCalled(); + })); + + it('should open tree properly', () => { + scheduler.schedule(() => typeaheadComp.openTree(new Event('click'))); + scheduler.flush(); + + expect((typeaheadComp as any).modalService.open).toHaveBeenCalled(); + }); + }); + + }); }); // declare a test component 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 93e5b228e7..5d7413e4dd 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 @@ -201,6 +201,10 @@ export class DsDynamicTypeaheadComponent extends DsDynamicVocabularyComponent im this.dispatchUpdate(event.item); } + /** + * Open modal to show tree for hierarchical vocabulary + * @param event The click event fired + */ openTree(event) { event.preventDefault(); event.stopImmediatePropagation(); From 30d53c5954832b2d1948f5bb1b0b420c71fb51ac Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 2 Jul 2020 12:12:56 +0200 Subject: [PATCH 16/62] [CST-3088] Renamed DsDynamicTypeaheadComponent to DsDynamicOneboxComponent --- ...c-form-control-container.component.spec.ts | 8 +- ...ynamic-form-control-container.component.ts | 25 +- .../models/ds-dynamic-input.model.ts | 2 +- .../dynamic-onebox.component.html} | 0 .../dynamic-onebox.component.scss} | 0 .../dynamic-onebox.component.spec.ts} | 270 +++++++++--------- .../dynamic-onebox.component.ts} | 14 +- .../dynamic-onebox.model.ts} | 10 +- .../form/builder/form-builder.service.spec.ts | 14 +- .../parsers/onebox-field-parser.spec.ts | 6 +- .../builder/parsers/onebox-field-parser.ts | 14 +- src/app/shared/shared.module.ts | 6 +- 12 files changed, 188 insertions(+), 181 deletions(-) rename src/app/shared/form/builder/ds-dynamic-form-ui/models/{typeahead/dynamic-typeahead.component.html => onebox/dynamic-onebox.component.html} (100%) rename src/app/shared/form/builder/ds-dynamic-form-ui/models/{typeahead/dynamic-typeahead.component.scss => onebox/dynamic-onebox.component.scss} (100%) rename src/app/shared/form/builder/ds-dynamic-form-ui/models/{typeahead/dynamic-typeahead.component.spec.ts => onebox/dynamic-onebox.component.spec.ts} (50%) rename src/app/shared/form/builder/ds-dynamic-form-ui/models/{typeahead/dynamic-typeahead.component.ts => onebox/dynamic-onebox.component.ts} (95%) rename src/app/shared/form/builder/ds-dynamic-form-ui/models/{typeahead/dynamic-typeahead.model.ts => onebox/dynamic-onebox.model.ts} (58%) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts index 171ad69f64..e4e6ac6e90 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts @@ -49,10 +49,10 @@ import { DynamicListRadioGroupModel } from './models/list/dynamic-list-radio-gro import { DynamicLookupModel } from './models/lookup/dynamic-lookup.model'; import { DynamicScrollableDropdownModel } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; import { DynamicTagModel } from './models/tag/dynamic-tag.model'; -import { DynamicTypeaheadModel } from './models/typeahead/dynamic-typeahead.model'; +import { DynamicOneboxModel } from './models/onebox/dynamic-onebox.model'; import { DynamicQualdropModel } from './models/ds-dynamic-qualdrop.model'; import { DynamicLookupNameModel } from './models/lookup/dynamic-lookup-name.model'; -import { DsDynamicTypeaheadComponent } from './models/typeahead/dynamic-typeahead.component'; +import { DsDynamicOneboxComponent } from './models/onebox/dynamic-onebox.component'; import { DsDynamicScrollableDropdownComponent } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.component'; import { DsDynamicTagComponent } from './models/tag/dynamic-tag.component'; import { DsDynamicListComponent } from './models/list/dynamic-list.component'; @@ -101,7 +101,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { new DynamicSwitchModel({ id: 'switch' }), new DynamicTextAreaModel({ id: 'textarea' }), new DynamicTimePickerModel({ id: 'timepicker' }), - new DynamicTypeaheadModel({ id: 'typeahead', metadataFields: [], repeatable: false, submissionId: '1234' }), + new DynamicOneboxModel({ id: 'onebox', metadataFields: [], repeatable: false, submissionId: '1234' }), new DynamicScrollableDropdownModel({ id: 'scrollableDropdown', vocabularyOptions: vocabularyOptions, @@ -312,7 +312,7 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { expect(testFn(formModel[13])).toBeNull(); expect(testFn(formModel[14])).toEqual(DynamicNGBootstrapTextAreaComponent); expect(testFn(formModel[15])).toEqual(DynamicNGBootstrapTimePickerComponent); - expect(testFn(formModel[16])).toEqual(DsDynamicTypeaheadComponent); + expect(testFn(formModel[16])).toEqual(DsDynamicOneboxComponent); expect(testFn(formModel[17])).toEqual(DsDynamicScrollableDropdownComponent); expect(testFn(formModel[18])).toEqual(DsDynamicTagComponent); expect(testFn(formModel[19])).toEqual(DsDynamicListComponent); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index 2089ce8bca..3eaca6a67a 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -1,5 +1,6 @@ import { - ChangeDetectionStrategy, ChangeDetectorRef, + ChangeDetectionStrategy, + ChangeDetectorRef, Component, ComponentFactoryResolver, ContentChildren, @@ -29,13 +30,15 @@ import { DYNAMIC_FORM_CONTROL_TYPE_SELECT, DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA, DYNAMIC_FORM_CONTROL_TYPE_TIMEPICKER, - DynamicDatePickerModel, DynamicFormComponentService, + DynamicDatePickerModel, + DynamicFormComponentService, DynamicFormControl, DynamicFormControlContainerComponent, DynamicFormControlEvent, DynamicFormControlModel, DynamicFormLayout, - DynamicFormLayoutService, DynamicFormRelationService, + DynamicFormLayoutService, + DynamicFormRelationService, DynamicFormValidationService, DynamicTemplateDirective, } from '@ng-dynamic-forms/core'; @@ -56,7 +59,7 @@ import { ReorderableRelationship } from './existing-metadata-list-element/existing-metadata-list-element.component'; -import { DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD } from './models/typeahead/dynamic-typeahead.model'; +import { DYNAMIC_FORM_CONTROL_TYPE_ONEBOX } from './models/onebox/dynamic-onebox.model'; import { DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; import { DYNAMIC_FORM_CONTROL_TYPE_TAG } from './models/tag/dynamic-tag.model'; import { DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER } from './models/date-picker/date-picker.model'; @@ -68,7 +71,7 @@ import { DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME } from './models/lookup/dynamic-l import { DsDynamicTagComponent } from './models/tag/dynamic-tag.component'; import { DsDatePickerComponent } from './models/date-picker/date-picker.component'; import { DsDynamicListComponent } from './models/list/dynamic-list.component'; -import { DsDynamicTypeaheadComponent } from './models/typeahead/dynamic-typeahead.component'; +import { DsDynamicOneboxComponent } from './models/onebox/dynamic-onebox.component'; import { DsDynamicScrollableDropdownComponent } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.component'; import { DsDynamicLookupComponent } from './models/lookup/dynamic-lookup.component'; import { DsDynamicFormGroupComponent } from './models/form-group/dynamic-form-group.component'; @@ -78,7 +81,7 @@ import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './models/relation-grou import { DsDatePickerInlineComponent } from './models/date-picker-inline/dynamic-date-picker-inline.component'; import { DYNAMIC_FORM_CONTROL_TYPE_CUSTOM_SWITCH } from './models/custom-switch/custom-switch.model'; import { CustomSwitchComponent } from './models/custom-switch/custom-switch.component'; -import { map, startWith, switchMap, find } from 'rxjs/operators'; +import { map, startWith, switchMap } from 'rxjs/operators'; import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; import { SearchResult } from '../../../search/search-result.model'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; @@ -88,7 +91,11 @@ import { SelectableListService } from '../../../object-list/selectable-list/sele import { DsDynamicDisabledComponent } from './models/disabled/dynamic-disabled.component'; import { DYNAMIC_FORM_CONTROL_TYPE_DISABLED } from './models/disabled/dynamic-disabled.model'; import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/dynamic-lookup-relation-modal.component'; -import { getAllSucceededRemoteData, getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators'; +import { + getAllSucceededRemoteData, + getRemoteDataPayload, + getSucceededRemoteData +} from '../../../../core/shared/operators'; import { RemoteData } from '../../../../core/data/remote-data'; import { Item } from '../../../../core/shared/item.model'; import { ItemDataService } from '../../../../core/data/item-data.service'; @@ -136,8 +143,8 @@ export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type< case DYNAMIC_FORM_CONTROL_TYPE_TIMEPICKER: return DynamicNGBootstrapTimePickerComponent; - case DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD: - return DsDynamicTypeaheadComponent; + case DYNAMIC_FORM_CONTROL_TYPE_ONEBOX: + return DsDynamicOneboxComponent; case DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN: return DsDynamicScrollableDropdownComponent; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts index b723f7aa40..90c26b69cf 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts @@ -41,7 +41,7 @@ export class DsDynamicInputModel extends DynamicInputModel { this.language = config.language; if (!this.language) { - // TypeAhead + // Onebox if (config.value instanceof FormFieldMetadataValueObject) { this.language = config.value.language; } else if (Array.isArray(config.value)) { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html similarity index 100% rename from src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.html rename to src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.html diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.scss similarity index 100% rename from src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.scss rename to src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.scss 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/onebox/dynamic-onebox.component.spec.ts similarity index 50% rename from src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.spec.ts rename to src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts index d837af5402..3982f659af 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/onebox/dynamic-onebox.component.spec.ts @@ -13,8 +13,8 @@ import { TranslateModule } from '@ngx-translate/core'; import { VocabularyOptions } from '../../../../../../core/submission/vocabularies/models/vocabulary-options.model'; import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; import { VocabularyServiceStub } from '../../../../../testing/vocabulary-service.stub'; -import { DsDynamicTypeaheadComponent } from './dynamic-typeahead.component'; -import { DynamicTypeaheadModel } from './dynamic-typeahead.model'; +import { DsDynamicOneboxComponent } from './dynamic-onebox.component'; +import { DynamicOneboxModel } from './dynamic-onebox.model'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; import { createTestComponent } from '../../../../../testing/utils.test'; import { AuthorityConfidenceStateDirective } from '../../../../../authority-confidence/authority-confidence-state.directive'; @@ -26,27 +26,27 @@ import { CdkTreeModule } from '@angular/cdk/tree'; import { TestScheduler } from 'rxjs/testing'; import { getTestScheduler } from 'jasmine-marbles'; -export let TYPEAHEAD_TEST_GROUP; +export let ONEBOX_TEST_GROUP; -export let TYPEAHEAD_TEST_MODEL_CONFIG; +export let ONEBOX_TEST_MODEL_CONFIG; function init() { - TYPEAHEAD_TEST_GROUP = new FormGroup({ - typeahead: new FormControl(), + ONEBOX_TEST_GROUP = new FormGroup({ + onebox: new FormControl(), }); - TYPEAHEAD_TEST_MODEL_CONFIG = { + ONEBOX_TEST_MODEL_CONFIG = { vocabularyOptions: { closed: false, - metadata: 'typeahead', + metadata: 'onebox', name: 'vocabulary', scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' } as VocabularyOptions, disabled: false, - id: 'typeahead', + id: 'onebox', label: 'Conference', minChars: 3, - name: 'typeahead', + name: 'onebox', placeholder: 'Conference', readOnly: false, required: false, @@ -55,13 +55,13 @@ function init() { }; } -describe('DsDynamicTypeaheadComponent test suite', () => { +describe('DsDynamicOneboxComponent test suite', () => { let scheduler: TestScheduler; let testComp: TestComponent; - let typeaheadComp: DsDynamicTypeaheadComponent; + let oneboxComponent: DsDynamicOneboxComponent; let testFixture: ComponentFixture; - let typeaheadFixture: ComponentFixture; + let oneboxCompFixture: ComponentFixture; let vocabularyServiceStub: any; let html; let modal; @@ -122,7 +122,7 @@ describe('DsDynamicTypeaheadComponent test suite', () => { CdkTreeModule ], declarations: [ - DsDynamicTypeaheadComponent, + DsDynamicOneboxComponent, TestComponent, AuthorityConfidenceStateDirective, ObjNgFor, @@ -130,7 +130,7 @@ describe('DsDynamicTypeaheadComponent test suite', () => { ], // declare the test component providers: [ ChangeDetectorRef, - DsDynamicTypeaheadComponent, + DsDynamicOneboxComponent, { provide: VocabularyService, useValue: vocabularyServiceStub }, { provide: DynamicFormLayoutService, useValue: {} }, { provide: DynamicFormValidationService, useValue: {} }, @@ -145,12 +145,12 @@ describe('DsDynamicTypeaheadComponent test suite', () => { // synchronous beforeEach beforeEach(() => { html = ` - `; + (focus)="onFocus($event)">`; spyOn(vocabularyServiceStub, 'findVocabularyById').and.returnValue(createSuccessfulRemoteDataObject$(vocabulary)); testFixture = createTestComponent(html, TestComponent) as ComponentFixture; @@ -160,7 +160,7 @@ describe('DsDynamicTypeaheadComponent test suite', () => { afterEach(() => { testFixture.destroy(); }); - it('should create DsDynamicTypeaheadComponent', inject([DsDynamicTypeaheadComponent], (app: DsDynamicTypeaheadComponent) => { + it('should create DsDynamicOneboxComponent', inject([DsDynamicOneboxComponent], (app: DsDynamicOneboxComponent) => { expect(app).toBeDefined(); })); }); @@ -173,190 +173,190 @@ describe('DsDynamicTypeaheadComponent test suite', () => { describe('when init model value is empty', () => { beforeEach(() => { - typeaheadFixture = TestBed.createComponent(DsDynamicTypeaheadComponent); - typeaheadComp = typeaheadFixture.componentInstance; // FormComponent test instance - typeaheadComp.group = TYPEAHEAD_TEST_GROUP; - typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG); - typeaheadFixture.detectChanges(); + oneboxCompFixture = TestBed.createComponent(DsDynamicOneboxComponent); + oneboxComponent = oneboxCompFixture.componentInstance; // FormComponent test instance + oneboxComponent.group = ONEBOX_TEST_GROUP; + oneboxComponent.model = new DynamicOneboxModel(ONEBOX_TEST_MODEL_CONFIG); + oneboxCompFixture.detectChanges(); }); afterEach(() => { - typeaheadFixture.destroy(); - typeaheadComp = null; + oneboxCompFixture.destroy(); + oneboxComponent = null; }); it('should init component properly', () => { - expect(typeaheadComp.currentValue).not.toBeDefined(); + expect(oneboxComponent.currentValue).not.toBeDefined(); }); it('should search when 3+ characters typed', fakeAsync(() => { - spyOn((typeaheadComp as any).vocabularyService, 'getVocabularyEntriesByValue').and.callThrough(); + spyOn((oneboxComponent as any).vocabularyService, 'getVocabularyEntriesByValue').and.callThrough(); - typeaheadComp.search(observableOf('test')).subscribe(); + oneboxComponent.search(observableOf('test')).subscribe(); tick(300); - typeaheadFixture.detectChanges(); + oneboxCompFixture.detectChanges(); - expect((typeaheadComp as any).vocabularyService.getVocabularyEntriesByValue).toHaveBeenCalled(); + expect((oneboxComponent as any).vocabularyService.getVocabularyEntriesByValue).toHaveBeenCalled(); })); it('should set model.value on input type when VocabularyOptions.closed is false', () => { - const inputDe = typeaheadFixture.debugElement.query(By.css('input.form-control')); + const inputDe = oneboxCompFixture.debugElement.query(By.css('input.form-control')); const inputElement = inputDe.nativeElement; inputElement.value = 'test value'; inputElement.dispatchEvent(new Event('input')); - expect(typeaheadComp.inputValue).toEqual(new FormFieldMetadataValueObject('test value')) + expect(oneboxComponent.inputValue).toEqual(new FormFieldMetadataValueObject('test value')) }); it('should not set model.value on input type when VocabularyOptions.closed is true', () => { - typeaheadComp.model.vocabularyOptions.closed = true; - typeaheadFixture.detectChanges(); - const inputDe = typeaheadFixture.debugElement.query(By.css('input.form-control')); + oneboxComponent.model.vocabularyOptions.closed = true; + oneboxCompFixture.detectChanges(); + const inputDe = oneboxCompFixture.debugElement.query(By.css('input.form-control')); const inputElement = inputDe.nativeElement; inputElement.value = 'test value'; inputElement.dispatchEvent(new Event('input')); - expect(typeaheadComp.model.value).not.toBeDefined(); + expect(oneboxComponent.model.value).not.toBeDefined(); }); it('should emit blur Event onBlur when popup is closed', () => { - spyOn(typeaheadComp.blur, 'emit'); - spyOn(typeaheadComp.instance, 'isPopupOpen').and.returnValue(false); - typeaheadComp.onBlur(new Event('blur')); - expect(typeaheadComp.blur.emit).toHaveBeenCalled(); + spyOn(oneboxComponent.blur, 'emit'); + spyOn(oneboxComponent.instance, 'isPopupOpen').and.returnValue(false); + oneboxComponent.onBlur(new Event('blur')); + expect(oneboxComponent.blur.emit).toHaveBeenCalled(); }); it('should not emit blur Event onBlur when popup is opened', () => { - spyOn(typeaheadComp.blur, 'emit'); - spyOn(typeaheadComp.instance, 'isPopupOpen').and.returnValue(true); - const input = typeaheadFixture.debugElement.query(By.css('input')); + spyOn(oneboxComponent.blur, 'emit'); + spyOn(oneboxComponent.instance, 'isPopupOpen').and.returnValue(true); + const input = oneboxCompFixture.debugElement.query(By.css('input')); input.nativeElement.blur(); - expect(typeaheadComp.blur.emit).not.toHaveBeenCalled(); + expect(oneboxComponent.blur.emit).not.toHaveBeenCalled(); }); it('should emit change Event onBlur when VocabularyOptions.closed is false and inputValue is changed', () => { - typeaheadComp.inputValue = 'test value'; - typeaheadFixture.detectChanges(); - spyOn(typeaheadComp.blur, 'emit'); - spyOn(typeaheadComp.change, 'emit'); - spyOn(typeaheadComp.instance, 'isPopupOpen').and.returnValue(false); - typeaheadComp.onBlur(new Event('blur',)); - expect(typeaheadComp.change.emit).toHaveBeenCalled(); - expect(typeaheadComp.blur.emit).toHaveBeenCalled(); + oneboxComponent.inputValue = 'test value'; + oneboxCompFixture.detectChanges(); + spyOn(oneboxComponent.blur, 'emit'); + spyOn(oneboxComponent.change, 'emit'); + spyOn(oneboxComponent.instance, 'isPopupOpen').and.returnValue(false); + oneboxComponent.onBlur(new Event('blur',)); + expect(oneboxComponent.change.emit).toHaveBeenCalled(); + expect(oneboxComponent.blur.emit).toHaveBeenCalled(); }); it('should not emit change Event onBlur when VocabularyOptions.closed is false and inputValue is not changed', () => { - typeaheadComp.inputValue = 'test value'; - typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG); - (typeaheadComp.model as any).value = 'test value'; - typeaheadFixture.detectChanges(); - spyOn(typeaheadComp.blur, 'emit'); - spyOn(typeaheadComp.change, 'emit'); - spyOn(typeaheadComp.instance, 'isPopupOpen').and.returnValue(false); - typeaheadComp.onBlur(new Event('blur',)); - expect(typeaheadComp.change.emit).not.toHaveBeenCalled(); - expect(typeaheadComp.blur.emit).toHaveBeenCalled(); + oneboxComponent.inputValue = 'test value'; + oneboxComponent.model = new DynamicOneboxModel(ONEBOX_TEST_MODEL_CONFIG); + (oneboxComponent.model as any).value = 'test value'; + oneboxCompFixture.detectChanges(); + spyOn(oneboxComponent.blur, 'emit'); + spyOn(oneboxComponent.change, 'emit'); + spyOn(oneboxComponent.instance, 'isPopupOpen').and.returnValue(false); + oneboxComponent.onBlur(new Event('blur',)); + expect(oneboxComponent.change.emit).not.toHaveBeenCalled(); + expect(oneboxComponent.blur.emit).toHaveBeenCalled(); }); it('should not emit change Event onBlur when VocabularyOptions.closed is false and inputValue is null', () => { - typeaheadComp.inputValue = null; - typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG); - (typeaheadComp.model as any).value = 'test value'; - typeaheadFixture.detectChanges(); - spyOn(typeaheadComp.blur, 'emit'); - spyOn(typeaheadComp.change, 'emit'); - spyOn(typeaheadComp.instance, 'isPopupOpen').and.returnValue(false); - typeaheadComp.onBlur(new Event('blur',)); - expect(typeaheadComp.change.emit).not.toHaveBeenCalled(); - expect(typeaheadComp.blur.emit).toHaveBeenCalled(); + oneboxComponent.inputValue = null; + oneboxComponent.model = new DynamicOneboxModel(ONEBOX_TEST_MODEL_CONFIG); + (oneboxComponent.model as any).value = 'test value'; + oneboxCompFixture.detectChanges(); + spyOn(oneboxComponent.blur, 'emit'); + spyOn(oneboxComponent.change, 'emit'); + spyOn(oneboxComponent.instance, 'isPopupOpen').and.returnValue(false); + oneboxComponent.onBlur(new Event('blur',)); + expect(oneboxComponent.change.emit).not.toHaveBeenCalled(); + expect(oneboxComponent.blur.emit).toHaveBeenCalled(); }); it('should emit focus Event onFocus', () => { - spyOn(typeaheadComp.focus, 'emit'); - typeaheadComp.onFocus(new Event('focus')); - expect(typeaheadComp.focus.emit).toHaveBeenCalled(); + spyOn(oneboxComponent.focus, 'emit'); + oneboxComponent.onFocus(new Event('focus')); + expect(oneboxComponent.focus.emit).toHaveBeenCalled(); }); }); describe('when init model value is not empty', () => { beforeEach(() => { - typeaheadFixture = TestBed.createComponent(DsDynamicTypeaheadComponent); - typeaheadComp = typeaheadFixture.componentInstance; // FormComponent test instance - typeaheadComp.group = TYPEAHEAD_TEST_GROUP; - typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG); + oneboxCompFixture = TestBed.createComponent(DsDynamicOneboxComponent); + oneboxComponent = oneboxCompFixture.componentInstance; // FormComponent test instance + oneboxComponent.group = ONEBOX_TEST_GROUP; + oneboxComponent.model = new DynamicOneboxModel(ONEBOX_TEST_MODEL_CONFIG); const entry = observableOf(Object.assign(new VocabularyEntry(), { authority: null, value: 'test', display: 'testDisplay' })); - spyOn((typeaheadComp as any).vocabularyService, 'getVocabularyEntryByValue').and.returnValue(entry); - spyOn((typeaheadComp as any).vocabularyService, 'getVocabularyEntryByID').and.returnValue(entry); - (typeaheadComp.model as any).value = new FormFieldMetadataValueObject('test', null, null, 'testDisplay'); - typeaheadFixture.detectChanges(); + spyOn((oneboxComponent as any).vocabularyService, 'getVocabularyEntryByValue').and.returnValue(entry); + spyOn((oneboxComponent as any).vocabularyService, 'getVocabularyEntryByID').and.returnValue(entry); + (oneboxComponent.model as any).value = new FormFieldMetadataValueObject('test', null, null, 'testDisplay'); + oneboxCompFixture.detectChanges(); }); afterEach(() => { - typeaheadFixture.destroy(); - typeaheadComp = null; + oneboxCompFixture.destroy(); + oneboxComponent = null; }); it('should init component properly', fakeAsync(() => { tick(); - expect(typeaheadComp.currentValue).toEqual(new FormFieldMetadataValueObject('test', null, null, 'testDisplay')); - expect((typeaheadComp as any).vocabularyService.getVocabularyEntryByValue).toHaveBeenCalled(); + expect(oneboxComponent.currentValue).toEqual(new FormFieldMetadataValueObject('test', null, null, 'testDisplay')); + expect((oneboxComponent as any).vocabularyService.getVocabularyEntryByValue).toHaveBeenCalled(); })); it('should emit change Event onChange and currentValue is empty', () => { - typeaheadComp.currentValue = null; - spyOn(typeaheadComp.change, 'emit'); - typeaheadComp.onChange(new Event('change')); - expect(typeaheadComp.change.emit).toHaveBeenCalled(); - expect(typeaheadComp.model.value).toBeNull(); + oneboxComponent.currentValue = null; + spyOn(oneboxComponent.change, 'emit'); + oneboxComponent.onChange(new Event('change')); + expect(oneboxComponent.change.emit).toHaveBeenCalled(); + expect(oneboxComponent.model.value).toBeNull(); }); }); describe('when init model value is not empty and has authority', () => { beforeEach(() => { - typeaheadFixture = TestBed.createComponent(DsDynamicTypeaheadComponent); - typeaheadComp = typeaheadFixture.componentInstance; // FormComponent test instance - typeaheadComp.group = TYPEAHEAD_TEST_GROUP; - typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG); + oneboxCompFixture = TestBed.createComponent(DsDynamicOneboxComponent); + oneboxComponent = oneboxCompFixture.componentInstance; // FormComponent test instance + oneboxComponent.group = ONEBOX_TEST_GROUP; + oneboxComponent.model = new DynamicOneboxModel(ONEBOX_TEST_MODEL_CONFIG); const entry = observableOf(Object.assign(new VocabularyEntry(), { authority: 'test001', value: 'test001', display: 'test' })); - spyOn((typeaheadComp as any).vocabularyService, 'getVocabularyEntryByValue').and.returnValue(entry); - spyOn((typeaheadComp as any).vocabularyService, 'getVocabularyEntryByID').and.returnValue(entry); - (typeaheadComp.model as any).value = new FormFieldMetadataValueObject('test', null, 'test001'); - typeaheadFixture.detectChanges(); + spyOn((oneboxComponent as any).vocabularyService, 'getVocabularyEntryByValue').and.returnValue(entry); + spyOn((oneboxComponent as any).vocabularyService, 'getVocabularyEntryByID').and.returnValue(entry); + (oneboxComponent.model as any).value = new FormFieldMetadataValueObject('test', null, 'test001'); + oneboxCompFixture.detectChanges(); }); afterEach(() => { - typeaheadFixture.destroy(); - typeaheadComp = null; + oneboxCompFixture.destroy(); + oneboxComponent = null; }); it('should init component properly', fakeAsync(() => { tick(); - expect(typeaheadComp.currentValue).toEqual(new FormFieldMetadataValueObject('test001', null, 'test001', 'test')); - expect((typeaheadComp as any).vocabularyService.getVocabularyEntryByID).toHaveBeenCalled(); + expect(oneboxComponent.currentValue).toEqual(new FormFieldMetadataValueObject('test001', null, 'test001', 'test')); + expect((oneboxComponent as any).vocabularyService.getVocabularyEntryByID).toHaveBeenCalled(); })); it('should emit change Event onChange and currentValue is empty', () => { - typeaheadComp.currentValue = null; - spyOn(typeaheadComp.change, 'emit'); - typeaheadComp.onChange(new Event('change')); - expect(typeaheadComp.change.emit).toHaveBeenCalled(); - expect(typeaheadComp.model.value).toBeNull(); + oneboxComponent.currentValue = null; + spyOn(oneboxComponent.change, 'emit'); + oneboxComponent.onChange(new Event('change')); + expect(oneboxComponent.change.emit).toHaveBeenCalled(); + expect(oneboxComponent.model.value).toBeNull(); }); }); }); @@ -370,63 +370,63 @@ describe('DsDynamicTypeaheadComponent test suite', () => { describe('when init model value is empty', () => { beforeEach(() => { - typeaheadFixture = TestBed.createComponent(DsDynamicTypeaheadComponent); - typeaheadComp = typeaheadFixture.componentInstance; // FormComponent test instance - typeaheadComp.group = TYPEAHEAD_TEST_GROUP; - typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG); - typeaheadFixture.detectChanges(); + oneboxCompFixture = TestBed.createComponent(DsDynamicOneboxComponent); + oneboxComponent = oneboxCompFixture.componentInstance; // FormComponent test instance + oneboxComponent.group = ONEBOX_TEST_GROUP; + oneboxComponent.model = new DynamicOneboxModel(ONEBOX_TEST_MODEL_CONFIG); + oneboxCompFixture.detectChanges(); }); afterEach(() => { - typeaheadFixture.destroy(); - typeaheadComp = null; + oneboxCompFixture.destroy(); + oneboxComponent = null; }); it('should init component properly', () => { - expect(typeaheadComp.currentValue).not.toBeDefined(); + expect(oneboxComponent.currentValue).not.toBeDefined(); }); it('should open tree properly', () => { - scheduler.schedule(() => typeaheadComp.openTree(new Event('click'))); + scheduler.schedule(() => oneboxComponent.openTree(new Event('click'))); scheduler.flush(); - expect((typeaheadComp as any).modalService.open).toHaveBeenCalled(); + expect((oneboxComponent as any).modalService.open).toHaveBeenCalled(); }); }); describe('when init model value is not empty', () => { beforeEach(() => { - typeaheadFixture = TestBed.createComponent(DsDynamicTypeaheadComponent); - typeaheadComp = typeaheadFixture.componentInstance; // FormComponent test instance - typeaheadComp.group = TYPEAHEAD_TEST_GROUP; - typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG); + oneboxCompFixture = TestBed.createComponent(DsDynamicOneboxComponent); + oneboxComponent = oneboxCompFixture.componentInstance; // FormComponent test instance + oneboxComponent.group = ONEBOX_TEST_GROUP; + oneboxComponent.model = new DynamicOneboxModel(ONEBOX_TEST_MODEL_CONFIG); const entry = observableOf(Object.assign(new VocabularyEntry(), { authority: null, value: 'test', display: 'testDisplay' })); - spyOn((typeaheadComp as any).vocabularyService, 'getVocabularyEntryByValue').and.returnValue(entry); - spyOn((typeaheadComp as any).vocabularyService, 'getVocabularyEntryByID').and.returnValue(entry); - (typeaheadComp.model as any).value = new FormFieldMetadataValueObject('test', null, null, 'testDisplay'); - typeaheadFixture.detectChanges(); + spyOn((oneboxComponent as any).vocabularyService, 'getVocabularyEntryByValue').and.returnValue(entry); + spyOn((oneboxComponent as any).vocabularyService, 'getVocabularyEntryByID').and.returnValue(entry); + (oneboxComponent.model as any).value = new FormFieldMetadataValueObject('test', null, null, 'testDisplay'); + oneboxCompFixture.detectChanges(); }); afterEach(() => { - typeaheadFixture.destroy(); - typeaheadComp = null; + oneboxCompFixture.destroy(); + oneboxComponent = null; }); it('should init component properly', fakeAsync(() => { tick(); - expect(typeaheadComp.currentValue).toEqual(new FormFieldMetadataValueObject('test', null, null, 'testDisplay')); - expect((typeaheadComp as any).vocabularyService.getVocabularyEntryByValue).toHaveBeenCalled(); + expect(oneboxComponent.currentValue).toEqual(new FormFieldMetadataValueObject('test', null, null, 'testDisplay')); + expect((oneboxComponent as any).vocabularyService.getVocabularyEntryByValue).toHaveBeenCalled(); })); it('should open tree properly', () => { - scheduler.schedule(() => typeaheadComp.openTree(new Event('click'))); + scheduler.schedule(() => oneboxComponent.openTree(new Event('click'))); scheduler.flush(); - expect((typeaheadComp as any).modalService.open).toHaveBeenCalled(); + expect((oneboxComponent as any).modalService.open).toHaveBeenCalled(); }); }); @@ -440,8 +440,8 @@ describe('DsDynamicTypeaheadComponent test suite', () => { }) class TestComponent { - group: FormGroup = TYPEAHEAD_TEST_GROUP; + group: FormGroup = ONEBOX_TEST_GROUP; - model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG); + model = new DynamicOneboxModel(ONEBOX_TEST_MODEL_CONFIG); } 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/onebox/dynamic-onebox.component.ts similarity index 95% rename from src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts rename to src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts index 5d7413e4dd..fa9fe2103a 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/onebox/dynamic-onebox.component.ts @@ -17,7 +17,7 @@ import { Observable, of as observableOf, Subject, Subscription } from 'rxjs'; import { NgbModal, NgbModalRef, NgbTypeahead, NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap'; import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service'; -import { DynamicTypeaheadModel } from './dynamic-typeahead.model'; +import { DynamicOneboxModel } from './dynamic-onebox.model'; import { hasValue, isEmpty, isNotEmpty, isNotNull } from '../../../../../empty.util'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; import { ConfidenceType } from '../../../../../../core/shared/confidence-type'; @@ -31,14 +31,14 @@ import { VocabularyTreeviewComponent } from '../../../../../vocabulary-treeview/ import { VocabularyEntryDetail } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; @Component({ - selector: 'ds-dynamic-typeahead', - styleUrls: ['./dynamic-typeahead.component.scss'], - templateUrl: './dynamic-typeahead.component.html' + selector: 'ds-dynamic-onebox', + styleUrls: ['./dynamic-onebox.component.scss'], + templateUrl: './dynamic-onebox.component.html' }) -export class DsDynamicTypeaheadComponent extends DsDynamicVocabularyComponent implements OnInit { +export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent implements OnInit { @Input() bindId = true; @Input() group: FormGroup; - @Input() model: DynamicTypeaheadModel; + @Input() model: DynamicOneboxModel; @Output() blur: EventEmitter = new EventEmitter(); @Output() change: EventEmitter = new EventEmitter(); @@ -76,7 +76,7 @@ export class DsDynamicTypeaheadComponent extends DsDynamicVocabularyComponent im /** * Converts a stream of text values from the `` element to the stream of the array of items - * to display in the typeahead popup. + * to display in the onebox popup. */ search = (text$: Observable) => { return text$.pipe( diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.model.ts similarity index 58% rename from src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.model.ts rename to src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.model.ts index 866055ed04..4b973e3058 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.model.ts @@ -1,19 +1,19 @@ import { AUTOCOMPLETE_OFF, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core'; import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model'; -export const DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD = 'TYPEAHEAD'; +export const DYNAMIC_FORM_CONTROL_TYPE_ONEBOX = 'ONEBOX'; -export interface DsDynamicTypeaheadModelConfig extends DsDynamicInputModelConfig { +export interface DsDynamicOneboxModelConfig extends DsDynamicInputModelConfig { minChars?: number; value?: any; } -export class DynamicTypeaheadModel extends DsDynamicInputModel { +export class DynamicOneboxModel extends DsDynamicInputModel { @serializable() minChars: number; - @serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD; + @serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_ONEBOX; - constructor(config: DsDynamicTypeaheadModelConfig, layout?: DynamicFormControlLayout) { + constructor(config: DsDynamicOneboxModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); diff --git a/src/app/shared/form/builder/form-builder.service.spec.ts b/src/app/shared/form/builder/form-builder.service.spec.ts index 434fe6a2e1..ae3aa782b9 100644 --- a/src/app/shared/form/builder/form-builder.service.spec.ts +++ b/src/app/shared/form/builder/form-builder.service.spec.ts @@ -34,7 +34,7 @@ import { DynamicScrollableDropdownModel } from './ds-dynamic-form-ui/models/scro import { DynamicRelationGroupModel } from './ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; import { DynamicLookupModel } from './ds-dynamic-form-ui/models/lookup/dynamic-lookup.model'; import { DynamicDsDatePickerModel } from './ds-dynamic-form-ui/models/date-picker/date-picker.model'; -import { DynamicTypeaheadModel } from './ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.model'; +import { DynamicOneboxModel } from './ds-dynamic-form-ui/models/onebox/dynamic-onebox.model'; import { DynamicListRadioGroupModel } from './ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model'; import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model'; import { FormFieldModel } from './models/form-field.model'; @@ -195,7 +195,7 @@ describe('FormBuilderService test suite', () => { new DynamicColorPickerModel({id: 'testColorPicker'}), - new DynamicTypeaheadModel({id: 'testTypeahead', repeatable: false, metadataFields: [], submissionId: '1234'}), + new DynamicOneboxModel({id: 'testOnebox', repeatable: false, metadataFields: [], submissionId: '1234'}), new DynamicScrollableDropdownModel({id: 'testScrollableDropdown', vocabularyOptions: vocabularyOptions, repeatable: false, metadataFields: [], submissionId: '1234'}), @@ -433,7 +433,7 @@ describe('FormBuilderService test suite', () => { expect(formModel[2] instanceof DynamicRowGroupModel).toBe(true); expect((formModel[2] as DynamicRowGroupModel).group.length).toBe(1); - expect((formModel[2] as DynamicRowGroupModel).get(0) instanceof DynamicTypeaheadModel).toBe(true); + expect((formModel[2] as DynamicRowGroupModel).get(0) instanceof DynamicOneboxModel).toBe(true); }); it('should return form\'s fields value from form model', () => { @@ -449,7 +449,7 @@ describe('FormBuilderService test suite', () => { }; expect(service.getValueFromModel(formModel)).toEqual(value); - ((formModel[2] as DynamicRowGroupModel).get(0) as DynamicTypeaheadModel).valueUpdates.next('test one'); + ((formModel[2] as DynamicRowGroupModel).get(0) as DynamicOneboxModel).valueUpdates.next('test one'); value = { issue: [new FormFieldMetadataValueObject('test')], conference: [new FormFieldMetadataValueObject('test one')] @@ -462,11 +462,11 @@ describe('FormBuilderService test suite', () => { const value = {} as any; ((formModel[0] as DynamicRowGroupModel).get(1) as DsDynamicInputModel).valueUpdates.next('test'); - ((formModel[2] as DynamicRowGroupModel).get(0) as DynamicTypeaheadModel).valueUpdates.next('test one'); + ((formModel[2] as DynamicRowGroupModel).get(0) as DynamicOneboxModel).valueUpdates.next('test one'); service.clearAllModelsValue(formModel); - expect(((formModel[0] as DynamicRowGroupModel).get(1) as DynamicTypeaheadModel).value).toEqual(undefined) - expect(((formModel[2] as DynamicRowGroupModel).get(0) as DynamicTypeaheadModel).value).toEqual(undefined) + expect(((formModel[0] as DynamicRowGroupModel).get(1) as DynamicOneboxModel).value).toEqual(undefined) + expect(((formModel[2] as DynamicRowGroupModel).get(0) as DynamicOneboxModel).value).toEqual(undefined) }); it('should return true when model has a custom group model as parent', () => { diff --git a/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts b/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts index c2d1b6f565..e6a99d8e74 100644 --- a/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts @@ -1,7 +1,7 @@ import { FormFieldModel } from '../models/form-field.model'; import { OneboxFieldParser } from './onebox-field-parser'; import { DynamicQualdropModel } from '../ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model'; -import { DynamicTypeaheadModel } from '../ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.model'; +import { DynamicOneboxModel } from '../ds-dynamic-form-ui/models/onebox/dynamic-onebox.model'; import { DsDynamicInputModel } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model'; import { ParserOptions } from './parser-options'; @@ -92,12 +92,12 @@ describe('OneboxFieldParser test suite', () => { expect(fieldModel instanceof DsDynamicInputModel).toBe(true); }); - it('should return a DynamicTypeaheadModel object when selectableMetadata has authority', () => { + it('should return a DynamicOneboxModel object when selectableMetadata has authority', () => { const parser = new OneboxFieldParser(submissionId, field1, initFormValues, parserOptions); const fieldModel = parser.parse(); - expect(fieldModel instanceof DynamicTypeaheadModel).toBe(true); + expect(fieldModel instanceof DynamicOneboxModel).toBe(true); }); }); diff --git a/src/app/shared/form/builder/parsers/onebox-field-parser.ts b/src/app/shared/form/builder/parsers/onebox-field-parser.ts index 598918ac2e..3eb5764a37 100644 --- a/src/app/shared/form/builder/parsers/onebox-field-parser.ts +++ b/src/app/shared/form/builder/parsers/onebox-field-parser.ts @@ -12,9 +12,9 @@ import { FormFieldMetadataValueObject } from '../models/form-field-metadata-valu import { isNotEmpty } from '../../../empty.util'; import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model'; import { - DsDynamicTypeaheadModelConfig, - DynamicTypeaheadModel -} from '../ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.model'; + DsDynamicOneboxModelConfig, + DynamicOneboxModel +} from '../ds-dynamic-form-ui/models/onebox/dynamic-onebox.model'; export class OneboxFieldParser extends FieldParser { @@ -76,11 +76,11 @@ export class OneboxFieldParser extends FieldParser { return new DynamicQualdropModel(inputSelectGroup, clsGroup); } else if (this.configData.selectableMetadata[0].controlledVocabulary) { - const typeaheadModelConfig: DsDynamicTypeaheadModelConfig = this.initModel(null, label); - this.setVocabularyOptions(typeaheadModelConfig, this.parserOptions.collectionUUID); - this.setValues(typeaheadModelConfig, fieldValue, true); + const oneboxModelConfig: DsDynamicOneboxModelConfig = this.initModel(null, label); + this.setVocabularyOptions(oneboxModelConfig, this.parserOptions.collectionUUID); + this.setValues(oneboxModelConfig, fieldValue, true); - return new DynamicTypeaheadModel(typeaheadModelConfig); + return new DynamicOneboxModel(oneboxModelConfig); } else { const inputModelConfig: DsDynamicInputModelConfig = this.initModel(null, label); this.setValues(inputModelConfig, fieldValue); diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 38bb9afd5d..7790698059 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -48,7 +48,7 @@ import { VarDirective } from './utils/var.directive'; import { AuthNavMenuComponent } from './auth-nav-menu/auth-nav-menu.component'; import { LogOutComponent } from './log-out/log-out.component'; import { FormComponent } from './form/form.component'; -import { DsDynamicTypeaheadComponent } from './form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component'; +import { DsDynamicOneboxComponent } from './form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component'; import { DsDynamicScrollableDropdownComponent } from './form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component'; import { DsDynamicFormControlContainerComponent, @@ -271,7 +271,7 @@ const COMPONENTS = [ DsDynamicLookupRelationModalComponent, DsDynamicScrollableDropdownComponent, DsDynamicTagComponent, - DsDynamicTypeaheadComponent, + DsDynamicOneboxComponent, DsDynamicRelationGroupComponent, DsDatePickerComponent, DsDynamicFormGroupComponent, @@ -415,7 +415,7 @@ const ENTRY_COMPONENTS = [ DsDynamicLookupRelationModalComponent, DsDynamicScrollableDropdownComponent, DsDynamicTagComponent, - DsDynamicTypeaheadComponent, + DsDynamicOneboxComponent, DsDynamicRelationGroupComponent, DsDatePickerComponent, DsDynamicFormGroupComponent, From 3f0ab2cf1310f5f8bd29040a502466e553742f17 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 2 Jul 2020 14:15:16 +0200 Subject: [PATCH 17/62] [CST-3088] Fixed lint errors --- .../core/submission/vocabularies/vocabulary.service.spec.ts | 1 - .../models/onebox/dynamic-onebox.component.spec.ts | 4 ++-- .../shared/vocabulary-treeview/vocabulary-treeview.service.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts index 94ac30ab56..962507ad94 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts @@ -607,7 +607,6 @@ describe('VocabularyService', () => { }); }); - describe('searchByTop', () => { it('should proxy the call to vocabularyEntryDetailDataService.searchBy', () => { const options: VocabularyFindOptions = new VocabularyFindOptions( diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts index 3982f659af..f43d779ae3 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts @@ -65,7 +65,7 @@ describe('DsDynamicOneboxComponent test suite', () => { let vocabularyServiceStub: any; let html; let modal; - let vocabulary = { + const vocabulary = { id: 'vocabulary', name: 'vocabulary', scrollable: true, @@ -82,7 +82,7 @@ describe('DsDynamicOneboxComponent test suite', () => { } } - let hierarchicalVocabulary = { + const hierarchicalVocabulary = { id: 'hierarchicalVocabulary', name: 'hierarchicalVocabulary', scrollable: true, diff --git a/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts b/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts index 8beead5419..836ce7672a 100644 --- a/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts +++ b/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts @@ -230,7 +230,7 @@ export class VocabularyTreeviewService { const entryDetail: VocabularyEntryDetail = Object.assign(new VocabularyEntryDetail(), entry, { id: entryId }); - const hasChildren = entry.hasOtherInformation() && (entry.otherInformation as any)!.hasChildren == 'true'; + 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( From 599ddf465305fb1df3b1c2813d944afa358b0ef2 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 2 Jul 2020 14:28:12 +0200 Subject: [PATCH 18/62] [CST-3088] Renamed browseEntries variable to entries --- src/app/core/data/entries-response-parsing.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/core/data/entries-response-parsing.service.ts b/src/app/core/data/entries-response-parsing.service.ts index 2f49eb3871..ed13c3f228 100644 --- a/src/app/core/data/entries-response-parsing.service.ts +++ b/src/app/core/data/entries-response-parsing.service.ts @@ -23,12 +23,12 @@ export abstract class EntriesResponseParsingService e parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { if (isNotEmpty(data.payload)) { - let browseEntries = []; + let entries = []; if (isNotEmpty(data.payload._embedded) && Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) { const serializer = new DSpaceSerializer(this.getSerializerModel()); - browseEntries = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]); + entries = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]); } - return new GenericSuccessResponse(browseEntries, data.statusCode, data.statusText, this.processPageInfo(data.payload)); + return new GenericSuccessResponse(entries, data.statusCode, data.statusText, this.processPageInfo(data.payload)); } else { return new ErrorResponse( Object.assign( From 2238599a7d695a20a06463c6693e0ae15e14d31a Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 2 Jul 2020 17:57:31 +0200 Subject: [PATCH 19/62] [CST-3088] remove duplicated test --- .../vocabularies/vocabulary.service.spec.ts | 103 +----------------- 1 file changed, 1 insertion(+), 102 deletions(-) diff --git a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts index 962507ad94..91eda41df1 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts @@ -9,7 +9,7 @@ import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.s import { ObjectCacheService } from '../../cache/object-cache.service'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { RequestService } from '../../data/request.service'; -import { FindListOptions, VocabularyEntriesRequest } from '../../data/request.models'; +import { VocabularyEntriesRequest } from '../../data/request.models'; import { RequestParam } from '../../cache/models/request-param.model'; import { PageInfo } from '../../shared/page-info.model'; import { PaginatedList } from '../../data/paginated-list'; @@ -67,55 +67,6 @@ describe('VocabularyService', () => { } }; - const vocabularyEntry: any = { - display: 'testValue1', - value: 'testValue1', - otherInformation: {}, - type: 'vocabularyEntry' - }; - - const vocabularyEntryWithAuthority: any = { - authority: 'authorityId1', - display: 'testValue1', - value: 'testValue1', - otherInformation: { - id: 'VR131402', - parent: 'Research Subject Categories::SOCIAL SCIENCES::Social sciences::Social work', - hasChildren: 'false', - note: 'Familjeforskning' - }, - type: 'vocabularyEntry', - _links: { - vocabularyEntryDetail: { - href: 'https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:VR131402' - } - } - }; - - const vocabularyEntryDetail: any = { - authority: 'authorityId1', - display: 'testValue1', - value: 'testValue1', - otherInformation: { - id: 'VR131402', - parent: 'Research Subject Categories::SOCIAL SCIENCES::Social sciences::Social work', - hasChildren: 'true', - note: 'Familjeforskning' - }, - type: 'vocabularyEntryDetail', - _links: { - self: { - href: 'https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:VR131402' - }, - parent: { - href: 'https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:parent' - }, - children: { - href: 'https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:children' - } - } - }; - const vocabularyEntryParentDetail: any = { authority: 'authorityId2', display: 'testParent', @@ -185,27 +136,6 @@ describe('VocabularyService', () => { } }; - const anotherVocabularyEntryDetail: any = { - authority: 'authorityId1', - display: 'children', - value: 'children', - otherInformation: { - id: 'VR131402', - parent: 'Research Subject Categories::SOCIAL SCIENCES::Social sciences::Social work', - hasChildren: 'false', - note: 'Familjeforskning' - }, - type: 'vocabularyEntryDetail', - _links: { - self: { - href: 'https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:children' - }, - parent: { - href: 'https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:testValue1' - } - } - }; - 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`; @@ -230,17 +160,13 @@ describe('VocabularyService', () => { } const pageInfo = new PageInfo(); const array = [vocabulary, hierarchicalVocabulary]; - const arrayEntries = [vocabularyEntryDetail, anotherVocabularyEntryDetail]; const childrenEntries = [vocabularyEntryChildDetail, vocabularyEntryChild2Detail]; const paginatedList = new PaginatedList(pageInfo, array); - const entriesPaginatedList = new PaginatedList(pageInfo, arrayEntries); const childrenPaginatedList = new PaginatedList(pageInfo, childrenEntries); const vocabularyRD = createSuccessfulRemoteDataObject(vocabulary); - const vocabularyEntryDetailRD = createSuccessfulRemoteDataObject(vocabularyEntryDetail); const vocabularyEntryDetailParentRD = createSuccessfulRemoteDataObject(vocabularyEntryParentDetail); const vocabularyEntryChildrenRD = createSuccessfulRemoteDataObject(childrenPaginatedList); const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); - const entriesPaginatedListRD = createSuccessfulRemoteDataObject(entriesPaginatedList); const getRequestEntry$ = (successful: boolean) => { return observableOf({ response: { isSuccessful: successful, payload: vocabulary } as any @@ -375,33 +301,6 @@ describe('VocabularyService', () => { }); - describe('getVocabularyEntries', () => { - - beforeEach(() => { - requestService = getMockRequestService(getRequestEntry$(true)); - rdbService = getMockRemoteDataBuildService(); - spyOn(rdbService, 'toRemoteDataObservable').and.callThrough(); - service = initTestService(); - }); - - it('should configure a new VocabularyEntriesRequest', () => { - const expected = new VocabularyEntriesRequest(requestService.generateRequestId(), entriesRequestURL); - - scheduler.schedule(() => service.getVocabularyEntries(vocabularyOptions, pageInfo).subscribe()); - scheduler.flush(); - - expect(requestService.configure).toHaveBeenCalledWith(expected); - }); - - it('should call RemoteDataBuildService to create the RemoteData Observable', () => { - service.getVocabularyEntries(vocabularyOptions, pageInfo); - - expect(rdbService.toRemoteDataObservable).toHaveBeenCalled(); - - }); - - }); - describe('', () => { beforeEach(() => { From 2da96aac25a06b7d178ed7481ecb2a672f8d21ea Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 3 Jul 2020 15:43:08 +0200 Subject: [PATCH 20/62] initialized submission object's sections properly on submission submit --- .../submission/submit/submission-submit.component.html | 1 + .../submission/submit/submission-submit.component.spec.ts | 1 + src/app/submission/submit/submission-submit.component.ts | 8 ++++++++ 3 files changed, 10 insertions(+) diff --git a/src/app/submission/submit/submission-submit.component.html b/src/app/submission/submit/submission-submit.component.html index c9e8c6b51a..25f397bf4e 100644 --- a/src/app/submission/submit/submission-submit.component.html +++ b/src/app/submission/submit/submission-submit.component.html @@ -1,6 +1,7 @@
diff --git a/src/app/submission/submit/submission-submit.component.spec.ts b/src/app/submission/submit/submission-submit.component.spec.ts index d0a2291ba9..19cd6d55dd 100644 --- a/src/app/submission/submit/submission-submit.component.spec.ts +++ b/src/app/submission/submit/submission-submit.component.spec.ts @@ -69,6 +69,7 @@ describe('SubmissionSubmitComponent Component', () => { expect(comp.submissionId.toString()).toEqual(submissionId); expect(comp.collectionId).toBe(submissionObject.collection.id); expect(comp.selfUrl).toBe(submissionObject._links.self.href); + expect(comp.sections).toBe(submissionObject.sections); expect(comp.submissionDefinition).toBe(submissionObject.submissionDefinition); })); diff --git a/src/app/submission/submit/submission-submit.component.ts b/src/app/submission/submit/submission-submit.component.ts index d3d3ca4e66..30612c9560 100644 --- a/src/app/submission/submit/submission-submit.component.ts +++ b/src/app/submission/submit/submission-submit.component.ts @@ -10,6 +10,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { SubmissionService } from '../submission.service'; import { SubmissionObject } from '../../core/submission/models/submission-object.model'; import { Collection } from '../../core/shared/collection.model'; +import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model'; /** * This component allows to submit a new workspaceitem. @@ -33,6 +34,12 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { */ public collectionParam: string; + /** + * The list of submission's sections + * @type {WorkspaceitemSectionsObject} + */ + public sections: WorkspaceitemSectionsObject; + /** * The submission self url * @type {string} @@ -95,6 +102,7 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { this.router.navigate(['/mydspace']); } else { this.collectionId = (submissionObject.collection as Collection).id; + this.sections = submissionObject.sections; this.selfUrl = submissionObject._links.self.href; this.submissionDefinition = (submissionObject.submissionDefinition as SubmissionDefinitionsModel); this.submissionId = submissionObject.id; From 58f18737f70742a5d1d12701b49cb635831d8a4b Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 3 Jul 2020 15:53:29 +0200 Subject: [PATCH 21/62] [CST-3088] tried to resolve travis fail --- .../vocabularies/vocabulary.service.spec.ts | 175 +++++++++--------- 1 file changed, 92 insertions(+), 83 deletions(-) diff --git a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts index 91eda41df1..0998b64579 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts @@ -198,107 +198,115 @@ describe('VocabularyService', () => { halService = jasmine.createSpyObj('halService', { getEndpoint: cold('a', { a: endpointURL }) }); - - responseCacheEntry = new RequestEntry(); - responseCacheEntry.response = new RestResponse(true, 200, 'Success'); - - requestService = jasmine.createSpyObj('requestService', { - generateRequestId: requestUUID, - configure: true, - removeByHrefSubstring: {}, - getByHref: observableOf(responseCacheEntry), - getByUUID: observableOf(responseCacheEntry), - }); - rdbService = jasmine.createSpyObj('rdbService', { - buildSingle: hot('a|', { - a: vocabularyRD - }), - buildList: hot('a|', { - a: paginatedListRD - }), - }); - - service = initTestService(); - - spyOn((service as any).vocabularyDataService, 'findById').and.callThrough(); - spyOn((service as any).vocabularyDataService, 'findAll').and.callThrough(); - spyOn((service as any).vocabularyDataService, 'findByHref').and.callThrough(); - spyOn((service as any).vocabularyDataService, 'searchBy').and.callThrough(); - spyOn((service as any).vocabularyDataService, 'getSearchByHref').and.returnValue(observableOf(searchRequestURL)); - spyOn((service as any).vocabularyDataService, 'getFindAllHref').and.returnValue(observableOf(entriesRequestURL)); }); afterEach(() => { service = null; }); - describe('findVocabularyById', () => { - it('should proxy the call to vocabularyDataService.findVocabularyById', () => { - scheduler.schedule(() => service.findVocabularyById(vocabularyId)); - scheduler.flush(); + describe('', () => { + beforeEach(() => { + responseCacheEntry = new RequestEntry(); + responseCacheEntry.response = new RestResponse(true, 200, 'Success'); - expect((service as any).vocabularyDataService.findById).toHaveBeenCalledWith(vocabularyId); - }); - - it('should return a RemoteData for the object with the given id', () => { - const result = service.findVocabularyById(vocabularyId); - const expected = cold('a|', { - a: vocabularyRD + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: requestUUID, + configure: true, + removeByHrefSubstring: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: observableOf(responseCacheEntry), }); - expect(result).toBeObservable(expected); - }); - }); - - describe('findVocabularyByHref', () => { - it('should proxy the call to vocabularyDataService.findVocabularyByHref', () => { - scheduler.schedule(() => service.findVocabularyByHref(requestURL)); - scheduler.flush(); - - expect((service as any).vocabularyDataService.findByHref).toHaveBeenCalledWith(requestURL); - }); - - it('should return a RemoteData for the object with the given URL', () => { - const result = service.findVocabularyByHref(requestURL); - const expected = cold('a|', { - a: vocabularyRD + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: hot('a|', { + a: vocabularyRD + }), + buildList: hot('a|', { + a: paginatedListRD + }), }); - expect(result).toBeObservable(expected); - }); - }); - describe('findAllVocabularies', () => { - it('should proxy the call to vocabularyDataService.findAllVocabularies', () => { - scheduler.schedule(() => service.findAllVocabularies()); - scheduler.flush(); + service = initTestService(); - expect((service as any).vocabularyDataService.findAll).toHaveBeenCalled(); + spyOn((service as any).vocabularyDataService, 'findById').and.callThrough(); + spyOn((service as any).vocabularyDataService, 'findAll').and.callThrough(); + spyOn((service as any).vocabularyDataService, 'findByHref').and.callThrough(); + spyOn((service as any).vocabularyDataService, 'searchBy').and.callThrough(); + spyOn((service as any).vocabularyDataService, 'getSearchByHref').and.returnValue(observableOf(searchRequestURL)); + spyOn((service as any).vocabularyDataService, 'getFindAllHref').and.returnValue(observableOf(entriesRequestURL)); }); - it('should return a RemoteData>', () => { - const result = service.findAllVocabularies(); - const expected = cold('a|', { - a: paginatedListRD + afterEach(() => { + service = null; + }); + + describe('findVocabularyById', () => { + it('should proxy the call to vocabularyDataService.findVocabularyById', () => { + scheduler.schedule(() => service.findVocabularyById(vocabularyId)); + scheduler.flush(); + + expect((service as any).vocabularyDataService.findById).toHaveBeenCalledWith(vocabularyId); }); - expect(result).toBeObservable(expected); - }); - }); - describe('searchVocabularyByMetadataAndCollection', () => { - it('should proxy the call to vocabularyDataService.findVocabularyByHref', () => { - scheduler.schedule(() => service.searchVocabularyByMetadataAndCollection(vocabularyOptions).subscribe()); - scheduler.flush(); - - expect((service as any).vocabularyDataService.findByHref).toHaveBeenCalledWith(searchRequestURL); - }); - - it('should return a RemoteData for the search', () => { - const result = service.searchVocabularyByMetadataAndCollection(vocabularyOptions); - const expected = cold('a|', { - a: vocabularyRD + it('should return a RemoteData for the object with the given id', () => { + const result = service.findVocabularyById(vocabularyId); + const expected = cold('a|', { + a: vocabularyRD + }); + expect(result).toBeObservable(expected); }); - expect(result).toBeObservable(expected); }); + describe('findVocabularyByHref', () => { + it('should proxy the call to vocabularyDataService.findVocabularyByHref', () => { + scheduler.schedule(() => service.findVocabularyByHref(requestURL)); + scheduler.flush(); + + expect((service as any).vocabularyDataService.findByHref).toHaveBeenCalledWith(requestURL); + }); + + it('should return a RemoteData for the object with the given URL', () => { + const result = service.findVocabularyByHref(requestURL); + const expected = cold('a|', { + a: vocabularyRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('findAllVocabularies', () => { + it('should proxy the call to vocabularyDataService.findAllVocabularies', () => { + scheduler.schedule(() => service.findAllVocabularies()); + scheduler.flush(); + + expect((service as any).vocabularyDataService.findAll).toHaveBeenCalled(); + }); + + it('should return a RemoteData>', () => { + const result = service.findAllVocabularies(); + const expected = cold('a|', { + a: paginatedListRD + }); + expect(result).toBeObservable(expected); + }); + }); + + describe('searchVocabularyByMetadataAndCollection', () => { + it('should proxy the call to vocabularyDataService.findVocabularyByHref', () => { + scheduler.schedule(() => service.searchVocabularyByMetadataAndCollection(vocabularyOptions).subscribe()); + scheduler.flush(); + + expect((service as any).vocabularyDataService.findByHref).toHaveBeenCalledWith(searchRequestURL); + }); + + it('should return a RemoteData for the search', () => { + const result = service.searchVocabularyByMetadataAndCollection(vocabularyOptions); + const expected = cold('a|', { + a: vocabularyRD + }); + expect(result).toBeObservable(expected); + }); + + }); }); describe('', () => { @@ -383,6 +391,7 @@ describe('VocabularyService', () => { }); }); + }); describe('vocabularyEntryDetails endpoint', () => { From aa265c02a522e4d8c7f75cc370c004471d0ffed1 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 6 Jul 2020 10:28:23 +0200 Subject: [PATCH 22/62] [CST-3088] Fixed travis failure --- .../onebox/dynamic-onebox.component.spec.ts | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts index f43d779ae3..5bbae9c98c 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts @@ -3,7 +3,10 @@ import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/c import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { async, ComponentFixture, fakeAsync, inject, TestBed, tick, } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { CdkTreeModule } from '@angular/cdk/tree'; +import { TestScheduler } from 'rxjs/testing'; +import { getTestScheduler } from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { DynamicFormLayoutService, DynamicFormsCoreModule, DynamicFormValidationService } from '@ng-dynamic-forms/core'; @@ -22,14 +25,23 @@ import { ObjNgFor } from '../../../../../utils/object-ngfor.pipe'; import { VocabularyEntry } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry.model'; import { createSuccessfulRemoteDataObject$ } from '../../../../../remote-data.utils'; import { VocabularyTreeviewComponent } from '../../../../../vocabulary-treeview/vocabulary-treeview.component'; -import { CdkTreeModule } from '@angular/cdk/tree'; -import { TestScheduler } from 'rxjs/testing'; -import { getTestScheduler } from 'jasmine-marbles'; export let ONEBOX_TEST_GROUP; export let ONEBOX_TEST_MODEL_CONFIG; +/* tslint:disable:max-classes-per-file */ + +// Mock class for NgbModalRef +export class MockNgbModalRef { + componentInstance = { + vocabularyOptions: undefined, + preloadLevel: undefined, + selectedItem: undefined + }; + result: Promise = new Promise((resolve, reject) => resolve(true)); +} + function init() { ONEBOX_TEST_GROUP = new FormGroup({ onebox: new FormControl(), @@ -63,6 +75,7 @@ describe('DsDynamicOneboxComponent test suite', () => { let testFixture: ComponentFixture; let oneboxCompFixture: ComponentFixture; let vocabularyServiceStub: any; + let modalService: any; let html; let modal; const vocabulary = { @@ -102,14 +115,14 @@ describe('DsDynamicOneboxComponent test suite', () => { // async beforeEach beforeEach(async(() => { vocabularyServiceStub = new VocabularyServiceStub(); - modal = jasmine.createSpyObj('modal', ['open', 'close', 'dismiss']); -/* jasmine.createSpyObj('modal', + // modal = jasmine.createSpyObj('modal', ['open', 'close', 'dismiss']); + modal = jasmine.createSpyObj('modal', { - open: jasmine.createSpy('open'), + open: jasmine.createSpy('open').and.returnValue(new MockNgbModalRef()), close: jasmine.createSpy('close'), dismiss: jasmine.createSpy('dismiss'), } - );*/ + ); init(); TestBed.configureTestingModule({ imports: [ @@ -365,13 +378,13 @@ describe('DsDynamicOneboxComponent test suite', () => { beforeEach(() => { scheduler = getTestScheduler(); spyOn(vocabularyServiceStub, 'findVocabularyById').and.returnValue(createSuccessfulRemoteDataObject$(hierarchicalVocabulary)); + oneboxCompFixture = TestBed.createComponent(DsDynamicOneboxComponent); + oneboxComponent = oneboxCompFixture.componentInstance; // FormComponent test instance + modalService = TestBed.get(NgbModal); }); describe('when init model value is empty', () => { beforeEach(() => { - - oneboxCompFixture = TestBed.createComponent(DsDynamicOneboxComponent); - oneboxComponent = oneboxCompFixture.componentInstance; // FormComponent test instance oneboxComponent.group = ONEBOX_TEST_GROUP; oneboxComponent.model = new DynamicOneboxModel(ONEBOX_TEST_MODEL_CONFIG); oneboxCompFixture.detectChanges(); @@ -396,8 +409,6 @@ describe('DsDynamicOneboxComponent test suite', () => { describe('when init model value is not empty', () => { beforeEach(() => { - oneboxCompFixture = TestBed.createComponent(DsDynamicOneboxComponent); - oneboxComponent = oneboxCompFixture.componentInstance; // FormComponent test instance oneboxComponent.group = ONEBOX_TEST_GROUP; oneboxComponent.model = new DynamicOneboxModel(ONEBOX_TEST_MODEL_CONFIG); const entry = observableOf(Object.assign(new VocabularyEntry(), { @@ -445,3 +456,5 @@ class TestComponent { model = new DynamicOneboxModel(ONEBOX_TEST_MODEL_CONFIG); } + +/* tslint:enable:max-classes-per-file */ From a2834764035265cb537891cee289656ed1cc8445 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 16 Jul 2020 13:08:17 +0200 Subject: [PATCH 23/62] [CST-3088] Fixed travis failure --- .../form/process-form.component.spec.ts | 3 ++- .../onebox/dynamic-onebox.component.spec.ts | 24 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/app/process-page/form/process-form.component.spec.ts b/src/app/process-page/form/process-form.component.spec.ts index 12326111da..d32405d8bf 100644 --- a/src/app/process-page/form/process-form.component.spec.ts +++ b/src/app/process-page/form/process-form.component.spec.ts @@ -75,8 +75,9 @@ describe('ProcessFormComponent', () => { fixture.detectChanges(); }); - it('should create', () => { + it('should create', (done) => { expect(component).toBeTruthy(); + done(); }); it('should call invoke on the scriptService on submit', () => { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts index 5bbae9c98c..e94d3a208c 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts @@ -1,7 +1,7 @@ // Load the implementations that should be tested import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { async, ComponentFixture, fakeAsync, inject, TestBed, tick, } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, inject, TestBed, tick, } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { CdkTreeModule } from '@angular/cdk/tree'; @@ -113,12 +113,12 @@ describe('DsDynamicOneboxComponent test suite', () => { } // async beforeEach - beforeEach(async(() => { + beforeEach(() => { vocabularyServiceStub = new VocabularyServiceStub(); - // modal = jasmine.createSpyObj('modal', ['open', 'close', 'dismiss']); + modal = jasmine.createSpyObj('modal', { - open: jasmine.createSpy('open').and.returnValue(new MockNgbModalRef()), + open: jasmine.createSpy('open'), close: jasmine.createSpy('close'), dismiss: jasmine.createSpy('dismiss'), } @@ -150,9 +150,9 @@ describe('DsDynamicOneboxComponent test suite', () => { { provide: NgbModal, useValue: modal } ], schemas: [CUSTOM_ELEMENTS_SCHEMA] - }); + }).compileComponents(); - })); + }); describe('', () => { // synchronous beforeEach @@ -381,6 +381,7 @@ describe('DsDynamicOneboxComponent test suite', () => { oneboxCompFixture = TestBed.createComponent(DsDynamicOneboxComponent); oneboxComponent = oneboxCompFixture.componentInstance; // FormComponent test instance modalService = TestBed.get(NgbModal); + modalService.open.and.returnValue(new MockNgbModalRef()); }); describe('when init model value is empty', () => { @@ -395,15 +396,17 @@ describe('DsDynamicOneboxComponent test suite', () => { oneboxComponent = null; }); - it('should init component properly', () => { + it('should init component properly', fakeAsync(() => { + tick(); expect(oneboxComponent.currentValue).not.toBeDefined(); - }); + })); - it('should open tree properly', () => { + it('should open tree properly', (done) => { scheduler.schedule(() => oneboxComponent.openTree(new Event('click'))); scheduler.flush(); expect((oneboxComponent as any).modalService.open).toHaveBeenCalled(); + done(); }); }); @@ -433,11 +436,12 @@ describe('DsDynamicOneboxComponent test suite', () => { expect((oneboxComponent as any).vocabularyService.getVocabularyEntryByValue).toHaveBeenCalled(); })); - it('should open tree properly', () => { + it('should open tree properly', (done) => { scheduler.schedule(() => oneboxComponent.openTree(new Event('click'))); scheduler.flush(); expect((oneboxComponent as any).modalService.open).toHaveBeenCalled(); + done(); }); }); From c33a63aa90480f6235bdbeaa1df91b620cc06ae9 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 16 Jul 2020 13:13:24 +0200 Subject: [PATCH 24/62] [CST-3088] Removed metadata and collection params when making vocabulary requests --- .../models/vocabulary-find-options.model.ts | 11 +--- .../vocabularies/vocabulary.service.spec.ts | 59 +++++++++++++------ .../vocabularies/vocabulary.service.ts | 10 ---- 3 files changed, 43 insertions(+), 37 deletions(-) diff --git a/src/app/core/submission/vocabularies/models/vocabulary-find-options.model.ts b/src/app/core/submission/vocabularies/models/vocabulary-find-options.model.ts index 9dc12fad57..bd9bd55b95 100644 --- a/src/app/core/submission/vocabularies/models/vocabulary-find-options.model.ts +++ b/src/app/core/submission/vocabularies/models/vocabulary-find-options.model.ts @@ -8,9 +8,7 @@ import { isNotEmpty } from '../../../../shared/empty.util'; */ export class VocabularyFindOptions extends FindListOptions { - constructor(public collection, - public metadata, - public query: string = '', + constructor(public query: string = '', public filter?: string, public exact?: boolean, public entryID?: string, @@ -21,12 +19,7 @@ export class VocabularyFindOptions extends FindListOptions { super(); const searchParams = []; - if (isNotEmpty(metadata)) { - searchParams.push(new RequestParam('metadata', metadata)) - } - if (isNotEmpty(collection)) { - searchParams.push(new RequestParam('collection', collection)) - } + if (isNotEmpty(query)) { searchParams.push(new RequestParam('query', query)) } diff --git a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts index 0998b64579..682cc215e0 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts @@ -13,7 +13,7 @@ import { VocabularyEntriesRequest } from '../../data/request.models'; import { RequestParam } from '../../cache/models/request-param.model'; import { PageInfo } from '../../shared/page-info.model'; import { PaginatedList } from '../../data/paginated-list'; -import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { RequestEntry } from '../../data/request.reducer'; import { RestResponse } from '../../cache/response.models'; import { VocabularyService } from './vocabulary.service'; @@ -67,6 +67,27 @@ describe('VocabularyService', () => { } }; + const vocabularyEntry: any = { + display: 'testValue1', + value: 'testValue1', + otherInformation: {}, + type: 'vocabularyEntry' + }; + + const vocabularyEntry2: any = { + display: 'testValue2', + value: 'testValue2', + otherInformation: {}, + type: 'vocabularyEntry' + }; + + const vocabularyEntry3: any = { + display: 'testValue3', + value: 'testValue3', + otherInformation: {}, + type: 'vocabularyEntry' + }; + const vocabularyEntryParentDetail: any = { authority: 'authorityId2', display: 'testParent', @@ -148,10 +169,10 @@ describe('VocabularyService', () => { const collectionUUID = '8b39g7ya-5a4b-438b-851f-be1d5b4a1c5a'; const entryID = 'dsfsfsdf-5a4b-438b-851f-be1d5b4a1c5a'; const searchRequestURL = `https://rest.api/rest/api/submission/vocabularies/search/byMetadataAndCollection?metadata=${metadata}&collection=${collectionUUID}`; - const entriesRequestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}/entries?metadata=${metadata}&collection=${collectionUUID}`; - const entriesByValueRequestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}/entries?metadata=${metadata}&collection=${collectionUUID}&filter=test&exact=false`; - const entryByValueRequestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}/entries?metadata=${metadata}&collection=${collectionUUID}&filter=test&exact=true`; - const entryByIDRequestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}/entries?metadata=${metadata}&collection=${collectionUUID}&entryID=${entryID}`; + const entriesRequestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}/entries`; + const entriesByValueRequestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}/entries?filter=test&exact=false`; + const entryByValueRequestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}/entries?filter=test&exact=true`; + const entryByIDRequestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}/entries?entryID=${entryID}`; const vocabularyOptions: VocabularyOptions = { name: vocabularyId, metadata: metadata, @@ -160,16 +181,19 @@ describe('VocabularyService', () => { } const pageInfo = new PageInfo(); const array = [vocabulary, hierarchicalVocabulary]; + const arrayEntries = [vocabularyEntry, vocabularyEntry2, vocabularyEntry3]; const childrenEntries = [vocabularyEntryChildDetail, vocabularyEntryChild2Detail]; const paginatedList = new PaginatedList(pageInfo, array); + const paginatedListEntries = new PaginatedList(pageInfo, arrayEntries); const childrenPaginatedList = new PaginatedList(pageInfo, childrenEntries); const vocabularyRD = createSuccessfulRemoteDataObject(vocabulary); + const vocabularyEntriesRD = createSuccessfulRemoteDataObject$(paginatedListEntries); const vocabularyEntryDetailParentRD = createSuccessfulRemoteDataObject(vocabularyEntryParentDetail); const vocabularyEntryChildrenRD = createSuccessfulRemoteDataObject(childrenPaginatedList); const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); - const getRequestEntry$ = (successful: boolean) => { + const getRequestEntries$ = (successful: boolean) => { return observableOf({ - response: { isSuccessful: successful, payload: vocabulary } as any + response: { isSuccessful: successful, payload: arrayEntries } as any } as RequestEntry) }; objectCache = {} as ObjectCacheService; @@ -312,8 +336,8 @@ describe('VocabularyService', () => { describe('', () => { beforeEach(() => { - requestService = getMockRequestService(getRequestEntry$(true)); - rdbService = getMockRemoteDataBuildService(); + requestService = getMockRequestService(getRequestEntries$(true)); + rdbService = getMockRemoteDataBuildService(undefined, vocabularyEntriesRD); spyOn(rdbService, 'toRemoteDataObservable').and.callThrough(); service = initTestService(); }); @@ -329,10 +353,10 @@ describe('VocabularyService', () => { }); it('should call RemoteDataBuildService to create the RemoteData Observable', () => { - service.getVocabularyEntries(vocabularyOptions, pageInfo); + scheduler.schedule(() => service.getVocabularyEntries(vocabularyOptions, pageInfo)); + scheduler.flush(); expect(rdbService.toRemoteDataObservable).toHaveBeenCalled(); - }); }); @@ -347,7 +371,8 @@ describe('VocabularyService', () => { }); it('should call RemoteDataBuildService to create the RemoteData Observable', () => { - service.getVocabularyEntriesByValue('test', false, vocabularyOptions, pageInfo); + scheduler.schedule(() => service.getVocabularyEntriesByValue('test', false, vocabularyOptions, pageInfo)); + scheduler.flush(); expect(rdbService.toRemoteDataObservable).toHaveBeenCalled(); @@ -365,7 +390,8 @@ describe('VocabularyService', () => { }); it('should call RemoteDataBuildService to create the RemoteData Observable', () => { - service.getVocabularyEntryByValue('test', vocabularyOptions); + scheduler.schedule(() => service.getVocabularyEntryByValue('test', vocabularyOptions)); + scheduler.flush(); expect(rdbService.toRemoteDataObservable).toHaveBeenCalled(); @@ -383,7 +409,8 @@ describe('VocabularyService', () => { }); it('should call RemoteDataBuildService to create the RemoteData Observable', () => { - service.getVocabularyEntryByID('test', vocabularyOptions); + scheduler.schedule(() => service.getVocabularyEntryByID('test', vocabularyOptions)); + scheduler.flush(); expect(rdbService.toRemoteDataObservable).toHaveBeenCalled(); @@ -495,8 +522,6 @@ describe('VocabularyService', () => { null, null, null, - null, - null, pageInfo.elementsPerPage, pageInfo.currentPage ); @@ -522,8 +547,6 @@ describe('VocabularyService', () => { null, null, null, - null, - null, pageInfo.elementsPerPage, pageInfo.currentPage ); diff --git a/src/app/core/submission/vocabularies/vocabulary.service.ts b/src/app/core/submission/vocabularies/vocabulary.service.ts index 1dac3b56a6..648f4744d3 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.ts @@ -150,8 +150,6 @@ export class VocabularyService { getVocabularyEntries(vocabularyOptions: VocabularyOptions, pageInfo: PageInfo): Observable>> { const options: VocabularyFindOptions = new VocabularyFindOptions( - vocabularyOptions.scope, - vocabularyOptions.metadata, null, null, null, @@ -179,8 +177,6 @@ export class VocabularyService { */ getVocabularyEntriesByValue(value: string, exact: boolean, vocabularyOptions: VocabularyOptions, pageInfo: PageInfo): Observable>> { const options: VocabularyFindOptions = new VocabularyFindOptions( - vocabularyOptions.scope, - vocabularyOptions.metadata, null, value, exact, @@ -229,8 +225,6 @@ export class VocabularyService { getVocabularyEntryByID(ID: string, vocabularyOptions: VocabularyOptions): Observable { const pageInfo = new PageInfo() const options: VocabularyFindOptions = new VocabularyFindOptions( - vocabularyOptions.scope, - vocabularyOptions.metadata, null, null, null, @@ -335,8 +329,6 @@ export class VocabularyService { null, null, null, - null, - null, pageInfo.elementsPerPage, pageInfo.currentPage ); @@ -358,8 +350,6 @@ export class VocabularyService { null, null, null, - null, - null, pageInfo.elementsPerPage, pageInfo.currentPage ); From c7ef818454c9c6cf671c3f5b310619ff75365b44 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 16 Jul 2020 14:35:57 +0200 Subject: [PATCH 25/62] [CST-3088] fix ProcessFormComponent test --- .../form/process-form.component.spec.ts | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/app/process-page/form/process-form.component.spec.ts b/src/app/process-page/form/process-form.component.spec.ts index d32405d8bf..97423c8617 100644 --- a/src/app/process-page/form/process-form.component.spec.ts +++ b/src/app/process-page/form/process-form.component.spec.ts @@ -1,4 +1,4 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule } from '@angular/forms'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; @@ -14,6 +14,8 @@ import { NotificationsServiceStub } from '../../shared/testing/notifications-ser import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; import { RequestService } from '../../core/data/request.service'; import { Router } from '@angular/router'; +import { TestScheduler } from 'rxjs/testing'; +import { getTestScheduler } from 'jasmine-marbles'; describe('ProcessFormComponent', () => { let component: ProcessFormComponent; @@ -21,6 +23,9 @@ describe('ProcessFormComponent', () => { let scriptService; let parameterValues; let script; + let scheduler: TestScheduler; + let requestService: RequestService; + let router: Router; function init() { const param1 = new ScriptParameter(); @@ -41,10 +46,18 @@ describe('ProcessFormComponent', () => { } }) } - ) + ); + + requestService = jasmine.createSpyObj('requestService', { + removeByHrefSubstring: jasmine.createSpy('removeByHrefSubstring') + }); + + router = jasmine.createSpyObj('requestService', { + navigateByUrl: jasmine.createSpy('navigateByUrl') + }); } - beforeEach(async(() => { + beforeEach(() => { init(); TestBed.configureTestingModule({ imports: [ @@ -59,15 +72,16 @@ describe('ProcessFormComponent', () => { providers: [ { provide: ScriptDataService, useValue: scriptService }, { provide: NotificationsService, useClass: NotificationsServiceStub }, - { provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeBySubstring']) }, - { provide: Router, useValue: {} }, + { provide: RequestService, useValue: requestService }, + { provide: Router, useValue: router }, ], schemas: [NO_ERRORS_SCHEMA] }) .compileComponents(); - })); + }); beforeEach(() => { + scheduler = getTestScheduler(); fixture = TestBed.createComponent(ProcessFormComponent); component = fixture.componentInstance; component.parameters = parameterValues; @@ -75,13 +89,15 @@ describe('ProcessFormComponent', () => { fixture.detectChanges(); }); - it('should create', (done) => { + it('should create', () => { expect(component).toBeTruthy(); - done(); }); - it('should call invoke on the scriptService on submit', () => { - component.submitForm({ controls: {} } as any); + it('should call invoke on the scriptService on submit', (done) => { + scheduler.schedule(() => component.submitForm({ controls: {} } as any)); + scheduler.flush(); + expect(scriptService.invoke).toHaveBeenCalled(); + done(); }); }); From 607318f6c5cfdb8ef1e517b968264c33648b74a2 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 13 Jul 2020 15:42:52 +0200 Subject: [PATCH 26/62] Fix CommunityDataService's topLinkPath --- src/app/core/data/community-data.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index 123c3eccd1..474bdef44a 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -24,7 +24,7 @@ import { RequestService } from './request.service'; @dataService(COMMUNITY) export class CommunityDataService extends ComColDataService { protected linkPath = 'communities'; - protected topLinkPath = 'communities/search/top'; + protected topLinkPath = 'search/top'; protected cds = this; constructor( From 6fecdfadb2521a1d6269170579ec3025eeff4090 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 21 Jul 2020 18:53:09 +0200 Subject: [PATCH 27/62] Fixed issue that redirect always to home page after checking authentication on CSR --- src/app/core/auth/auth.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 85e5eebb9e..7f61bec9f3 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -423,6 +423,7 @@ export class AuthService { } else { // If redirectUrl is empty use history. this.routeService.getHistory().pipe( + filter((history) => history.length > 0), take(1) ).subscribe((history) => { let redirUrl; From 47ab023e7a090523246f76cc83b90d9aeb003caa Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 22 Jul 2020 17:03:52 +0200 Subject: [PATCH 28/62] fixed VocabularyTreeviewComponent selector --- .../vocabulary-treeview/vocabulary-treeview.component.spec.ts | 2 +- .../shared/vocabulary-treeview/vocabulary-treeview.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 9af00b6be7..16ee2a55dd 100644 --- a/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.spec.ts +++ b/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.spec.ts @@ -79,7 +79,7 @@ describe('VocabularyTreeviewComponent test suite', () => { // synchronous beforeEach beforeEach(() => { const html = ` - `; + `; testFixture = createTestComponent(html, TestComponent) as ComponentFixture; testComp = testFixture.componentInstance; diff --git a/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts b/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts index 8c7543849d..6de1a11e00 100644 --- a/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts +++ b/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts @@ -22,7 +22,7 @@ import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocab * Component that show a hierarchical vocabulary in a tree view */ @Component({ - selector: 'ds-authority-treeview', + selector: 'ds-vocabulary-treeview', templateUrl: './vocabulary-treeview.component.html', styleUrls: ['./vocabulary-treeview.component.scss'] }) From d88a863a0d8b1041f01bcf0b9d698b3440e21529 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 22 Jul 2020 17:40:03 +0200 Subject: [PATCH 29/62] fixed issue that didn't allow to select tree node after a search --- .../vocabulary-treeview-node.model.ts | 1 + .../vocabulary-treeview.component.ts | 5 ++- .../vocabulary-treeview.service.spec.ts | 33 ++++++++++++----- .../vocabulary-treeview.service.ts | 36 ++++++++++--------- 4 files changed, 48 insertions(+), 27 deletions(-) diff --git a/src/app/shared/vocabulary-treeview/vocabulary-treeview-node.model.ts b/src/app/shared/vocabulary-treeview/vocabulary-treeview-node.model.ts index 60473c77ca..2ed21cbdf0 100644 --- a/src/app/shared/vocabulary-treeview/vocabulary-treeview-node.model.ts +++ b/src/app/shared/vocabulary-treeview/vocabulary-treeview-node.model.ts @@ -34,6 +34,7 @@ export class TreeviewFlatNode { constructor(public item: VocabularyEntryDetail, public level = 1, public expandable = false, + public childrenLoaded = false, public pageInfo: PageInfo = new PageInfo(), public loadMoreParentItem: VocabularyEntryDetail | null = null, public isSearchNode = false, diff --git a/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts b/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts index 6de1a11e00..cbd9c25a9b 100644 --- a/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts +++ b/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts @@ -143,6 +143,7 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { node.item, level, node.hasChildren, + (node.hasChildren && isNotEmpty(node.children)), node.pageInfo, node.loadMoreParentItem, node.isSearchNode, @@ -150,7 +151,9 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { ); this.nodeMap.set(node.item.id, newNode); - if ((((level + 1) < this.preloadLevel) && newNode.expandable) || newNode.isSearchNode || newNode.isInInitValueHierarchy) { + if ((((level + 1) < this.preloadLevel) && newNode.childrenLoaded) + || (newNode.isSearchNode && newNode.childrenLoaded) + || newNode.isInInitValueHierarchy) { if (!newNode.isSearchNode) { this.loadChildren(newNode); } 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 2f03549898..944ca0bfb0 100644 --- a/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.spec.ts +++ b/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.spec.ts @@ -13,6 +13,7 @@ import { PageInfo } from '../../core/shared/page-info.model'; import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; import { PaginatedList } from '../../core/data/paginated-list'; import { createSuccessfulRemoteDataObject } from '../remote-data.utils'; +import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model'; describe('VocabularyTreeviewService test suite', () => { @@ -34,6 +35,7 @@ describe('VocabularyTreeviewService test suite', () => { let childNode: TreeviewNode; let child2: VocabularyEntryDetail; let childNode2: TreeviewNode; + let childEntry3: VocabularyEntry; let child3: VocabularyEntryDetail; let childNode3: TreeviewNode; let searchItemNode: TreeviewNode; @@ -71,37 +73,46 @@ describe('VocabularyTreeviewService test suite', () => { }); 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); + loadMoreRootFlatNode = new TreeviewFlatNode(LOAD_MORE_ROOT_NODE, 1, false, false, new PageInfo(), null); item = new VocabularyEntryDetail(); - item.id = item.value = item.display = 'root1'; + item.id = 'vocabularyTest:root1'; + item.value = item.display = 'root1'; 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.id = 'vocabularyTest:root2'; + item2.value = item2.display = 'root2'; item2.otherInformation = { id: 'root2' }; itemNode2 = new TreeviewNode(item2, false, pageInfo); item3 = new VocabularyEntryDetail(); - item3.id = item3.value = item3.display = 'root3'; + item3.id = 'vocabularyTest:root3'; + item3.value = item3.display = 'root3'; item3.otherInformation = { id: 'root3' }; itemNode3 = new TreeviewNode(item3, false, pageInfo); child = new VocabularyEntryDetail(); - child.id = child.value = child.display = 'root1-child1'; + child.id = 'vocabularyTest:root1-child1'; + child.value = child.display = 'root1-child1'; child.otherInformation = { parent: 'root1', hasChildren: 'true', id: 'root1-child1' }; childNode = new TreeviewNode(child); searchChildNode = new TreeviewNode(child, true, new PageInfo(), item, true); + childEntry3 = new VocabularyEntry(); + childEntry3.value = childEntry3.display = 'root1-child1-child1'; + childEntry3.otherInformation = { parent: 'root1-child1', id: 'root1-child1-child1' }; child3 = new VocabularyEntryDetail(); - child3.id = child3.value = child3.display = 'root1-child1-child1'; + child3.id = 'vocabularyTest:root1-child1-child1'; + child3.value = child3.display = 'root1-child1-child1'; child3.otherInformation = { parent: 'root1-child1', id: 'root1-child1-child1' }; childNode3 = new TreeviewNode(child3); searchChildNode3 = new TreeviewNode(child3, false, new PageInfo(), child, true); child2 = new VocabularyEntryDetail(); - child2.id = child2.value = child2.display = 'root1-child2'; + child2.id = 'vocabularyTest:root1-child2'; + child2.value = child2.display = 'root1-child2'; child2.otherInformation = { parent: 'root1', id: 'root1-child2' }; childNode2 = new TreeviewNode(child2, true); initValueChildNode2 = new TreeviewNode(child2, false, new PageInfo(), item, false, true); @@ -184,7 +195,6 @@ describe('VocabularyTreeviewService test suite', () => { 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]); }); @@ -290,7 +300,11 @@ describe('VocabularyTreeviewService test suite', () => { currentPage: 1 }); serviceAsAny.vocabularyService.getVocabularyEntriesByValue.and.returnValue(hot('-a', { - a: createSuccessfulRemoteDataObject(new PaginatedList(pageInfo, [child3])) + a: createSuccessfulRemoteDataObject(new PaginatedList(pageInfo, [childEntry3])) + })); + + serviceAsAny.vocabularyService.findEntryDetailByValue.and.returnValue(hot('-a', { + a: createSuccessfulRemoteDataObject(child3) })); serviceAsAny.vocabularyService.getEntryDetailParent.and.returnValues( @@ -305,6 +319,7 @@ describe('VocabularyTreeviewService test suite', () => { scheduler.schedule(() => service.searchByQuery(vocabularyOptions)); scheduler.flush(); + searchChildNode.childrenChange.next([searchChildNode3]); searchItemNode.childrenChange.next([searchChildNode]); expect(serviceAsAny.dataChange.value.length).toEqual(1); diff --git a/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts b/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts index 836ce7672a..698f5bb0d2 100644 --- a/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts +++ b/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts @@ -134,13 +134,13 @@ export class VocabularyTreeviewService { * @param onlyFirstTime */ loadMore(item: VocabularyEntryDetail, onlyFirstTime = false) { - if (!this.nodeMap.has(item.id)) { + if (!this.nodeMap.has(item.otherInformation.id)) { return; } - const parent: TreeviewNode = this.nodeMap.get(item.id)!; - const children = this.nodeMap.get(item.id)!.children || []; + const parent: TreeviewNode = this.nodeMap.get(item.otherInformation.id)!; + const children = this.nodeMap.get(item.otherInformation.id)!.children || []; children.pop(); - this.getChildrenNodesByParent(item.id, parent.pageInfo).subscribe((list: PaginatedList) => { + this.getChildrenNodesByParent(item.otherInformation.id, parent.pageInfo).subscribe((list: PaginatedList) => { if (onlyFirstTime && parent.children!.length > 0) { return; @@ -187,9 +187,14 @@ export class VocabularyTreeviewService { this.vocabularyService.getVocabularyEntriesByValue(query, false, this.vocabularyOptions, new PageInfo()).pipe( getFirstSucceededRemoteListPayload(), flatMap((result: VocabularyEntry[]) => (result.length > 0) ? result : observableOf(null)), - flatMap((entry: VocabularyEntry) => this.getNodeHierarchy(entry)), + flatMap((entry: VocabularyEntry) => + this.vocabularyService.findEntryDetailByValue(entry.otherInformation.id, this.vocabularyName).pipe( + getFirstSucceededRemoteDataPayload() + ) + ), + flatMap((entry: VocabularyEntryDetail) => this.getNodeHierarchy(entry)), scan((acc: TreeviewNode[], value: TreeviewNode) => { - if (isEmpty(value) || findIndex(acc, (node) => node.item.id === value.item.id) !== -1) { + if (isEmpty(value) || findIndex(acc, (node) => node.item.otherInformation.id === value.item.otherInformation.id) !== -1) { return acc; } else { return [...acc, value] @@ -217,24 +222,21 @@ export class VocabularyTreeviewService { /** * Generate a {@link TreeviewNode} object from vocabulary entry * - * @param entry The vocabulary entry + * @param entry The vocabulary entry detail * @param isSearchNode A Boolean representing if given entry is the result of a search * @param toStore A Boolean representing if the node created is to store or not * @return TreeviewNode */ - private _generateNode(entry: VocabularyEntry, isSearchNode = false, toStore = true): TreeviewNode { + private _generateNode(entry: VocabularyEntryDetail, isSearchNode = false, toStore = true): TreeviewNode { const entryId = entry.otherInformation.id; if (this.nodeMap.has(entryId)) { return this.nodeMap.get(entryId)!; } - const entryDetail: VocabularyEntryDetail = Object.assign(new VocabularyEntryDetail(), entry, { - id: entryId - }); 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, + entry, hasChildren, pageInfo, null, @@ -328,7 +330,7 @@ export class VocabularyTreeviewService { * @param toStore A Boolean representing if the node created is to store or not * @return Observable */ - private getNodeHierarchy(item: VocabularyEntry, children?: TreeviewNode[], toStore = true): Observable { + private getNodeHierarchy(item: VocabularyEntryDetail, children?: TreeviewNode[], toStore = true): Observable { if (isEmpty(item)) { return observableOf(null); } @@ -337,7 +339,7 @@ export class VocabularyTreeviewService { if (isNotEmpty(children)) { const newChildren = children .filter((entry: TreeviewNode) => { - return findIndex(node.children, (nodeEntry) => nodeEntry.item.id === entry.item.id) === -1; + return findIndex(node.children, (nodeEntry) => nodeEntry.item.otherInformation.id === entry.item.otherInformation.id) === -1; }); newChildren.forEach((entry: TreeviewNode) => { entry.loadMoreParentItem = node.item @@ -346,7 +348,7 @@ export class VocabularyTreeviewService { } if (node.item.hasOtherInformation() && isNotEmpty(node.item.otherInformation.parent)) { - return this.getParentNode(node.item.id).pipe( + return this.getParentNode(node.item.otherInformation.id).pipe( flatMap((parentItem: VocabularyEntryDetail) => this.getNodeHierarchy(parentItem, [node], toStore)) ) } else { @@ -362,8 +364,8 @@ export class VocabularyTreeviewService { * @return string[] */ private getNodeHierarchyIds(node: TreeviewNode, hierarchyIds: string[] = []): string[] { - if (!hierarchyIds.includes(node.item.id)) { - hierarchyIds.push(node.item.id); + if (!hierarchyIds.includes(node.item.otherInformation.id)) { + hierarchyIds.push(node.item.otherInformation.id); } if (isNotEmpty(node.children)) { return this.getNodeHierarchyIds(node.children[0], hierarchyIds); From fb1b6c7a7abf0f79337d0fce64ad8752a6e92344 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 22 Jul 2020 17:49:36 +0200 Subject: [PATCH 30/62] Changed findEntryDetailByValue method name to a properly findEntryDetailById --- .../submission/vocabularies/vocabulary.service.spec.ts | 6 +++--- .../core/submission/vocabularies/vocabulary.service.ts | 8 ++++---- .../relation-group/dynamic-relation-group.components.ts | 2 +- .../vocabulary-treeview.service.spec.ts | 6 +++--- .../vocabulary-treeview/vocabulary-treeview.service.ts | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts index 682cc215e0..16e0eaf844 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts @@ -481,16 +481,16 @@ describe('VocabularyService', () => { }); }); - describe('findEntryDetailByValue', () => { + describe('findEntryDetailById', () => { it('should proxy the call to vocabularyDataService.findVocabularyById', () => { - scheduler.schedule(() => service.findEntryDetailByValue('testValue', hierarchicalVocabulary.id)); + scheduler.schedule(() => service.findEntryDetailById('testValue', hierarchicalVocabulary.id)); scheduler.flush(); const expectedId = `${hierarchicalVocabulary.id}:testValue` expect((service as any).vocabularyEntryDetailDataService.findById).toHaveBeenCalledWith(expectedId); }); it('should return a RemoteData for the object with the given id', () => { - const result = service.findEntryDetailByValue('testValue', hierarchicalVocabulary.id); + const result = service.findEntryDetailById('testValue', hierarchicalVocabulary.id); const expected = cold('a|', { a: vocabularyEntryDetailParentRD }); diff --git a/src/app/core/submission/vocabularies/vocabulary.service.ts b/src/app/core/submission/vocabularies/vocabulary.service.ts index 648f4744d3..5200d703b9 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.ts @@ -283,15 +283,15 @@ export class VocabularyService { /** * Returns an observable of {@link RemoteData} of a {@link VocabularyEntryDetail}, based on its ID, with a list of {@link FollowLinkConfig}, * to automatically resolve {@link HALLink}s of the object - * @param value The entry value for which to provide detailed information. + * @param id The entry id for which to provide detailed information. * @param name The name of {@link Vocabulary} to which the entry belongs * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved * @return {Observable>} * Return an observable that emits VocabularyEntryDetail object */ - findEntryDetailByValue(value: string, name: string, ...linksToFollow: Array>): Observable> { - const id = `${name}:${value}`; - return this.vocabularyEntryDetailDataService.findById(id, ...linksToFollow); + findEntryDetailById(id: string, name: string, ...linksToFollow: Array>): Observable> { + const findId = `${name}:${id}`; + return this.vocabularyEntryDetailDataService.findById(findId, ...linksToFollow); } /** diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts index 51cf69d560..5c2616dd34 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts @@ -233,7 +233,7 @@ export class DsDynamicRelationGroupComponent extends DynamicFormControlComponent if (isObject(valueObj[fieldName]) && valueObj[fieldName].hasAuthority() && isNotEmpty(valueObj[fieldName].authority)) { const fieldId = fieldName.replace(/\./g, '_'); const model = this.formBuilderService.findById(fieldId, this.formModel); - return$ = this.vocabularyService.findEntryDetailByValue( + return$ = this.vocabularyService.findEntryDetailById( valueObj[fieldName].authority, (model as any).vocabularyOptions.name ).pipe( 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 944ca0bfb0..f45478f82b 100644 --- a/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.spec.ts +++ b/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.spec.ts @@ -57,7 +57,7 @@ describe('VocabularyTreeviewService test suite', () => { const vocabularyServiceStub = jasmine.createSpyObj('VocabularyService', { getVocabularyEntriesByValue: jasmine.createSpy('getVocabularyEntriesByValue'), getEntryDetailParent: jasmine.createSpy('getEntryDetailParent'), - findEntryDetailByValue: jasmine.createSpy('findEntryDetailByValue'), + findEntryDetailById: jasmine.createSpy('findEntryDetailById'), searchTopEntries: jasmine.createSpy('searchTopEntries'), getEntryDetailChildren: jasmine.createSpy('getEntryDetailChildren'), clearSearchTopRequests: jasmine.createSpy('clearSearchTopRequests') @@ -202,7 +202,7 @@ describe('VocabularyTreeviewService test suite', () => { serviceAsAny.vocabularyService.searchTopEntries.and.returnValue(hot('-c', { a: createSuccessfulRemoteDataObject(new PaginatedList(pageInfo, [item, item2, item3])) })); - serviceAsAny.vocabularyService.findEntryDetailByValue.and.returnValue( + serviceAsAny.vocabularyService.findEntryDetailById.and.returnValue( hot('-a', { a: createSuccessfulRemoteDataObject(child2) }) @@ -303,7 +303,7 @@ describe('VocabularyTreeviewService test suite', () => { a: createSuccessfulRemoteDataObject(new PaginatedList(pageInfo, [childEntry3])) })); - serviceAsAny.vocabularyService.findEntryDetailByValue.and.returnValue(hot('-a', { + serviceAsAny.vocabularyService.findEntryDetailById.and.returnValue(hot('-a', { a: createSuccessfulRemoteDataObject(child3) })); diff --git a/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts b/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts index 698f5bb0d2..dd58a6aede 100644 --- a/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts +++ b/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.ts @@ -188,7 +188,7 @@ export class VocabularyTreeviewService { getFirstSucceededRemoteListPayload(), flatMap((result: VocabularyEntry[]) => (result.length > 0) ? result : observableOf(null)), flatMap((entry: VocabularyEntry) => - this.vocabularyService.findEntryDetailByValue(entry.otherInformation.id, this.vocabularyName).pipe( + this.vocabularyService.findEntryDetailById(entry.otherInformation.id, this.vocabularyName).pipe( getFirstSucceededRemoteDataPayload() ) ), @@ -289,7 +289,7 @@ export class VocabularyTreeviewService { * @return Observable */ private getById(entryId: string): Observable { - return this.vocabularyService.findEntryDetailByValue(entryId, this.vocabularyName).pipe( + return this.vocabularyService.findEntryDetailById(entryId, this.vocabularyName).pipe( getFirstSucceededRemoteDataPayload() ); } From 7e7ce7b06e0e8b7347ee146f31d6a33e302936ab Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 30 Jul 2020 18:37:45 +0200 Subject: [PATCH 31/62] Added check for VocabularyEntry when initializing a controlled vocabulary component's init model --- .../models/dynamic-vocabulary.component.ts | 7 +++++++ 1 file changed, 7 insertions(+) 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 5e6acf8581..d3263f1584 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 @@ -70,6 +70,13 @@ export abstract class DsDynamicVocabularyComponent extends DynamicFormControlCom return this.model.value as any; } })); + } else if (isNotEmpty(this.model.value) && (this.model.value instanceof VocabularyEntry)) { + initValue$ = observableOf(Object.assign(new FormFieldMetadataValueObject(), this.model.value, { + value: this.model.value.value, + authority: this.model.value.authority, + display: this.model.value.display, + otherInformation: this.model.value.otherInformation || null + })); } else { initValue$ = observableOf(new FormFieldMetadataValueObject(this.model.value)); } From 7dae82cbbd54034510db2dcd79b6d15210ad49ad Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 30 Jul 2020 18:41:04 +0200 Subject: [PATCH 32/62] added virtualValue property to FormFieldMetadataValueObject --- .../models/form-field-metadata-value.model.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) 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 2b451a7217..752db8bd5c 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 @@ -51,26 +51,53 @@ export class FormFieldMetadataValueObject implements MetadataValueInterface { this.otherInformation = otherInformation; } + /** + * Returns true if this this object has an authority value + */ hasAuthority(): boolean { return isNotEmpty(this.authority); } + /** + * Returns true if this this object has a value + */ hasValue(): boolean { return isNotEmpty(this.value); } + /** + * Returns true if this this object has otherInformation property with value + */ hasOtherInformation(): boolean { return isNotEmpty(this.otherInformation); } + /** + * Returns true if this object value contains a placeholder + */ hasPlaceholder() { return this.hasValue() && this.value === PLACEHOLDER_PARENT_METADATA; } + /** + * Returns true if this Metadatum's authority key starts with 'virtual::' + */ get isVirtual(): boolean { return hasValue(this.authority) && this.authority.startsWith(VIRTUAL_METADATA_PREFIX); } + /** + * If this is a virtual Metadatum, it returns everything in the authority key after 'virtual::'. + * Returns undefined otherwise. + */ + get virtualValue(): string { + if (this.isVirtual) { + return this.authority.substring(this.authority.indexOf(VIRTUAL_METADATA_PREFIX) + VIRTUAL_METADATA_PREFIX.length); + } else { + return undefined; + } + } + toString() { return this.display || this.value; } From 1a9d20161c0cf0d8f618e72e2c7c9d6f12d27356 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 30 Jul 2020 18:43:19 +0200 Subject: [PATCH 33/62] Fixed issue while preparing json patch value in case of array in the JsonPatchOperationsBuilder --- .../builder/json-patch-operations-builder.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/app/core/json-patch/builder/json-patch-operations-builder.ts b/src/app/core/json-patch/builder/json-patch-operations-builder.ts index 7ce0358ab2..3755821bf9 100644 --- a/src/app/core/json-patch/builder/json-patch-operations-builder.ts +++ b/src/app/core/json-patch/builder/json-patch-operations-builder.ts @@ -1,9 +1,14 @@ import { Store } from '@ngrx/store'; import { CoreState } from '../../core.reducers'; -import { NewPatchAddOperationAction, NewPatchMoveOperationAction, NewPatchRemoveOperationAction, NewPatchReplaceOperationAction } from '../json-patch-operations.actions'; +import { + NewPatchAddOperationAction, + NewPatchMoveOperationAction, + NewPatchRemoveOperationAction, + NewPatchReplaceOperationAction +} from '../json-patch-operations.actions'; import { JsonPatchOperationPathObject } from './json-patch-operation-path-combiner'; import { Injectable } from '@angular/core'; -import { hasNoValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; +import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; import { dateToISOFormat } from '../../../shared/date.util'; import { VocabularyEntry } from '../../submission/vocabularies/models/vocabulary-entry.model'; import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; @@ -96,7 +101,7 @@ export class JsonPatchOperationsBuilder { protected prepareValue(value: any, plain: boolean, first: boolean) { let operationValue: any = null; - if (isNotEmpty(value)) { + if (hasValue(value)) { if (plain) { operationValue = value; } else { From 85506238b36a9eaaba3d5804e40aa2e2cbc9f23d Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 30 Jul 2020 18:46:17 +0200 Subject: [PATCH 34/62] Fixed issue with form dropdown field which didn't close menu after entry selection --- .../dynamic-scrollable-dropdown.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html index 7a35287a99..f40d58bb0e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html @@ -26,7 +26,7 @@ [scrollWindow]="false"> -

{{'form.loading' | translate}}

From 090fb94c4da57df2dbe415f520d5cd0e4ae32f85 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 30 Jul 2020 18:50:47 +0200 Subject: [PATCH 35/62] Normalized submission section data when retrieving them from item object --- ...ynamic-form-control-container.component.ts | 5 +- .../models/ds-dynamic-input.model.ts | 12 ++- .../objects/submission-objects.actions.ts | 9 ++- .../submission-objects.effects.spec.ts | 2 +- .../objects/submission-objects.effects.ts | 74 ++++++++++++++----- .../objects/submission-objects.reducer.ts | 2 +- 6 files changed, 76 insertions(+), 28 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index edc6efc5f9..7873f3a86e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -116,6 +116,7 @@ import { followLink } from '../../../utils/follow-link-config.model'; import { paginatedRelationsToItems } from '../../../../+item-page/simple/item-types/shared/item-relationships-utils'; import { RelationshipOptions } from '../models/relationship-options.model'; import { FormBuilderService } from '../form-builder.service'; +import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type | null { switch (model.type) { @@ -303,9 +304,9 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo } if (hasValue(this.model.metadataValue)) { - this.value = Object.assign(new MetadataValue(), this.model.metadataValue); + this.value = Object.assign(new FormFieldMetadataValueObject(), this.model.metadataValue); } else { - this.value = Object.assign(new MetadataValue(), this.model.value); + this.value = Object.assign(new FormFieldMetadataValueObject(), this.model.value); } if (hasValue(this.value) && this.value.isVirtual) { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts index c91a0aea54..290e29dc65 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts @@ -1,4 +1,9 @@ -import { DynamicFormControlLayout, DynamicInputModel, DynamicInputModelConfig, serializable } from '@ng-dynamic-forms/core'; +import { + DynamicFormControlLayout, + DynamicInputModel, + DynamicInputModelConfig, + serializable +} from '@ng-dynamic-forms/core'; import { Subject } from 'rxjs'; import { LanguageCode } from '../../models/form-field-language-value.model'; @@ -6,7 +11,6 @@ import { VocabularyOptions } from '../../../../../core/submission/vocabularies/m import { hasValue } from '../../../../empty.util'; import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model'; import { RelationshipOptions } from '../../models/relationship-options.model'; -import { MetadataValue } from '../../../../../core/shared/metadata.models'; export interface DsDynamicInputModelConfig extends DynamicInputModelConfig { vocabularyOptions?: VocabularyOptions; @@ -19,7 +23,7 @@ export interface DsDynamicInputModelConfig extends DynamicInputModelConfig { metadataFields: string[]; submissionId: string; hasSelectableMetadata: boolean; - metadataValue?: MetadataValue; + metadataValue?: FormFieldMetadataValueObject; } @@ -34,7 +38,7 @@ export class DsDynamicInputModel extends DynamicInputModel { @serializable() metadataFields: string[]; @serializable() submissionId: string; @serializable() hasSelectableMetadata: boolean; - @serializable() metadataValue: MetadataValue; + @serializable() metadataValue: FormFieldMetadataValueObject; constructor(config: DsDynamicInputModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); diff --git a/src/app/submission/objects/submission-objects.actions.ts b/src/app/submission/objects/submission-objects.actions.ts index 1e3e44aba9..73c070846c 100644 --- a/src/app/submission/objects/submission-objects.actions.ts +++ b/src/app/submission/objects/submission-objects.actions.ts @@ -42,7 +42,8 @@ export const SubmissionObjectActionTypes = { DISABLE_SECTION: type('dspace/submission/DISABLE_SECTION'), SECTION_STATUS_CHANGE: type('dspace/submission/SECTION_STATUS_CHANGE'), SECTION_LOADING_STATUS_CHANGE: type('dspace/submission/SECTION_LOADING_STATUS_CHANGE'), - UPLOAD_SECTION_DATA: type('dspace/submission/UPLOAD_SECTION_DATA'), + UPDATE_SECTION_DATA: type('dspace/submission/UPDATE_SECTION_DATA'), + UPDATE_SECTION_DATA_SUCCESS: type('dspace/submission/UPDATE_SECTION_DATA_SUCCESS'), SAVE_AND_DEPOSIT_SUBMISSION: type('dspace/submission/SAVE_AND_DEPOSIT_SUBMISSION'), DEPOSIT_SUBMISSION: type('dspace/submission/DEPOSIT_SUBMISSION'), DEPOSIT_SUBMISSION_SUCCESS: type('dspace/submission/DEPOSIT_SUBMISSION_SUCCESS'), @@ -199,7 +200,7 @@ export class DisableSectionAction implements Action { } export class UpdateSectionDataAction implements Action { - type = SubmissionObjectActionTypes.UPLOAD_SECTION_DATA; + type = SubmissionObjectActionTypes.UPDATE_SECTION_DATA; payload: { submissionId: string; sectionId: string; @@ -227,6 +228,10 @@ export class UpdateSectionDataAction implements Action { } } +export class UpdateSectionDataSuccessAction implements Action { + type = SubmissionObjectActionTypes.UPDATE_SECTION_DATA_SUCCESS; +} + export class RemoveSectionErrorsAction implements Action { type = SubmissionObjectActionTypes.REMOVE_SECTION_ERRORS; payload: { diff --git a/src/app/submission/objects/submission-objects.effects.spec.ts b/src/app/submission/objects/submission-objects.effects.spec.ts index 6c2e9eefc6..c35968c0a0 100644 --- a/src/app/submission/objects/submission-objects.effects.spec.ts +++ b/src/app/submission/objects/submission-objects.effects.spec.ts @@ -276,7 +276,7 @@ describe('SubmissionObjectEffects test suite', () => { describe('saveSubmissionSuccess$', () => { - it('should return a UPLOAD_SECTION_DATA action for each updated section', () => { + it('should return a UPDATE_SECTION_DATA action for each updated section', () => { store.nextState({ submission: { objects: submissionState diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts index 2dfed9ee47..ecda75759e 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; -import { isEqual, union } from 'lodash'; +import { union } from 'lodash'; import { from as observableFrom, Observable, of as observableOf } from 'rxjs'; import { catchError, filter, map, mergeMap, switchMap, take, tap, withLatestFrom } from 'rxjs/operators'; @@ -40,7 +40,8 @@ import { SaveSubmissionSectionFormSuccessAction, SubmissionObjectAction, SubmissionObjectActionTypes, - UpdateSectionDataAction + UpdateSectionDataAction, + UpdateSectionDataSuccessAction } from './submission-objects.actions'; import { SubmissionObjectEntry, SubmissionSectionObject } from './submission-objects.reducer'; import { Item } from '../../core/shared/item.model'; @@ -48,6 +49,8 @@ import { RemoteData } from '../../core/data/remote-data'; import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; import { SubmissionObjectDataService } from '../../core/submission/submission-object-data.service'; import { followLink } from '../../shared/utils/follow-link-config.model'; +import { normalizeSectionData } from '../../core/submission/submission-response-parsing.service'; +import { difference } from '../../shared/object.util'; @Injectable() export class SubmissionObjectEffects { @@ -69,7 +72,7 @@ export class SubmissionObjectEffects { if (sectionDefinition.sectionType !== SectionsType.SubmissionForm) { sectionData = (isNotUndefined(action.payload.sections) && isNotUndefined(action.payload.sections[sectionId])) ? action.payload.sections[sectionId] : Object.create(null); } else { - sectionData = action.payload.item.metadata; + sectionData = normalizeSectionData(action.payload.item.metadata); } const sectionErrors = null; mappedActions.push( @@ -246,28 +249,37 @@ export class SubmissionObjectEffects { * Adds all metadata an item to the SubmissionForm sections of the submission */ @Effect() addAllMetadataToSectionData = this.actions$.pipe( - ofType(SubmissionObjectActionTypes.UPLOAD_SECTION_DATA), + ofType(SubmissionObjectActionTypes.UPDATE_SECTION_DATA), switchMap((action: UpdateSectionDataAction) => { return this.sectionService.getSectionState(action.payload.submissionId, action.payload.sectionId) .pipe(map((section: SubmissionSectionObject) => [action, section]), take(1)); }), filter(([action, section]: [UpdateSectionDataAction, SubmissionSectionObject]) => section.sectionType === SectionsType.SubmissionForm), switchMap(([action, section]: [UpdateSectionDataAction, SubmissionSectionObject]) => { - const submissionObject$ = this.submissionObjectService - .findById(action.payload.submissionId, followLink('item')).pipe( - getFirstSucceededRemoteDataPayload() + if (section.sectionType === SectionsType.SubmissionForm) { + const submissionObject$ = this.submissionObjectService + .findById(action.payload.submissionId, followLink('item')).pipe( + getFirstSucceededRemoteDataPayload() + ); + + const item$ = submissionObject$.pipe( + switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable>).pipe( + getFirstSucceededRemoteDataPayload(), + ))); + + return item$.pipe( + map((item: Item) => item.metadata), + map((metadata: any) => { + if (!this.isEqual(action.payload.data, normalizeSectionData(metadata))) { + return new UpdateSectionDataAction(action.payload.submissionId, action.payload.sectionId, normalizeSectionData(metadata), action.payload.errors) + } else { + return new UpdateSectionDataSuccessAction(); + } + }) ); - - const item$ = submissionObject$.pipe( - switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable>).pipe( - getFirstSucceededRemoteDataPayload(), - ))); - - return item$.pipe( - map((item: Item) => item.metadata), - filter((metadata) => !isEqual(action.payload.data, metadata)), - map((metadata: any) => new UpdateSectionDataAction(action.payload.submissionId, action.payload.sectionId, metadata, action.payload.errors)) - ); + } else { + return observableOf(new UpdateSectionDataSuccessAction()); + } }), ); @@ -382,4 +394,30 @@ export class SubmissionObjectEffects { return mappedActions; } + /** + * Check if the section data has been enriched by the server + * + * @param sectionData + * the section metadata retrieved from the server + * @param itemData + * the item data retrieved from the server + */ + isEqual(sectionData: any, itemData: any): boolean { + const diffResult = []; + + // compare current form data state with section data retrieved from store + const diffObj = difference(sectionData, itemData); + + // iterate over differences to check whether they are actually different + Object.keys(diffObj) + .forEach((key) => { + diffObj[key].forEach((value) => { + if (value.hasOwnProperty('value')) { + diffResult.push(value); + } + }); + }); + return isEmpty(diffResult); + } + } diff --git a/src/app/submission/objects/submission-objects.reducer.ts b/src/app/submission/objects/submission-objects.reducer.ts index e0aeefd7b6..098160c737 100644 --- a/src/app/submission/objects/submission-objects.reducer.ts +++ b/src/app/submission/objects/submission-objects.reducer.ts @@ -262,7 +262,7 @@ export function submissionObjectReducer(state = initialState, action: Submission return changeSectionState(state, action as EnableSectionAction, true); } - case SubmissionObjectActionTypes.UPLOAD_SECTION_DATA: { + case SubmissionObjectActionTypes.UPDATE_SECTION_DATA: { return updateSectionData(state, action as UpdateSectionDataAction); } From 9ced3530abc3fcbb541687e45f4bca678101b4d8 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 30 Jul 2020 19:13:59 +0200 Subject: [PATCH 36/62] Added inline comments --- src/app/submission/objects/submission-objects.effects.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts index ecda75759e..0f4158a760 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -72,6 +72,8 @@ export class SubmissionObjectEffects { if (sectionDefinition.sectionType !== SectionsType.SubmissionForm) { sectionData = (isNotUndefined(action.payload.sections) && isNotUndefined(action.payload.sections[sectionId])) ? action.payload.sections[sectionId] : Object.create(null); } else { + // Normalize item metadata before to init section + // TODO to review after https://github.com/DSpace/dspace-angular/issues/818 is resolved sectionData = normalizeSectionData(action.payload.item.metadata); } const sectionErrors = null; @@ -271,6 +273,8 @@ export class SubmissionObjectEffects { map((item: Item) => item.metadata), map((metadata: any) => { if (!this.isEqual(action.payload.data, normalizeSectionData(metadata))) { + // Normalize item metadata before to update section + // TODO to review after https://github.com/DSpace/dspace-angular/issues/818 is resolved return new UpdateSectionDataAction(action.payload.submissionId, action.payload.sectionId, normalizeSectionData(metadata), action.payload.errors) } else { return new UpdateSectionDataSuccessAction(); From c9519f44fbc93be7305c13a02532e4391cf54567 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 31 Jul 2020 13:30:23 +0200 Subject: [PATCH 37/62] Retrieve entries url from vocabulary's link --- src/app/core/data/data.service.ts | 2 +- .../vocabularies/vocabulary.service.spec.ts | 5 +++++ .../vocabularies/vocabulary.service.ts | 17 ++++++++++++----- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index bb1a65cc3a..3b4e5f7d42 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -112,7 +112,7 @@ export abstract class DataService implements UpdateDa * Return an observable that emits created HREF * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ - protected buildHrefFromFindOptions(href: string, options: FindListOptions, extraArgs: string[] = [], ...linksToFollow: Array>): string { + public buildHrefFromFindOptions(href: string, options: FindListOptions, extraArgs: string[] = [], ...linksToFollow: Array>): string { let args = [...extraArgs]; if (hasValue(options.currentPage) && typeof options.currentPage === 'number') { diff --git a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts index ca7711637e..a172ae2a45 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts @@ -187,6 +187,7 @@ describe('VocabularyService', () => { const paginatedListEntries = new PaginatedList(pageInfo, arrayEntries); const childrenPaginatedList = new PaginatedList(pageInfo, childrenEntries); const vocabularyRD = createSuccessfulRemoteDataObject(vocabulary); + const vocabularyRD$ = createSuccessfulRemoteDataObject$(vocabulary); const vocabularyEntriesRD = createSuccessfulRemoteDataObject$(paginatedListEntries); const vocabularyEntryDetailParentRD = createSuccessfulRemoteDataObject(vocabularyEntryParentDetail); const vocabularyEntryChildrenRD = createSuccessfulRemoteDataObject(childrenPaginatedList); @@ -342,9 +343,11 @@ describe('VocabularyService', () => { rdbService = getMockRemoteDataBuildService(undefined, vocabularyEntriesRD); spyOn(rdbService, 'toRemoteDataObservable').and.callThrough(); service = initTestService(); + spyOn(service, 'findVocabularyById').and.returnValue(vocabularyRD$); }); describe('getVocabularyEntries', () => { + it('should configure a new VocabularyEntriesRequest', () => { const expected = new VocabularyEntriesRequest(requestService.generateRequestId(), entriesRequestURL); @@ -363,6 +366,7 @@ describe('VocabularyService', () => { }); describe('getVocabularyEntriesByValue', () => { + it('should configure a new VocabularyEntriesRequest', () => { const expected = new VocabularyEntriesRequest(requestService.generateRequestId(), entriesByValueRequestURL); @@ -382,6 +386,7 @@ describe('VocabularyService', () => { }); describe('getVocabularyEntryByValue', () => { + it('should configure a new VocabularyEntriesRequest', () => { const expected = new VocabularyEntriesRequest(requestService.generateRequestId(), entryByValueRequestURL); diff --git a/src/app/core/submission/vocabularies/vocabulary.service.ts b/src/app/core/submission/vocabularies/vocabulary.service.ts index 5200d703b9..90438e77e9 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.ts @@ -26,6 +26,7 @@ import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty. import { configureRequest, filterSuccessfulResponses, + getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteListPayload, getRequestFromRequestHref } from '../../shared/operators'; @@ -158,11 +159,13 @@ export class VocabularyService { pageInfo.currentPage ); - return this.vocabularyDataService.getFindAllHref(options, `${vocabularyOptions.name}/entries`).pipe( + return this.findVocabularyById(vocabularyOptions.name).pipe( + getFirstSucceededRemoteDataPayload(), + map((vocabulary: Vocabulary) => this.vocabularyDataService.buildHrefFromFindOptions(vocabulary._links.entries.href, options)), isNotEmptyOperator(), distinctUntilChanged(), getVocabularyEntriesFor(this.requestService, this.rdbService) - ); + ) } /** @@ -185,11 +188,13 @@ export class VocabularyService { pageInfo.currentPage ); - return this.vocabularyDataService.getFindAllHref(options, `${vocabularyOptions.name}/entries`).pipe( + return this.findVocabularyById(vocabularyOptions.name).pipe( + getFirstSucceededRemoteDataPayload(), + map((vocabulary: Vocabulary) => this.vocabularyDataService.buildHrefFromFindOptions(vocabulary._links.entries.href, options)), isNotEmptyOperator(), distinctUntilChanged(), getVocabularyEntriesFor(this.requestService, this.rdbService) - ); + ) } /** @@ -233,7 +238,9 @@ export class VocabularyService { pageInfo.currentPage ); - return this.vocabularyDataService.getFindAllHref(options, `${vocabularyOptions.name}/entries`).pipe( + return this.findVocabularyById(vocabularyOptions.name).pipe( + getFirstSucceededRemoteDataPayload(), + map((vocabulary: Vocabulary) => this.vocabularyDataService.buildHrefFromFindOptions(vocabulary._links.entries.href, options)), isNotEmptyOperator(), distinctUntilChanged(), getVocabularyEntriesFor(this.requestService, this.rdbService), From 2ed144bd1c04e29785abc3ed177af42a3d3f5dfa Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 31 Jul 2020 13:37:26 +0200 Subject: [PATCH 38/62] Fixed wrong links in mock object --- .../vocabulary-entries-response-parsing.service.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.spec.ts b/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.spec.ts index 9f28ccea17..7d4ec8be7c 100644 --- a/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.spec.ts +++ b/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.spec.ts @@ -60,16 +60,16 @@ describe('VocabularyEntriesResponseParsingService', () => { }, _links: { first: { - href: 'https://rest.api/discover/browses/author/entries?page=0&size=5' + href: 'https://rest.api/discover/browses/author/entries/first?page=0&size=5' }, self: { href: 'https://rest.api/discover/browses/author/entries' }, next: { - href: 'https://rest.api/discover/browses/author/entries?page=1&size=5' + href: 'https://rest.api/discover/browses/author/entries/next?page=1&size=5' }, last: { - href: 'https://rest.api/discover/browses/author/entries?page=9&size=5' + href: 'https://rest.api/discover/browses/author/entries/last?page=9&size=5' } }, page: { From 79db49043f7573831fd391429fbfc170ca0c6f53 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 31 Jul 2020 14:16:20 +0200 Subject: [PATCH 39/62] Make metadata and scope properties as optional in the VocabularyOptions --- .../vocabularies/models/vocabulary-options.model.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/core/submission/vocabularies/models/vocabulary-options.model.ts b/src/app/core/submission/vocabularies/models/vocabulary-options.model.ts index c28d8504bd..bdbd19f85f 100644 --- a/src/app/core/submission/vocabularies/models/vocabulary-options.model.ts +++ b/src/app/core/submission/vocabularies/models/vocabulary-options.model.ts @@ -11,12 +11,12 @@ export class VocabularyOptions { /** * The metadata field name (e.g. "dc.type") for which the vocabulary is used: */ - metadata: string; + metadata?: string; /** * The uuid of the collection where the item is being submitted */ - scope: string; + scope?: string; /** * A boolean representing if value is closely related to a vocabulary entry or not @@ -24,8 +24,8 @@ export class VocabularyOptions { closed: boolean; constructor(name: string, - metadata: string, - scope: string, + metadata?: string, + scope?: string, closed: boolean = false) { this.name = name; this.metadata = metadata; From 0582bc56222863f55453b36d77a527e0f688e930 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 31 Jul 2020 15:17:51 +0200 Subject: [PATCH 40/62] Fixed issue when adding repeatable onebox field with vocabulary --- .../models/onebox/dynamic-onebox.component.ts | 4 +++- src/app/shared/form/form.component.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts index fa9fe2103a..0cc9f66ef7 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts @@ -46,7 +46,7 @@ export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent imple @ViewChild('instance', { static: false }) instance: NgbTypeahead; - pageInfo: PageInfo; + pageInfo: PageInfo = new PageInfo(); searching = false; searchFailed = false; hideSearchingWhenUnsubscribed$ = new Observable(() => () => this.changeSearchingStatus(false)); @@ -247,6 +247,7 @@ export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent imple this.getInitValueFromModel() .subscribe((formValue: FormFieldMetadataValueObject) => { this.currentValue = formValue; + this.cdr.detectChanges(); }); } else { if (isEmpty(value)) { @@ -258,6 +259,7 @@ export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent imple } this.currentValue = result; + this.cdr.detectChanges(); } } diff --git a/src/app/shared/form/form.component.ts b/src/app/shared/form/form.component.ts index dee06c29b2..286f2673de 100644 --- a/src/app/shared/form/form.component.ts +++ b/src/app/shared/form/form.component.ts @@ -11,6 +11,7 @@ import { FormService } from './form.service'; import { FormEntry, FormError } from './form.reducer'; import { DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN } from './builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; import { QUALDROP_GROUP_SUFFIX } from './builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model'; +import { DYNAMIC_FORM_CONTROL_TYPE_ONEBOX } from './builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.model'; const QUALDROP_GROUP_REGEX = new RegExp(`${QUALDROP_GROUP_SUFFIX}_\\d+$`); @@ -316,7 +317,7 @@ export class FormComponent implements OnDestroy, OnInit { // set that field to the new value const model = arrayContext.groups[arrayContext.groups.length - 1].group[0] as any; - if (model.type === DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN) { + if (model.type === DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN || model.type === DYNAMIC_FORM_CONTROL_TYPE_ONEBOX) { model.value = Object.values(value)[0]; } else if (this.formBuilderService.isQualdropGroup(model)) { const ctrl = formArrayControl.controls[formArrayControl.length - 1]; From 34da1dc0550d1fe80933c9124e515b7919282ff9 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 31 Jul 2020 15:18:41 +0200 Subject: [PATCH 41/62] Removed angular material dependency --- package.json | 1 - .../vocabulary-tree-flat-data-source.ts | 51 ++++++++ .../vocabulary-tree-flattener.ts | 111 ++++++++++++++++++ .../vocabulary-treeview.component.ts | 11 +- yarn.lock | 7 -- 5 files changed, 168 insertions(+), 13 deletions(-) create mode 100644 src/app/shared/vocabulary-treeview/vocabulary-tree-flat-data-source.ts create mode 100644 src/app/shared/vocabulary-treeview/vocabulary-tree-flattener.ts diff --git a/package.json b/package.json index 69ca11b82d..c1e5b05010 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,6 @@ "@angular/compiler": "~8.2.14", "@angular/core": "~8.2.14", "@angular/forms": "~8.2.14", - "@angular/material": "8.2.3", "@angular/platform-browser": "~8.2.14", "@angular/platform-browser-dynamic": "~8.2.14", "@angular/platform-server": "~8.2.14", diff --git a/src/app/shared/vocabulary-treeview/vocabulary-tree-flat-data-source.ts b/src/app/shared/vocabulary-treeview/vocabulary-tree-flat-data-source.ts new file mode 100644 index 0000000000..2340d477b6 --- /dev/null +++ b/src/app/shared/vocabulary-treeview/vocabulary-tree-flat-data-source.ts @@ -0,0 +1,51 @@ +import { CollectionViewer, DataSource } from '@angular/cdk/collections'; +import { FlatTreeControl } from '@angular/cdk/tree'; + +import { BehaviorSubject, merge, Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { VocabularyTreeFlattener } from './vocabulary-tree-flattener'; + +/** + * Data source for flat tree. + * The data source need to handle expansion/collapsion of the tree node and change the data feed + * to `MatTree`. + * The nested tree nodes of type `T` are flattened through `MatTreeFlattener`, and converted + * to type `F` for `MatTree` to consume. + */ +export class VocabularyTreeFlatDataSource extends DataSource { + _flattenedData = new BehaviorSubject([]); + + _expandedData = new BehaviorSubject([]); + + _data: BehaviorSubject; + get data() { return this._data.value; } + set data(value: T[]) { + this._data.next(value); + this._flattenedData.next(this._treeFlattener.flattenNodes(this.data)); + this._treeControl.dataNodes = this._flattenedData.value; + } + + constructor(private _treeControl: FlatTreeControl, + private _treeFlattener: VocabularyTreeFlattener, + initialData: T[] = []) { + super(); + this._data = new BehaviorSubject(initialData); + } + + connect(collectionViewer: CollectionViewer): Observable { + const changes = [ + collectionViewer.viewChange, + this._treeControl.expansionModel.onChange, + this._flattenedData + ]; + return merge(...changes).pipe(map(() => { + this._expandedData.next( + this._treeFlattener.expandFlattenedNodes(this._flattenedData.value, this._treeControl)); + return this._expandedData.value; + })); + } + + disconnect() { + // no op + } +} diff --git a/src/app/shared/vocabulary-treeview/vocabulary-tree-flattener.ts b/src/app/shared/vocabulary-treeview/vocabulary-tree-flattener.ts new file mode 100644 index 0000000000..419f48d222 --- /dev/null +++ b/src/app/shared/vocabulary-treeview/vocabulary-tree-flattener.ts @@ -0,0 +1,111 @@ +import { TreeControl } from '@angular/cdk/tree'; + +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; + +/** + * Tree flattener to convert a normal type of node to node with children & level information. + * Transform nested nodes of type `T` to flattened nodes of type `F`. + * + * For example, the input data of type `T` is nested, and contains its children data: + * SomeNode: { + * key: 'Fruits', + * children: [ + * NodeOne: { + * key: 'Apple', + * }, + * NodeTwo: { + * key: 'Pear', + * } + * ] + * } + * After flattener flatten the tree, the structure will become + * SomeNode: { + * key: 'Fruits', + * expandable: true, + * level: 1 + * }, + * NodeOne: { + * key: 'Apple', + * expandable: false, + * level: 2 + * }, + * NodeTwo: { + * key: 'Pear', + * expandable: false, + * level: 2 + * } + * and the output flattened type is `F` with additional information. + */ +export class VocabularyTreeFlattener { + + constructor(public transformFunction: (node: T, level: number) => F, + public getLevel: (node: F) => number, + public isExpandable: (node: F) => boolean, + public getChildren: (node: T) => + Observable | T[] | undefined | null) {} + + _flattenNode(node: T, level: number, + resultNodes: F[], parentMap: boolean[]): F[] { + const flatNode = this.transformFunction(node, level); + resultNodes.push(flatNode); + + if (this.isExpandable(flatNode)) { + const childrenNodes = this.getChildren(node); + if (childrenNodes) { + if (Array.isArray(childrenNodes)) { + this._flattenChildren(childrenNodes, level, resultNodes, parentMap); + } else { + childrenNodes.pipe(take(1)).subscribe((children) => { + this._flattenChildren(children, level, resultNodes, parentMap); + }); + } + } + } + return resultNodes; + } + + _flattenChildren(children: T[], level: number, + resultNodes: F[], parentMap: boolean[]): void { + children.forEach((child, index) => { + const childParentMap: boolean[] = parentMap.slice(); + childParentMap.push(index !== children.length - 1); + this._flattenNode(child, level + 1, resultNodes, childParentMap); + }); + } + + /** + * Flatten a list of node type T to flattened version of node F. + * Please note that type T may be nested, and the length of `structuredData` may be different + * from that of returned list `F[]`. + */ + flattenNodes(structuredData: T[]): F[] { + const resultNodes: F[] = []; + structuredData.forEach((node) => this._flattenNode(node, 0, resultNodes, [])); + return resultNodes; + } + + /** + * Expand flattened node with current expansion status. + * The returned list may have different length. + */ + expandFlattenedNodes(nodes: F[], treeControl: TreeControl): F[] { + const results: F[] = []; + const currentExpand: boolean[] = []; + currentExpand[0] = true; + + nodes.forEach((node) => { + let expand = true; + for (let i = 0; i <= this.getLevel(node); i++) { + expand = expand && currentExpand[i]; + } + if (expand) { + results.push(node); + } + if (this.isExpandable(node)) { + currentExpand[this.getLevel(node) + 1] = treeControl.isExpanded(node); + } + }); + return results; + } +} diff --git a/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts b/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts index cbd9c25a9b..9eb4bb860c 100644 --- a/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts +++ b/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.ts @@ -1,6 +1,5 @@ import { FlatTreeControl } from '@angular/cdk/tree'; import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { filter, find, startWith } from 'rxjs/operators'; @@ -17,6 +16,8 @@ import { LOAD_MORE, LOAD_MORE_ROOT, TreeviewFlatNode, TreeviewNode } from './voc 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'; +import { VocabularyTreeFlattener } from './vocabulary-tree-flattener'; +import { VocabularyTreeFlatDataSource } from './vocabulary-tree-flat-data-source'; /** * Component that show a hierarchical vocabulary in a tree view @@ -66,12 +67,12 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { /** * Tree flattener object. Able to convert a normal type of node to node with children and level information. */ - treeFlattener: MatTreeFlattener; + treeFlattener: VocabularyTreeFlattener; /** * Flat tree data source */ - dataSource: MatTreeFlatDataSource; + dataSource: VocabularyTreeFlatDataSource; /** * The content of the search box used to search for a vocabulary entry @@ -113,12 +114,12 @@ export class VocabularyTreeviewComponent implements OnDestroy, OnInit { private store: Store, private translate: TranslateService ) { - this.treeFlattener = new MatTreeFlattener(this.transformer, this.getLevel, + this.treeFlattener = new VocabularyTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren); this.treeControl = new FlatTreeControl(this.getLevel, this.isExpandable); - this.dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); + this.dataSource = new VocabularyTreeFlatDataSource(this.treeControl, this.treeFlattener); } /** diff --git a/yarn.lock b/yarn.lock index 1dd1bb6673..3a36e692d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -211,13 +211,6 @@ resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-8.2.14.tgz#e18b27a6841577ce489ad31540150da5a444ca37" integrity sha512-7EhN9JJbAJcH2xCa+rIOmekjiEuB0qwPdHuD5qn/wwMfRzMZo+Db4hHbR9KHrLH6H82PTwYKye/LLpDaZqoHOA== -"@angular/material@8.2.3": - version "8.2.3" - resolved "https://registry.yarnpkg.com/@angular/material/-/material-8.2.3.tgz#16543e4e06a3fde2651a25cfe126e88e714ae105" - integrity sha512-SOczkIaqes+r+9XF/UUiokidfFKBpHkOPIaFK857sFD0FBNPvPEpOr5oHKCG3feERRwAFqHS7Wo2ohVEWypb5A== - dependencies: - tslib "^1.7.1" - "@angular/platform-browser-dynamic@~8.2.14": version "8.2.14" resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-8.2.14.tgz#4439a79fe10ec45170e6940a28835e9ff0918950" From 9b69aaaa073e2e01567819bfa40775e3697be738 Mon Sep 17 00:00:00 2001 From: Danilo Di Nuzzo Date: Mon, 3 Aug 2020 16:52:28 +0200 Subject: [PATCH 42/62] added missing unsubscription in onebox component --- .../models/onebox/dynamic-onebox.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts index 0cc9f66ef7..b587f9429e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts @@ -123,11 +123,11 @@ export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent imple distinctUntilChanged() ); - this.group.get(this.model.id).valueChanges.pipe( + this.subs.push(this.group.get(this.model.id).valueChanges.pipe( filter((value) => this.currentValue !== value)) .subscribe((value) => { this.setCurrentValue(this.model.value); - }); + })); } /** From 4a30e5d7d5621300da3db83daee064ccf76c1a87 Mon Sep 17 00:00:00 2001 From: Danilo Di Nuzzo Date: Tue, 4 Aug 2020 09:43:36 +0200 Subject: [PATCH 43/62] fix response for test submission/vocabularies --- .../vocabulary-entries-response-parsing.service.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.spec.ts b/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.spec.ts index 7d4ec8be7c..8e3b63df74 100644 --- a/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.spec.ts +++ b/src/app/core/submission/vocabularies/vocabulary-entries-response-parsing.service.spec.ts @@ -60,16 +60,16 @@ describe('VocabularyEntriesResponseParsingService', () => { }, _links: { first: { - href: 'https://rest.api/discover/browses/author/entries/first?page=0&size=5' + href: 'https://rest.api/rest/api/submission/vocabularies/types/entries/first?page=0&size=5' }, self: { - href: 'https://rest.api/discover/browses/author/entries' + href: 'https://rest.api/rest/api/submission/vocabularies/types/entries' }, next: { - href: 'https://rest.api/discover/browses/author/entries/next?page=1&size=5' + href: 'https://rest.api/rest/api/submission/vocabularies/types/entries/next?page=1&size=5' }, last: { - href: 'https://rest.api/discover/browses/author/entries/last?page=9&size=5' + href: 'https://rest.api/rest/api/submission/vocabularies/types/entries/last?page=9&size=5' } }, page: { From 66cabcb342991c3cee542003d73eda370531c68f Mon Sep 17 00:00:00 2001 From: Danilo Di Nuzzo Date: Wed, 12 Aug 2020 14:01:29 +0200 Subject: [PATCH 44/62] refactory for metadata and collection params --- .../models/vocabulary-options.model.ts | 14 ------------- .../vocabularies/vocabulary.service.spec.ts | 20 ------------------- .../vocabularies/vocabulary.service.ts | 20 ------------------- ...c-form-control-container.component.spec.ts | 4 +--- .../list/dynamic-list.component.spec.ts | 8 ++------ .../lookup/dynamic-lookup.component.spec.ts | 16 ++++----------- .../onebox/dynamic-onebox.component.spec.ts | 4 +--- ...amic-scrollable-dropdown.component.spec.ts | 4 +--- .../models/tag/dynamic-tag.component.spec.ts | 4 +--- .../form/builder/form-builder.service.spec.ts | 4 +--- .../builder/parsers/dropdown-field-parser.ts | 2 +- .../form/builder/parsers/field-parser.ts | 4 +--- .../form/builder/parsers/list-field-parser.ts | 2 +- .../builder/parsers/lookup-field-parser.ts | 2 +- .../parsers/lookup-name-field-parser.ts | 2 +- .../builder/parsers/onebox-field-parser.ts | 2 +- .../form/builder/parsers/tag-field-parser.ts | 2 +- src/app/shared/mocks/form-models.mock.ts | 4 ++-- .../vocabulary-treeview.component.spec.ts | 4 ++-- .../vocabulary-treeview.service.spec.ts | 2 +- 20 files changed, 23 insertions(+), 101 deletions(-) diff --git a/src/app/core/submission/vocabularies/models/vocabulary-options.model.ts b/src/app/core/submission/vocabularies/models/vocabulary-options.model.ts index bdbd19f85f..fd103718e1 100644 --- a/src/app/core/submission/vocabularies/models/vocabulary-options.model.ts +++ b/src/app/core/submission/vocabularies/models/vocabulary-options.model.ts @@ -8,28 +8,14 @@ export class VocabularyOptions { */ name: string; - /** - * The metadata field name (e.g. "dc.type") for which the vocabulary is used: - */ - metadata?: string; - - /** - * The uuid of the collection where the item is being submitted - */ - scope?: string; - /** * A boolean representing if value is closely related to a vocabulary entry or not */ closed: boolean; constructor(name: string, - metadata?: string, - scope?: string, closed: boolean = false) { this.name = name; - this.metadata = metadata; - this.scope = scope; this.closed = closed; } } diff --git a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts index a172ae2a45..1119d4f6e6 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.spec.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.spec.ts @@ -175,8 +175,6 @@ describe('VocabularyService', () => { const entryByIDRequestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}/entries?entryID=${entryID}`; const vocabularyOptions: VocabularyOptions = { name: vocabularyId, - metadata: metadata, - scope: collectionUUID, closed: false } const pageInfo = new PageInfo(); @@ -316,24 +314,6 @@ describe('VocabularyService', () => { expect(result).toBeObservable(expected); }); }); - - describe('searchVocabularyByMetadataAndCollection', () => { - it('should proxy the call to vocabularyDataService.findVocabularyByHref', () => { - scheduler.schedule(() => service.searchVocabularyByMetadataAndCollection(vocabularyOptions).subscribe()); - scheduler.flush(); - - expect((service as any).vocabularyDataService.findByHref).toHaveBeenCalledWith(searchRequestURL); - }); - - it('should return a RemoteData for the search', () => { - const result = service.searchVocabularyByMetadataAndCollection(vocabularyOptions); - const expected = cold('a|', { - a: vocabularyRD - }); - expect(result).toBeObservable(expected); - }); - - }); }); describe('', () => { diff --git a/src/app/core/submission/vocabularies/vocabulary.service.ts b/src/app/core/submission/vocabularies/vocabulary.service.ts index 90438e77e9..595edfc861 100644 --- a/src/app/core/submission/vocabularies/vocabulary.service.ts +++ b/src/app/core/submission/vocabularies/vocabulary.service.ts @@ -255,26 +255,6 @@ export class VocabularyService { ); } - /** - * Return the controlled {@link Vocabulary} configured for the specified metadata and collection if any. - * - * @param vocabularyOptions The {@link VocabularyOptions} for the request - * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved - * @return {Observable>>} - * Return an observable that emits object list - */ - searchVocabularyByMetadataAndCollection(vocabularyOptions: VocabularyOptions, ...linksToFollow: Array>): Observable> { - const options: VocabularyFindOptions = new VocabularyFindOptions( - vocabularyOptions.scope, - vocabularyOptions.metadata - ); - - return this.vocabularyDataService.getSearchByHref(this.searchByMetadataAndCollectionMethod, options).pipe( - first((href: string) => hasValue(href)), - flatMap((href: string) => this.vocabularyDataService.findByHref(href)) - ) - } - /** * Returns an observable of {@link RemoteData} of a {@link VocabularyEntryDetail}, based on an href, with a list of {@link FollowLinkConfig}, * to automatically resolve {@link HALLink}s of the {@link VocabularyEntryDetail} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts index 81247dd266..0a4de25ebf 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts @@ -78,10 +78,8 @@ import { FormBuilderService } from '../form-builder.service'; describe('DsDynamicFormControlContainerComponent test suite', () => { const vocabularyOptions: VocabularyOptions = { - closed: false, - metadata: 'list', name: 'type_programme', - scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' + closed: false }; const formModel = [ new DynamicCheckboxModel({ id: 'checkbox' }), diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.spec.ts index b35ee10e8a..b041736e34 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.spec.ts @@ -36,10 +36,8 @@ export const LIST_TEST_GROUP = new FormGroup({ export const LIST_CHECKBOX_TEST_MODEL_CONFIG = { vocabularyOptions: { - closed: false, - metadata: 'listCheckbox', name: 'type_programme', - scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' + closed: false } as VocabularyOptions, disabled: false, id: 'listCheckbox', @@ -53,10 +51,8 @@ export const LIST_CHECKBOX_TEST_MODEL_CONFIG = { export const LIST_RADIO_TEST_MODEL_CONFIG = { vocabularyOptions: { - closed: false, - metadata: 'listRadio', name: 'type_programme', - scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' + closed: false } as VocabularyOptions, disabled: false, id: 'listRadio', diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts index 30c40aebcc..f577e643cd 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts @@ -25,10 +25,8 @@ import { ObjNgFor } from '../../../../../utils/object-ngfor.pipe'; let LOOKUP_TEST_MODEL_CONFIG: DynamicLookupModelConfig = { vocabularyOptions: { - closed: false, - metadata: 'lookup', name: 'RPAuthority', - scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' + closed: false } as VocabularyOptions, disabled: false, errorMessages: { required: 'Required field.' }, @@ -49,10 +47,8 @@ let LOOKUP_TEST_MODEL_CONFIG: DynamicLookupModelConfig = { let LOOKUP_NAME_TEST_MODEL_CONFIG = { vocabularyOptions: { - closed: false, - metadata: 'lookup-name', name: 'RPAuthority', - scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' + closed: false } as VocabularyOptions, disabled: false, errorMessages: { required: 'Required field.' }, @@ -80,10 +76,8 @@ describe('Dynamic Lookup component', () => { function init() { LOOKUP_TEST_MODEL_CONFIG = { vocabularyOptions: { - closed: false, - metadata: 'lookup', name: 'RPAuthority', - scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' + closed: false } as VocabularyOptions, disabled: false, errorMessages: { required: 'Required field.' }, @@ -104,10 +98,8 @@ describe('Dynamic Lookup component', () => { LOOKUP_NAME_TEST_MODEL_CONFIG = { vocabularyOptions: { - closed: false, - metadata: 'lookup-name', name: 'RPAuthority', - scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' + closed: false } as VocabularyOptions, disabled: false, errorMessages: { required: 'Required field.' }, diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts index e94d3a208c..7a18bcc6e4 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.spec.ts @@ -50,9 +50,7 @@ function init() { ONEBOX_TEST_MODEL_CONFIG = { vocabularyOptions: { closed: false, - metadata: 'onebox', - name: 'vocabulary', - scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' + name: 'vocabulary' } as VocabularyOptions, disabled: false, id: 'onebox', diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts index 2318f3b0ae..9dbeb41ae8 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts @@ -24,9 +24,7 @@ export const SD_TEST_GROUP = new FormGroup({ export const SD_TEST_MODEL_CONFIG = { vocabularyOptions: { closed: false, - metadata: 'dropdown', - name: 'common_iso_languages', - scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' + name: 'common_iso_languages' } as VocabularyOptions, disabled: false, errorMessages: { required: 'Required field.' }, diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.spec.ts index d0f7f4ec3b..d40ea72764 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.spec.ts @@ -46,9 +46,7 @@ function init() { TAG_TEST_MODEL_CONFIG = { vocabularyOptions: { closed: false, - metadata: 'tag', - name: 'common_iso_languages', - scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' + name: 'common_iso_languages' } as VocabularyOptions, disabled: false, id: 'tag', diff --git a/src/app/shared/form/builder/form-builder.service.spec.ts b/src/app/shared/form/builder/form-builder.service.spec.ts index 447ceb194d..46d058aae4 100644 --- a/src/app/shared/form/builder/form-builder.service.spec.ts +++ b/src/app/shared/form/builder/form-builder.service.spec.ts @@ -79,10 +79,8 @@ describe('FormBuilderService test suite', () => { }); const vocabularyOptions: VocabularyOptions = { - closed: false, - metadata: 'list', name: 'type_programme', - scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' + closed: false }; testModel = [ diff --git a/src/app/shared/form/builder/parsers/dropdown-field-parser.ts b/src/app/shared/form/builder/parsers/dropdown-field-parser.ts index 7fb5fb206c..a73e270ed4 100644 --- a/src/app/shared/form/builder/parsers/dropdown-field-parser.ts +++ b/src/app/shared/form/builder/parsers/dropdown-field-parser.ts @@ -32,7 +32,7 @@ export class DropdownFieldParser extends FieldParser { let layout: DynamicFormControlLayout; if (isNotEmpty(this.configData.selectableMetadata[0].controlledVocabulary)) { - this.setVocabularyOptions(dropdownModelConfig, this.parserOptions.collectionUUID); + this.setVocabularyOptions(dropdownModelConfig); if (isNotEmpty(fieldValue)) { dropdownModelConfig.value = fieldValue; } diff --git a/src/app/shared/form/builder/parsers/field-parser.ts b/src/app/shared/form/builder/parsers/field-parser.ts index 6c6b8fddaf..c902e455d5 100644 --- a/src/app/shared/form/builder/parsers/field-parser.ts +++ b/src/app/shared/form/builder/parsers/field-parser.ts @@ -109,12 +109,10 @@ export abstract class FieldParser { } } - public setVocabularyOptions(controlModel, scope) { + public setVocabularyOptions(controlModel) { if (isNotEmpty(this.configData.selectableMetadata) && isNotEmpty(this.configData.selectableMetadata[0].controlledVocabulary)) { controlModel.vocabularyOptions = new VocabularyOptions( this.configData.selectableMetadata[0].controlledVocabulary, - this.configData.selectableMetadata[0].metadata, - scope, this.configData.selectableMetadata[0].closed ) } diff --git a/src/app/shared/form/builder/parsers/list-field-parser.ts b/src/app/shared/form/builder/parsers/list-field-parser.ts index e9c9f4e5be..48211b0aff 100644 --- a/src/app/shared/form/builder/parsers/list-field-parser.ts +++ b/src/app/shared/form/builder/parsers/list-field-parser.ts @@ -24,7 +24,7 @@ export class ListFieldParser extends FieldParser { } }); } - this.setVocabularyOptions(listModelConfig, this.parserOptions.collectionUUID); + this.setVocabularyOptions(listModelConfig); } let listModel; diff --git a/src/app/shared/form/builder/parsers/lookup-field-parser.ts b/src/app/shared/form/builder/parsers/lookup-field-parser.ts index 6e28194dad..2c35d2ceb8 100644 --- a/src/app/shared/form/builder/parsers/lookup-field-parser.ts +++ b/src/app/shared/form/builder/parsers/lookup-field-parser.ts @@ -8,7 +8,7 @@ export class LookupFieldParser extends FieldParser { if (this.configData.selectableMetadata[0].controlledVocabulary) { const lookupModelConfig: DynamicLookupModelConfig = this.initModel(null, label); - this.setVocabularyOptions(lookupModelConfig, this.parserOptions.collectionUUID); + this.setVocabularyOptions(lookupModelConfig); this.setValues(lookupModelConfig, fieldValue, true); diff --git a/src/app/shared/form/builder/parsers/lookup-name-field-parser.ts b/src/app/shared/form/builder/parsers/lookup-name-field-parser.ts index 26806742c8..c791e07e74 100644 --- a/src/app/shared/form/builder/parsers/lookup-name-field-parser.ts +++ b/src/app/shared/form/builder/parsers/lookup-name-field-parser.ts @@ -11,7 +11,7 @@ export class LookupNameFieldParser extends FieldParser { if (this.configData.selectableMetadata[0].controlledVocabulary) { const lookupModelConfig: DynamicLookupNameModelConfig = this.initModel(null, label); - this.setVocabularyOptions(lookupModelConfig, this.parserOptions.collectionUUID); + this.setVocabularyOptions(lookupModelConfig); this.setValues(lookupModelConfig, fieldValue, true); diff --git a/src/app/shared/form/builder/parsers/onebox-field-parser.ts b/src/app/shared/form/builder/parsers/onebox-field-parser.ts index 3eb5764a37..b9ac531c68 100644 --- a/src/app/shared/form/builder/parsers/onebox-field-parser.ts +++ b/src/app/shared/form/builder/parsers/onebox-field-parser.ts @@ -77,7 +77,7 @@ export class OneboxFieldParser extends FieldParser { return new DynamicQualdropModel(inputSelectGroup, clsGroup); } else if (this.configData.selectableMetadata[0].controlledVocabulary) { const oneboxModelConfig: DsDynamicOneboxModelConfig = this.initModel(null, label); - this.setVocabularyOptions(oneboxModelConfig, this.parserOptions.collectionUUID); + this.setVocabularyOptions(oneboxModelConfig); this.setValues(oneboxModelConfig, fieldValue, true); return new DynamicOneboxModel(oneboxModelConfig); diff --git a/src/app/shared/form/builder/parsers/tag-field-parser.ts b/src/app/shared/form/builder/parsers/tag-field-parser.ts index 08685e0e35..dcc2c5575f 100644 --- a/src/app/shared/form/builder/parsers/tag-field-parser.ts +++ b/src/app/shared/form/builder/parsers/tag-field-parser.ts @@ -8,7 +8,7 @@ export class TagFieldParser extends FieldParser { const tagModelConfig: DynamicTagModelConfig = this.initModel(null, label); if (this.configData.selectableMetadata[0].controlledVocabulary && this.configData.selectableMetadata[0].controlledVocabulary.length > 0) { - this.setVocabularyOptions(tagModelConfig, this.parserOptions.collectionUUID); + this.setVocabularyOptions(tagModelConfig); } this.setValues(tagModelConfig, fieldValue, null, true); diff --git a/src/app/shared/mocks/form-models.mock.ts b/src/app/shared/mocks/form-models.mock.ts index c5f78b9571..739de944f0 100644 --- a/src/app/shared/mocks/form-models.mock.ts +++ b/src/app/shared/mocks/form-models.mock.ts @@ -152,7 +152,7 @@ const relationGroupConfig = { export const MockRelationModel: DynamicRelationGroupModel = new DynamicRelationGroupModel(relationGroupConfig); export const inputWithLanguageAndAuthorityConfig = { - vocabularyOptions: new VocabularyOptions('testAuthority', 'testWithAuthority', 'scope'), + vocabularyOptions: new VocabularyOptions('testAuthority', false), languageCodes: [ { display: 'English', @@ -207,7 +207,7 @@ export const inputWithLanguageConfig = { export const mockInputWithLanguageModel = new DsDynamicInputModel(inputWithLanguageConfig); export const inputWithLanguageAndAuthorityArrayConfig = { - vocabularyOptions: new VocabularyOptions('testAuthority', 'testWithAuthority', 'scope'), + vocabularyOptions: new VocabularyOptions('testAuthority', false), languageCodes: [ { display: 'English', 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 16ee2a55dd..6166dbc082 100644 --- a/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.spec.ts +++ b/src/app/shared/vocabulary-treeview/vocabulary-treeview.component.spec.ts @@ -31,7 +31,7 @@ describe('VocabularyTreeviewComponent test suite', () => { const emptyNodeMap = new Map(); const storedNodeMap = new Map().set('test', new TreeviewFlatNode(item2)); const nodeMap = new Map().set('test', new TreeviewFlatNode(item)); - const vocabularyOptions = new VocabularyOptions('vocabularyTest', 'metadata.test', '123456'); + const vocabularyOptions = new VocabularyOptions('vocabularyTest', false); const modalStub = jasmine.createSpyObj('modalStub', ['close']); const vocabularyTreeviewServiceStub = jasmine.createSpyObj('VocabularyTreeviewService', { initialize: jasmine.createSpy('initialize'), @@ -224,7 +224,7 @@ describe('VocabularyTreeviewComponent test suite', () => { }) class TestComponent { - vocabularyOptions: VocabularyOptions = new VocabularyOptions('vocabularyTest', 'metadata.test', '123456'); + vocabularyOptions: VocabularyOptions = new VocabularyOptions('vocabularyTest', false); preloadLevel = 2; } 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 f45478f82b..060c7f4d9b 100644 --- a/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.spec.ts +++ b/src/app/shared/vocabulary-treeview/vocabulary-treeview.service.spec.ts @@ -156,7 +156,7 @@ describe('VocabularyTreeviewService test suite', () => { searchNodeMap = new Map([ [item.id, searchItemNode], ]); - vocabularyOptions = new VocabularyOptions('vocabularyTest', 'metadata.test', '123456'); + vocabularyOptions = new VocabularyOptions('vocabularyTest', false); } beforeEach(async(() => { From 3178e5578c7703383fdff8d65008de0c845bf461 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 26 Aug 2020 14:45:53 +0200 Subject: [PATCH 45/62] Fixed issue after merge --- src/app/shared/shared.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 83bb947569..d04d31faa5 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -480,7 +480,7 @@ const ENTRY_COMPONENTS = [ FileDownloadLinkComponent, CurationFormComponent, ExportMetadataSelectorComponent, - ConfirmationModalComponent + ConfirmationModalComponent, VocabularyTreeviewComponent ]; From 87bed6d2882ded2aee9f6c55f8e2087ee0b0d6cd Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 26 Aug 2020 15:24:01 +0200 Subject: [PATCH 46/62] Fixed check for vocabulary options on DsDynamicListComponent --- .../models/list/dynamic-list.component.ts | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts index d7c492ccde..ad305cb17a 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts @@ -55,20 +55,35 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen super(layoutService, validationService); } + /** + * Initialize the component, setting up the field options + */ ngOnInit() { - if (this.hasAuthorityOptions()) { - this.setOptionsFromAuthority(); + if (this.model.vocabularyOptions && hasValue(this.model.vocabularyOptions.name)) { + this.setOptionsFromVocabulary(); } } + /** + * Emits a blur event containing a given value. + * @param event The value to emit. + */ onBlur(event: Event) { this.blur.emit(event); } + /** + * Emits a focus event containing a given value. + * @param event The value to emit. + */ onFocus(event: Event) { this.focus.emit(event); } + /** + * Updates model value with the current value + * @param event The change event. + */ onChange(event: Event) { const target = event.target as any; if (this.model.repeatable) { @@ -89,7 +104,10 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen this.change.emit(event); } - protected setOptionsFromAuthority() { + /** + * Setting up the field options from vocabulary + */ + protected setOptionsFromVocabulary() { if (this.model.vocabularyOptions.name && this.model.vocabularyOptions.name.length > 0) { const listGroup = this.group.controls[this.model.id] as FormGroup; const pageInfo: PageInfo = new PageInfo({ @@ -138,9 +156,4 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen } } - protected hasAuthorityOptions() { - return (hasValue(this.model.vocabularyOptions.scope) - && hasValue(this.model.vocabularyOptions.name) - && hasValue(this.model.vocabularyOptions.metadata)); - } } From 4080cce8fd32eea26c6d52ee11016b97ef9b4b79 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 26 Aug 2020 15:57:34 +0200 Subject: [PATCH 47/62] Fixed check for Hierarchical Vocabulary --- .../models/onebox/dynamic-onebox.component.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts index b587f9429e..f1e81fb57e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts @@ -56,6 +56,7 @@ export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent imple preloadLevel: number; private vocabulary$: Observable; + private isHierarchicalVocabulary$: Observable; private subs: Subscription[] = []; constructor(protected vocabularyService: VocabularyService, @@ -123,6 +124,10 @@ export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent imple distinctUntilChanged() ); + this.isHierarchicalVocabulary$ = this.vocabulary$.pipe( + map((result: Vocabulary) => result.hierarchical) + ); + this.subs.push(this.group.get(this.model.id).valueChanges.pipe( filter((value) => this.currentValue !== value)) .subscribe((value) => { @@ -143,9 +148,7 @@ export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent imple * Checks if configured vocabulary is Hierarchical or not */ isHierarchicalVocabulary(): Observable { - return this.vocabulary$.pipe( - map((result: Vocabulary) => result.hierarchical) - ); + return this.isHierarchicalVocabulary$; } /** From 92207cf66d18148aab06429d7288bc4d8752a79b Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 27 Aug 2020 09:30:38 +0200 Subject: [PATCH 48/62] Added missing TypeDocs --- .../core/data/entries-response-parsing.service.ts | 12 ++++++++++++ .../models/onebox/dynamic-onebox.component.ts | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/src/app/core/data/entries-response-parsing.service.ts b/src/app/core/data/entries-response-parsing.service.ts index ed13c3f228..09ae8ae1c5 100644 --- a/src/app/core/data/entries-response-parsing.service.ts +++ b/src/app/core/data/entries-response-parsing.service.ts @@ -9,6 +9,9 @@ import { RestRequest } from './request.models'; import { CacheableObject } from '../cache/object-cache.reducer'; import { GenericConstructor } from '../shared/generic-constructor'; +/** + * An abstract class to extend, responsible for parsing data for an entries response + */ export abstract class EntriesResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { protected toCache = false; @@ -19,8 +22,17 @@ export abstract class EntriesResponseParsingService e super(); } + /** + * Abstract method to implement that must return the dspace serializer Constructor to use during parse + */ abstract getSerializerModel(): GenericConstructor; + /** + * Parse response + * + * @param request + * @param data + */ parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { if (isNotEmpty(data.payload)) { let entries = []; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts index f1e81fb57e..11002204f5 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts @@ -30,6 +30,10 @@ import { Vocabulary } from '../../../../../../core/submission/vocabularies/model import { VocabularyTreeviewComponent } from '../../../../../vocabulary-treeview/vocabulary-treeview.component'; import { VocabularyEntryDetail } from '../../../../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; +/** + * Component representing a onebox input field. + * If field has a Hierarchical Vocabulary configured, it's rendered with vocabulary tree + */ @Component({ selector: 'ds-dynamic-onebox', styleUrls: ['./dynamic-onebox.component.scss'], From f167d5a62911596df404d20c5a1d9088b0c8ab2e Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Tue, 1 Sep 2020 14:25:50 +0200 Subject: [PATCH 49/62] fix issue where combining entities and authority control in the same field wouldn't work --- .../models/lookup/dynamic-lookup.component.ts | 1 + .../objects/submission-objects.effects.ts | 59 +++++-------------- ...ubmission-section-cc-licenses.component.ts | 8 ++- .../section-container.component.html | 1 + .../sections/form/section-form.component.ts | 4 +- .../submission/sections/sections.directive.ts | 9 ++- .../sections/sections.service.spec.ts | 17 ++++-- .../submission/sections/sections.service.ts | 35 +++++++++-- 8 files changed, 76 insertions(+), 58 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts index 3d946caa5e..90319ee64d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts @@ -81,6 +81,7 @@ export class DsDynamicLookupComponent extends DsDynamicVocabularyComponent imple */ public hasAuthorityValue() { return hasValue(this.model.value) + && typeof this.model.value === 'object' && this.model.value.hasAuthority(); } diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts index 0f4158a760..2a69a61a8c 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -2,10 +2,19 @@ import { Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; -import { union } from 'lodash'; +import { isEqual, union } from 'lodash'; import { from as observableFrom, Observable, of as observableOf } from 'rxjs'; -import { catchError, filter, map, mergeMap, switchMap, take, tap, withLatestFrom } from 'rxjs/operators'; +import { + catchError, + filter, + map, + mergeMap, + switchMap, + take, + tap, + withLatestFrom +} from 'rxjs/operators'; import { SubmissionObject } from '../../core/submission/models/submission-object.model'; import { WorkflowItem } from '../../core/submission/models/workflowitem.model'; import { WorkspaceitemSectionUploadObject } from '../../core/submission/models/workspaceitem-section-upload.model'; @@ -49,8 +58,6 @@ import { RemoteData } from '../../core/data/remote-data'; import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; import { SubmissionObjectDataService } from '../../core/submission/submission-object-data.service'; import { followLink } from '../../shared/utils/follow-link-config.model'; -import { normalizeSectionData } from '../../core/submission/submission-response-parsing.service'; -import { difference } from '../../shared/object.util'; @Injectable() export class SubmissionObjectEffects { @@ -72,9 +79,7 @@ export class SubmissionObjectEffects { if (sectionDefinition.sectionType !== SectionsType.SubmissionForm) { sectionData = (isNotUndefined(action.payload.sections) && isNotUndefined(action.payload.sections[sectionId])) ? action.payload.sections[sectionId] : Object.create(null); } else { - // Normalize item metadata before to init section - // TODO to review after https://github.com/DSpace/dspace-angular/issues/818 is resolved - sectionData = normalizeSectionData(action.payload.item.metadata); + sectionData = action.payload.item.metadata; } const sectionErrors = null; mappedActions.push( @@ -253,7 +258,7 @@ export class SubmissionObjectEffects { @Effect() addAllMetadataToSectionData = this.actions$.pipe( ofType(SubmissionObjectActionTypes.UPDATE_SECTION_DATA), switchMap((action: UpdateSectionDataAction) => { - return this.sectionService.getSectionState(action.payload.submissionId, action.payload.sectionId) + return this.sectionService.getSectionState(action.payload.submissionId, action.payload.sectionId, SectionsType.Upload) .pipe(map((section: SubmissionSectionObject) => [action, section]), take(1)); }), filter(([action, section]: [UpdateSectionDataAction, SubmissionSectionObject]) => section.sectionType === SectionsType.SubmissionForm), @@ -271,15 +276,8 @@ export class SubmissionObjectEffects { return item$.pipe( map((item: Item) => item.metadata), - map((metadata: any) => { - if (!this.isEqual(action.payload.data, normalizeSectionData(metadata))) { - // Normalize item metadata before to update section - // TODO to review after https://github.com/DSpace/dspace-angular/issues/818 is resolved - return new UpdateSectionDataAction(action.payload.submissionId, action.payload.sectionId, normalizeSectionData(metadata), action.payload.errors) - } else { - return new UpdateSectionDataSuccessAction(); - } - }) + filter((metadata) => !isEqual(action.payload.data, metadata)), + map((metadata: any) => new UpdateSectionDataAction(action.payload.submissionId, action.payload.sectionId, metadata, action.payload.errors)) ); } else { return observableOf(new UpdateSectionDataSuccessAction()); @@ -397,31 +395,4 @@ export class SubmissionObjectEffects { } return mappedActions; } - - /** - * Check if the section data has been enriched by the server - * - * @param sectionData - * the section metadata retrieved from the server - * @param itemData - * the item data retrieved from the server - */ - isEqual(sectionData: any, itemData: any): boolean { - const diffResult = []; - - // compare current form data state with section data retrieved from store - const diffObj = difference(sectionData, itemData); - - // iterate over differences to check whether they are actually different - Object.keys(diffObj) - .forEach((key) => { - diffObj[key].forEach((value) => { - if (value.hasOwnProperty('value')) { - diffResult.push(value); - } - }); - }); - return isEmpty(diffResult); - } - } diff --git a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts index d455bd5e22..9e0cfa9efa 100644 --- a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts +++ b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts @@ -1,6 +1,10 @@ import { Component, Inject } from '@angular/core'; import { Observable, of as observableOf, Subscription } from 'rxjs'; -import { Field, Option, SubmissionCcLicence } from '../../../core/submission/models/submission-cc-license.model'; +import { + Field, + Option, + SubmissionCcLicence +} from '../../../core/submission/models/submission-cc-license.model'; import { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators'; import { distinctUntilChanged, filter, map, take } from 'rxjs/operators'; import { SubmissionCcLicenseDataService } from '../../../core/submission/submission-cc-license-data.service'; @@ -228,7 +232,7 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent onSectionInit(): void { this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id); this.subscriptions.push( - this.sectionService.getSectionState(this.submissionId, this.sectionData.id).pipe( + this.sectionService.getSectionState(this.submissionId, this.sectionData.id, SectionsType.CcLicense).pipe( filter((sectionState) => { return isNotEmpty(sectionState) && (isNotEmpty(sectionState.data) || isNotEmpty(sectionState.errors)) }), diff --git a/src/app/submission/sections/container/section-container.component.html b/src/app/submission/sections/container/section-container.component.html index fb29f606e6..8262d51f6f 100644 --- a/src/app/submission/sections/container/section-container.component.html +++ b/src/app/submission/sections/container/section-container.component.html @@ -3,6 +3,7 @@ [ngClass]="{ 'section-focus' : sectionRef.isSectionActive() }" [mandatory]="sectionData.mandatory" [submissionId]="submissionId" + [sectionType]="sectionData.sectionType" [sectionId]="sectionData.id"> this.formConfig = config), flatMap(() => observableCombineLatest( - this.sectionService.getSectionData(this.submissionId, this.sectionData.id), + this.sectionService.getSectionData(this.submissionId, this.sectionData.id, this.sectionData.sectionType), this.submissionObjectService.getHrefByID(this.submissionId).pipe(take(1)).pipe( switchMap((href: string) => { this.objectCache.remove(href); @@ -318,7 +318,7 @@ export class SubmissionSectionformComponent extends SectionModelComponent { /** * Subscribe to section state */ - this.sectionService.getSectionState(this.submissionId, this.sectionData.id).pipe( + this.sectionService.getSectionState(this.submissionId, this.sectionData.id, this.sectionData.sectionType).pipe( filter((sectionState: SubmissionSectionObject) => { return isNotEmpty(sectionState) && (isNotEmpty(sectionState.data) || isNotEmpty(sectionState.errors)) }), diff --git a/src/app/submission/sections/sections.directive.ts b/src/app/submission/sections/sections.directive.ts index 0efb7225aa..1b0a6bfbc6 100644 --- a/src/app/submission/sections/sections.directive.ts +++ b/src/app/submission/sections/sections.directive.ts @@ -9,6 +9,7 @@ import { hasValue, isNotEmpty, isNotNull } from '../../shared/empty.util'; import { SubmissionSectionError, SubmissionSectionObject } from '../objects/submission-objects.reducer'; import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths'; import { SubmissionService } from '../submission.service'; +import { SectionsType } from './sections-type'; /** * Directive for handling generic section functionality @@ -31,6 +32,12 @@ export class SectionsDirective implements OnDestroy, OnInit { */ @Input() sectionId: string; + /** + * The section type + * @type {SectionsType} + */ + @Input() sectionType: SectionsType; + /** * The submission id * @type {string} @@ -104,7 +111,7 @@ export class SectionsDirective implements OnDestroy, OnInit { })); this.subs.push( - this.sectionService.getSectionState(this.submissionId, this.sectionId).pipe( + this.sectionService.getSectionState(this.submissionId, this.sectionId, this.sectionType).pipe( map((state: SubmissionSectionObject) => state.errors)) .subscribe((errors: SubmissionSectionError[]) => { if (isNotEmpty(errors)) { diff --git a/src/app/submission/sections/sections.service.spec.ts b/src/app/submission/sections/sections.service.spec.ts index e5cb3ddc09..5c7bff13ce 100644 --- a/src/app/submission/sections/sections.service.spec.ts +++ b/src/app/submission/sections/sections.service.spec.ts @@ -14,7 +14,11 @@ import { NotificationsServiceStub } from '../../shared/testing/notifications-ser import { SubmissionServiceStub } from '../../shared/testing/submission-service.stub'; import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; import { SectionsService } from './sections.service'; -import { mockSectionsData, mockSectionsErrors, mockSubmissionState } from '../../shared/mocks/submission.mock'; +import { + mockSectionsData, + mockSectionsErrors, + mockSubmissionState +} from '../../shared/mocks/submission.mock'; import { DisableSectionAction, EnableSectionAction, @@ -23,12 +27,17 @@ import { SectionStatusChangeAction, UpdateSectionDataAction } from '../objects/submission-objects.actions'; -import { FormAddError, FormClearErrorsAction, FormRemoveErrorAction } from '../../shared/form/form.actions'; +import { + FormAddError, + FormClearErrorsAction, + FormRemoveErrorAction +} from '../../shared/form/form.actions'; import parseSectionErrors from '../utils/parseSectionErrors'; import { SubmissionScopeType } from '../../core/submission/submission-scope-type'; import { SubmissionSectionError } from '../objects/submission-objects.reducer'; import { getMockScrollToService } from '../../shared/mocks/scroll-to-service.mock'; import { storeModuleConfig } from '../../app.reducer'; +import { SectionsType } from './sections-type'; describe('SectionsService test suite', () => { let notificationsServiceStub: NotificationsServiceStub; @@ -151,7 +160,7 @@ describe('SectionsService test suite', () => { b: sectionData[sectionId] }); - expect(service.getSectionData(submissionId, sectionId)).toBeObservable(expected); + expect(service.getSectionData(submissionId, sectionId, SectionsType.SubmissionForm)).toBeObservable(expected); }); }); @@ -175,7 +184,7 @@ describe('SectionsService test suite', () => { b: sectionState }); - expect(service.getSectionState(submissionId, sectionId)).toBeObservable(expected); + expect(service.getSectionState(submissionId, sectionId, SectionsType.SubmissionForm)).toBeObservable(expected); }); }); diff --git a/src/app/submission/sections/sections.service.ts b/src/app/submission/sections/sections.service.ts index 52ae941893..9d7fee3bb3 100644 --- a/src/app/submission/sections/sections.service.ts +++ b/src/app/submission/sections/sections.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { combineLatest, Observable } from 'rxjs'; -import { distinctUntilChanged, filter, map, take } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, take, tap } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import { ScrollToConfigOptions, ScrollToService } from '@nicky-lenaers/ngx-scroll-to'; @@ -25,6 +25,8 @@ import { FormAddError, FormClearErrorsAction, FormRemoveErrorAction } from '../. import { NotificationsService } from '../../shared/notifications/notifications.service'; import { SubmissionService } from '../submission.service'; import { WorkspaceitemSectionDataType } from '../../core/submission/models/workspaceitem-sections.model'; +import { SectionsType } from './sections-type'; +import { normalizeSectionData } from '../../core/submission/submission-response-parsing.service'; /** * A service that provides methods used in submission process. @@ -129,12 +131,23 @@ export class SectionsService { * The submission id * @param sectionId * The section id + * @param sectionType + * The type of section to retrieve * @return Observable * observable of [WorkspaceitemSectionDataType] */ - public getSectionData(submissionId: string, sectionId: string): Observable { + public getSectionData(submissionId: string, sectionId: string, sectionType: SectionsType): Observable { return this.store.select(submissionSectionDataFromIdSelector(submissionId, sectionId)).pipe( - distinctUntilChanged()); + map((sectionData: WorkspaceitemSectionDataType) => { + if (sectionType === SectionsType.SubmissionForm) { + return normalizeSectionData(sectionData) + } + else { + return sectionData; + } + }), + distinctUntilChanged(), + ); } /** @@ -159,14 +172,26 @@ export class SectionsService { * The submission id * @param sectionId * The section id + * @param sectionType + * The type of section to retrieve * @return Observable * observable of [SubmissionSectionObject] */ - public getSectionState(submissionId: string, sectionId: string): Observable { + public getSectionState(submissionId: string, sectionId: string, sectionType: SectionsType): Observable { return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe( filter((sectionObj: SubmissionSectionObject) => hasValue(sectionObj)), map((sectionObj: SubmissionSectionObject) => sectionObj), - distinctUntilChanged(), + map((sectionState: SubmissionSectionObject) => { + if (hasValue(sectionState.data) && sectionType === SectionsType.SubmissionForm) { + return Object.assign({}, sectionState, { + data: normalizeSectionData(sectionState.data) + }) + } + else { + return sectionState; + } + }), + distinctUntilChanged() ); } From 94119668dda8118e7fdebb79c14c2d37a96fc66a Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 3 Sep 2020 09:57:24 +0200 Subject: [PATCH 50/62] Fixed merge --- .../submission/vocabularies/models/vocabulary-entry.model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/submission/vocabularies/models/vocabulary-entry.model.ts b/src/app/core/submission/vocabularies/models/vocabulary-entry.model.ts index 5ef61eba2e..ca26c1b41e 100644 --- a/src/app/core/submission/vocabularies/models/vocabulary-entry.model.ts +++ b/src/app/core/submission/vocabularies/models/vocabulary-entry.model.ts @@ -4,7 +4,7 @@ import { HALLink } from '../../../shared/hal-link.model'; import { VOCABULARY_ENTRY } from './vocabularies.resource-type'; import { typedObject } from '../../../cache/builders/build-decorators'; import { excludeFromEquals } from '../../../utilities/equals.decorators'; -import { PLACEHOLDER_PARENT_METADATA } from '../../../../shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; +import { PLACEHOLDER_PARENT_METADATA } from '../../../../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-constants'; import { OtherInformation } from '../../../../shared/form/builder/models/form-field-metadata-value.model'; import { isNotEmpty } from '../../../../shared/empty.util'; import { ListableObject } from '../../../../shared/object-collection/shared/listable-object.model'; From a504b7cbd340243406f15e88f7325949e724b8f5 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 3 Sep 2020 11:18:32 +0200 Subject: [PATCH 51/62] Fixed issue with form field losing confidence value --- .../models/dynamic-vocabulary.component.ts | 32 ++++++++++++------- .../models/form-field-metadata-value.model.ts | 2 +- 2 files changed, 21 insertions(+), 13 deletions(-) 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 d3263f1584..3c95aa0585 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 @@ -60,23 +60,31 @@ export abstract class DsDynamicVocabularyComponent extends DynamicFormControlCom initValue$ = initEntry$.pipe(map((initEntry: VocabularyEntry) => { if (isNotEmpty(initEntry)) { // 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 || null - }); + return new FormFieldMetadataValueObject( + initEntry.value, + null, + initEntry.authority, + initEntry.display, + null, + null, + initEntry.otherInformation || null + ); } else { return this.model.value as any; } })); } else if (isNotEmpty(this.model.value) && (this.model.value instanceof VocabularyEntry)) { - initValue$ = observableOf(Object.assign(new FormFieldMetadataValueObject(), this.model.value, { - value: this.model.value.value, - authority: this.model.value.authority, - display: this.model.value.display, - otherInformation: this.model.value.otherInformation || null - })); + initValue$ = observableOf( + new FormFieldMetadataValueObject( + this.model.value.value, + null, + this.model.value.authority, + this.model.value.display, + null, + null, + this.model.value.otherInformation || null + ) + ); } else { initValue$ = observableOf(new FormFieldMetadataValueObject(this.model.value)); } 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 7f43349c6c..a80dbdc7de 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 @@ -35,7 +35,7 @@ export class FormFieldMetadataValueObject implements MetadataValueInterface { this.display = display || value; this.confidence = confidence; - if (authority != null && isEmpty(confidence)) { + if (authority != null && (isEmpty(confidence) || confidence === -1)) { this.confidence = ConfidenceType.CF_ACCEPTED; } else if (isNotEmpty(confidence)) { this.confidence = confidence; From b15113ad969b3adbbadd65b34f7c0718d757a041 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 3 Sep 2020 11:36:28 +0200 Subject: [PATCH 52/62] Fixed failed test --- .../ds-dynamic-form-ui/models/dynamic-vocabulary.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 3c95aa0585..83bacf6ea8 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 @@ -65,7 +65,7 @@ export abstract class DsDynamicVocabularyComponent extends DynamicFormControlCom null, initEntry.authority, initEntry.display, - null, + (this.model.value as any).place, null, initEntry.otherInformation || null ); @@ -80,7 +80,7 @@ export abstract class DsDynamicVocabularyComponent extends DynamicFormControlCom null, this.model.value.authority, this.model.value.display, - null, + 0, null, this.model.value.otherInformation || null ) From b8138ee3b97724e1206c8585ed674171f7e785fd Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 3 Sep 2020 12:27:55 +0200 Subject: [PATCH 53/62] Fixed lint error --- .../submission/sections/sections.service.ts | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/app/submission/sections/sections.service.ts b/src/app/submission/sections/sections.service.ts index 9d7fee3bb3..5aa3c1d3ea 100644 --- a/src/app/submission/sections/sections.service.ts +++ b/src/app/submission/sections/sections.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { combineLatest, Observable } from 'rxjs'; -import { distinctUntilChanged, filter, map, take, tap } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, take } from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import { ScrollToConfigOptions, ScrollToService } from '@nicky-lenaers/ngx-scroll-to'; @@ -17,8 +17,17 @@ import { SectionStatusChangeAction, UpdateSectionDataAction } from '../objects/submission-objects.actions'; -import { SubmissionObjectEntry, SubmissionSectionError, SubmissionSectionObject } from '../objects/submission-objects.reducer'; -import { submissionObjectFromIdSelector, submissionSectionDataFromIdSelector, submissionSectionErrorsFromIdSelector, submissionSectionFromIdSelector } from '../selectors'; +import { + SubmissionObjectEntry, + SubmissionSectionError, + SubmissionSectionObject +} from '../objects/submission-objects.reducer'; +import { + submissionObjectFromIdSelector, + submissionSectionDataFromIdSelector, + submissionSectionErrorsFromIdSelector, + submissionSectionFromIdSelector +} from '../selectors'; import { SubmissionScopeType } from '../../core/submission/submission-scope-type'; import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths'; import { FormAddError, FormClearErrorsAction, FormRemoveErrorAction } from '../../shared/form/form.actions'; @@ -141,8 +150,7 @@ export class SectionsService { map((sectionData: WorkspaceitemSectionDataType) => { if (sectionType === SectionsType.SubmissionForm) { return normalizeSectionData(sectionData) - } - else { + } else { return sectionData; } }), @@ -186,8 +194,7 @@ export class SectionsService { return Object.assign({}, sectionState, { data: normalizeSectionData(sectionState.data) }) - } - else { + } else { return sectionState; } }), From 99c0f6ceaa84cbaf45af53468c59434de5ab02f8 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 3 Sep 2020 14:45:10 +0200 Subject: [PATCH 54/62] Fixed issue that emits successful remote data twice with undefined payload in the first one --- .../core/cache/builders/remote-data-build.service.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 83cecca502..23743f6999 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { combineLatest as observableCombineLatest, Observable, of as observableOf, race as observableRace } from 'rxjs'; import { distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators'; -import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotUndefined } from '../../../shared/empty.util'; +import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../../shared/empty.util'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { PaginatedList } from '../../data/paginated-list'; @@ -68,11 +68,7 @@ export class RemoteDataBuildService { return fromResponse; } }), - hasValueOperator(), - map((obj: T) => - this.linkService.resolveLinks(obj, ...linksToFollow) - ), - startWith(undefined), + map((obj: T) => hasValue(obj) ? this.linkService.resolveLinks(obj, ...linksToFollow) : undefined), distinctUntilChanged() ); return this.toRemoteDataObservable(requestEntry$, payload$); @@ -88,7 +84,7 @@ export class RemoteDataBuildService { const response = reqEntry ? reqEntry.response : undefined; if (hasValue(response)) { isSuccessful = response.statusCode === 204 || - response.statusCode >= 200 && response.statusCode < 300 && hasValue(payload); + response.statusCode >= 200 && response.statusCode < 300 && (hasValue(payload) || responsePending); const errorMessage = isSuccessful === false ? (response as ErrorResponse).errorMessage : undefined; if (hasValue(errorMessage)) { error = new RemoteDataError( From a76e0796c6a53d18e3084be392528d0140ee15ae Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 3 Sep 2020 14:46:52 +0200 Subject: [PATCH 55/62] Fixed issue with LocaleInterceptor that blocked new request --- src/app/core/auth/auth.service.ts | 11 ++++++----- src/app/core/locale/locale.service.ts | 24 +++++++++++++----------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 7d854d9d4d..a0e33fc40c 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -21,8 +21,8 @@ import { getAuthenticationToken, getRedirectUrl, isAuthenticated, - isTokenRefreshing, - isAuthenticatedLoaded + isAuthenticatedLoaded, + isTokenRefreshing } from './selectors'; import { AppState, routerStateSelector } from '../../app.reducer'; import { @@ -34,7 +34,7 @@ import { NativeWindowRef, NativeWindowService } from '../services/window.service import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util'; import { RouteService } from '../services/route.service'; import { EPersonDataService } from '../eperson/eperson-data.service'; -import { getAllSucceededRemoteDataPayload } from '../shared/operators'; +import { getAllSucceededRemoteDataPayload, getFinishedRemoteData, getRemoteDataPayload } from '../shared/operators'; import { AuthMethod } from './models/auth.method'; export const LOGIN_ROUTE = '/login'; @@ -206,8 +206,9 @@ export class AuthService { return this.store.pipe( select(getAuthenticatedUserId), hasValueOperator(), - switchMap((id: string) => this.epersonService.findById(id) ), - getAllSucceededRemoteDataPayload() + switchMap((id: string) => this.epersonService.findById(id)), + getFinishedRemoteData(), + getRemoteDataPayload(), ) } diff --git a/src/app/core/locale/locale.service.ts b/src/app/core/locale/locale.service.ts index b7f9314a33..3fc637ff3b 100644 --- a/src/app/core/locale/locale.service.ts +++ b/src/app/core/locale/locale.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Inject } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; @@ -6,9 +6,9 @@ import { isEmpty, isNotEmpty } from '../../shared/empty.util'; import { CookieService } from '../services/cookie.service'; import { environment } from '../../../environments/environment'; import { AuthService } from '../auth/auth.service'; -import { Observable, of as observableOf, combineLatest } from 'rxjs'; -import { map, take, flatMap } from 'rxjs/operators'; -import { NativeWindowService, NativeWindowRef } from '../services/window.service'; +import { combineLatest, Observable, of as observableOf } from 'rxjs'; +import { flatMap, map, take } from 'rxjs/operators'; +import { NativeWindowRef, NativeWindowService } from '../services/window.service'; export const LANG_COOKIE = 'language_cookie'; @@ -19,7 +19,7 @@ export enum LANG_ORIGIN { UI, EPERSON, BROWSER -}; +} /** * Service to provide localization handler @@ -81,12 +81,14 @@ export class LocaleService { take(1), map((eperson) => { const languages: string[] = []; - const ePersonLang = eperson.firstMetadataValue(this.EPERSON_LANG_METADATA); - if (ePersonLang) { - languages.push(...this.setQuality( - [ePersonLang], - LANG_ORIGIN.EPERSON, - !isEmpty(this.translate.currentLang))); + if (eperson) { + const ePersonLang = eperson.firstMetadataValue(this.EPERSON_LANG_METADATA); + if (ePersonLang) { + languages.push(...this.setQuality( + [ePersonLang], + LANG_ORIGIN.EPERSON, + !isEmpty(this.translate.currentLang))); + } } return languages; }) From b58defc0f319e09d80fd0acd991a29cca95b84db Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 7 Sep 2020 09:26:02 +0200 Subject: [PATCH 56/62] Revert "Fixed issue that emits successful remote data twice with undefined payload in the first one" This reverts commit 99c0f6ce --- .../core/cache/builders/remote-data-build.service.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 23743f6999..83cecca502 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { combineLatest as observableCombineLatest, Observable, of as observableOf, race as observableRace } from 'rxjs'; import { distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators'; -import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../../shared/empty.util'; +import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotUndefined } from '../../../shared/empty.util'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { PaginatedList } from '../../data/paginated-list'; @@ -68,7 +68,11 @@ export class RemoteDataBuildService { return fromResponse; } }), - map((obj: T) => hasValue(obj) ? this.linkService.resolveLinks(obj, ...linksToFollow) : undefined), + hasValueOperator(), + map((obj: T) => + this.linkService.resolveLinks(obj, ...linksToFollow) + ), + startWith(undefined), distinctUntilChanged() ); return this.toRemoteDataObservable(requestEntry$, payload$); @@ -84,7 +88,7 @@ export class RemoteDataBuildService { const response = reqEntry ? reqEntry.response : undefined; if (hasValue(response)) { isSuccessful = response.statusCode === 204 || - response.statusCode >= 200 && response.statusCode < 300 && (hasValue(payload) || responsePending); + response.statusCode >= 200 && response.statusCode < 300 && hasValue(payload); const errorMessage = isSuccessful === false ? (response as ErrorResponse).errorMessage : undefined; if (hasValue(errorMessage)) { error = new RemoteDataError( From 6feb344aa027b63188468316b0aaacbfe8168aff Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 7 Sep 2020 09:26:23 +0200 Subject: [PATCH 57/62] Revert "Fixed issue with LocaleInterceptor that blocked new request" This reverts commit a76e0796 --- src/app/core/auth/auth.service.ts | 11 +++++------ src/app/core/locale/locale.service.ts | 24 +++++++++++------------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index a0e33fc40c..7d854d9d4d 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -21,8 +21,8 @@ import { getAuthenticationToken, getRedirectUrl, isAuthenticated, - isAuthenticatedLoaded, - isTokenRefreshing + isTokenRefreshing, + isAuthenticatedLoaded } from './selectors'; import { AppState, routerStateSelector } from '../../app.reducer'; import { @@ -34,7 +34,7 @@ import { NativeWindowRef, NativeWindowService } from '../services/window.service import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util'; import { RouteService } from '../services/route.service'; import { EPersonDataService } from '../eperson/eperson-data.service'; -import { getAllSucceededRemoteDataPayload, getFinishedRemoteData, getRemoteDataPayload } from '../shared/operators'; +import { getAllSucceededRemoteDataPayload } from '../shared/operators'; import { AuthMethod } from './models/auth.method'; export const LOGIN_ROUTE = '/login'; @@ -206,9 +206,8 @@ export class AuthService { return this.store.pipe( select(getAuthenticatedUserId), hasValueOperator(), - switchMap((id: string) => this.epersonService.findById(id)), - getFinishedRemoteData(), - getRemoteDataPayload(), + switchMap((id: string) => this.epersonService.findById(id) ), + getAllSucceededRemoteDataPayload() ) } diff --git a/src/app/core/locale/locale.service.ts b/src/app/core/locale/locale.service.ts index 3fc637ff3b..b7f9314a33 100644 --- a/src/app/core/locale/locale.service.ts +++ b/src/app/core/locale/locale.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@angular/core'; +import { Injectable, Inject } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; @@ -6,9 +6,9 @@ import { isEmpty, isNotEmpty } from '../../shared/empty.util'; import { CookieService } from '../services/cookie.service'; import { environment } from '../../../environments/environment'; import { AuthService } from '../auth/auth.service'; -import { combineLatest, Observable, of as observableOf } from 'rxjs'; -import { flatMap, map, take } from 'rxjs/operators'; -import { NativeWindowRef, NativeWindowService } from '../services/window.service'; +import { Observable, of as observableOf, combineLatest } from 'rxjs'; +import { map, take, flatMap } from 'rxjs/operators'; +import { NativeWindowService, NativeWindowRef } from '../services/window.service'; export const LANG_COOKIE = 'language_cookie'; @@ -19,7 +19,7 @@ export enum LANG_ORIGIN { UI, EPERSON, BROWSER -} +}; /** * Service to provide localization handler @@ -81,14 +81,12 @@ export class LocaleService { take(1), map((eperson) => { const languages: string[] = []; - if (eperson) { - const ePersonLang = eperson.firstMetadataValue(this.EPERSON_LANG_METADATA); - if (ePersonLang) { - languages.push(...this.setQuality( - [ePersonLang], - LANG_ORIGIN.EPERSON, - !isEmpty(this.translate.currentLang))); - } + const ePersonLang = eperson.firstMetadataValue(this.EPERSON_LANG_METADATA); + if (ePersonLang) { + languages.push(...this.setQuality( + [ePersonLang], + LANG_ORIGIN.EPERSON, + !isEmpty(this.translate.currentLang))); } return languages; }) From ac86976115718f465c28e2150b0934ad6e15e8c3 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 7 Sep 2020 09:27:38 +0200 Subject: [PATCH 58/62] Disabled EPerson language when sending Accept-Language header --- src/app/core/locale/locale.service.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/core/locale/locale.service.ts b/src/app/core/locale/locale.service.ts index b7f9314a33..ade5c1f864 100644 --- a/src/app/core/locale/locale.service.ts +++ b/src/app/core/locale/locale.service.ts @@ -19,7 +19,7 @@ export enum LANG_ORIGIN { UI, EPERSON, BROWSER -}; +} /** * Service to provide localization handler @@ -75,8 +75,9 @@ export class LocaleService { return obs$.pipe( take(1), flatMap(([isAuthenticated, isLoaded]) => { - let epersonLang$: Observable = observableOf([]); - if (isAuthenticated && isLoaded) { + // TODO to enabled again when https://github.com/DSpace/dspace-angular/issues/739 will be resolved + const epersonLang$: Observable = observableOf([]); +/* if (isAuthenticated && isLoaded) { epersonLang$ = this.authService.getAuthenticatedUserFromStore().pipe( take(1), map((eperson) => { @@ -91,7 +92,7 @@ export class LocaleService { return languages; }) ); - } + }*/ return epersonLang$.pipe( map((epersonLang: string[]) => { const languages: string[] = []; From 1e8669a73812e807e6656e474be8395ab337f2ce Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 11 Sep 2020 15:30:38 +0200 Subject: [PATCH 59/62] Fixed issue with change not detected --- .../ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts index 90319ee64d..ff3279e108 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts @@ -290,6 +290,7 @@ export class DsDynamicLookupComponent extends DsDynamicVocabularyComponent imple } else { this.firstInputValue = displayValue || ''; } + this.cdr.detectChanges(); } } From fab0a09727420c0e30c3930150f1620f9f24c589 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 11 Sep 2020 15:33:05 +0200 Subject: [PATCH 60/62] Fixed issue with adding lookup repeatable fields --- src/app/shared/form/form.component.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/app/shared/form/form.component.ts b/src/app/shared/form/form.component.ts index b6813afdfb..7a5d3932c8 100644 --- a/src/app/shared/form/form.component.ts +++ b/src/app/shared/form/form.component.ts @@ -2,16 +2,20 @@ import { distinctUntilChanged, filter, map } from 'rxjs/operators'; import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms'; -import { DynamicFormArrayModel, DynamicFormControlEvent, DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, } from '@ng-dynamic-forms/core'; +import { + DynamicFormArrayModel, + DynamicFormControlEvent, + DynamicFormControlModel, + DynamicFormGroupModel, + DynamicFormLayout, +} from '@ng-dynamic-forms/core'; import { findIndex } from 'lodash'; import { FormBuilderService } from './builder/form-builder.service'; import { Observable, Subscription } from 'rxjs'; import { hasValue, isNotEmpty, isNotNull, isNull } from '../empty.util'; import { FormService } from './form.service'; import { FormEntry, FormError } from './form.reducer'; -import { DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN } from './builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; import { QUALDROP_GROUP_SUFFIX } from './builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model'; -import { DYNAMIC_FORM_CONTROL_TYPE_ONEBOX } from './builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.model'; const QUALDROP_GROUP_REGEX = new RegExp(`${QUALDROP_GROUP_SUFFIX}_\\d+$`); @@ -151,8 +155,8 @@ export class FormComponent implements OnDestroy, OnInit { this.formValid = this.getFormGroupValidStatus(); this.subs.push(this.formGroup.statusChanges.pipe( - filter((currentStatus) => this.formValid !== this.getFormGroupValidStatus())) - .subscribe((currentStatus) => { + filter(() => this.formValid !== this.getFormGroupValidStatus())) + .subscribe(() => { this.formService.setStatusChanged(this.formId, this.getFormGroupValidStatus()); this.formValid = this.getFormGroupValidStatus(); })); @@ -317,7 +321,7 @@ export class FormComponent implements OnDestroy, OnInit { // set that field to the new value const model = arrayContext.groups[arrayContext.groups.length - 1].group[0] as any; - if (model.type === DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN || model.type === DYNAMIC_FORM_CONTROL_TYPE_ONEBOX) { + if (model.hasAuthority) { model.value = Object.values(value)[0]; const ctrl = formArrayControl.controls[formArrayControl.length - 1]; const ctrlValue = ctrl.value; From a178e215f00e13468568f39717c0c268cc8c94d7 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 11 Sep 2020 15:35:50 +0200 Subject: [PATCH 61/62] Fixed issue with preparing patch operation's value when value has an authority id but is not an instance of VocabularyEntry or FormFieldMetadataValueObject --- .../json-patch/builder/json-patch-operations-builder.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/core/json-patch/builder/json-patch-operations-builder.ts b/src/app/core/json-patch/builder/json-patch-operations-builder.ts index 3755821bf9..ced3750834 100644 --- a/src/app/core/json-patch/builder/json-patch-operations-builder.ts +++ b/src/app/core/json-patch/builder/json-patch-operations-builder.ts @@ -134,6 +134,8 @@ export class JsonPatchOperationsBuilder { operationValue = this.prepareAuthorityValue(value); } else if (value instanceof FormFieldLanguageValueObject) { operationValue = new FormFieldMetadataValueObject(value.value, value.language); + } else if (value.hasOwnProperty('authority')) { + operationValue = new FormFieldMetadataValueObject(value.value, value.language, value.authority); } else if (value.hasOwnProperty('value')) { operationValue = new FormFieldMetadataValueObject(value.value); } else { @@ -149,8 +151,8 @@ export class JsonPatchOperationsBuilder { return operationValue; } - protected prepareAuthorityValue(value: any) { - let operationValue: any = null; + protected prepareAuthorityValue(value: any): FormFieldMetadataValueObject { + let operationValue: FormFieldMetadataValueObject; if (isNotEmpty(value.authority)) { operationValue = new FormFieldMetadataValueObject(value.value, value.language, value.authority); } else { From c5cb1a2837d4738fe0d133255013d34d2a415865 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 8 Sep 2020 11:10:17 +0200 Subject: [PATCH 62/62] [CSTPER-75] Fixed confidence status for linked metadata with authority --- .../models/onebox/dynamic-onebox.component.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts index 11002204f5..43ea03228d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts @@ -259,10 +259,8 @@ export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent imple } else { if (isEmpty(value)) { result = ''; - } else if (typeof value === 'string') { - result = value; } else { - result = value.display; + result = value.value; } this.currentValue = result;