Merge branch 'Hard-redirect-after-log-in' into User-agreement

This commit is contained in:
Art Lowel
2020-09-11 16:54:17 +02:00
139 changed files with 5288 additions and 2165 deletions

View File

@@ -4,7 +4,6 @@ import { SharedModule } from '../shared/shared.module';
import { CommunityListPageComponent } from './community-list-page.component';
import { CommunityListPageRoutingModule } from './community-list-page.routing.module';
import { CommunityListComponent } from './community-list/community-list.component';
import { CdkTreeModule } from '@angular/cdk/tree';
/**
* The page which houses a title and the community list, as described in community-list.component
@@ -13,8 +12,7 @@ import { CdkTreeModule } from '@angular/cdk/tree';
imports: [
CommonModule,
SharedModule,
CommunityListPageRoutingModule,
CdkTreeModule,
CommunityListPageRoutingModule
],
declarations: [
CommunityListPageComponent,

View File

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

View File

@@ -5,7 +5,6 @@ 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 { PaginatedList } from '../data/paginated-list';
import { SubmissionObject } from '../submission/models/submission-object.model';
import { DSpaceObject } from '../shared/dspace-object.model';
@@ -181,17 +180,6 @@ export class TokenResponse extends RestResponse {
}
}
export class IntegrationSuccessResponse extends RestResponse {
constructor(
public dataDefinition: PaginatedList<IntegrationModel>,
public statusCode: number,
public statusText: string,
public pageInfo?: PageInfo
) {
super(true, statusCode, statusText);
}
}
export class PostPatchSuccessResponse extends RestResponse {
constructor(
public dataDefinition: any,

View File

@@ -1,6 +1,7 @@
import { CommonModule } from '@angular/common';
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
import { EffectsModule } from '@ngrx/effects';
@@ -15,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';
@@ -80,9 +81,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';
@@ -160,6 +158,12 @@ import { SubmissionCcLicenseDataService } from './submission/submission-cc-licen
import { SubmissionCcLicence } from './submission/models/submission-cc-license.model';
import { SubmissionCcLicenceUrl } from './submission/models/submission-cc-license-url.model';
import { SubmissionCcLicenseUrlDataService } from './submission/submission-cc-license-url-data.service';
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';
import { VocabularyTreeviewService } from '../shared/vocabulary-treeview/vocabulary-treeview.service';
import { ConfigurationDataService } from './data/configuration-data.service';
import { ConfigurationProperty } from './shared/configuration-property.model';
import { ReloadGuard } from './reload/reload.guard';
@@ -199,7 +203,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,
@@ -241,8 +245,6 @@ const PROVIDERS = [
SubmissionResponseParsingService,
SubmissionJsonPatchOperationsService,
JsonPatchOperationsBuilder,
AuthorityService,
IntegrationResponseParsingService,
UploaderService,
UUIDService,
NotificationsService,
@@ -311,7 +313,10 @@ const PROVIDERS = [
},
NotificationsService,
FilteredDiscoveryPageResponseParsingService,
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
{ provide: NativeWindowService, useFactory: NativeWindowFactory },
VocabularyService,
VocabularyEntriesResponseParsingService,
VocabularyTreeviewService
];
/**
@@ -342,7 +347,6 @@ export const models =
SubmissionSectionModel,
SubmissionUploadsModel,
AuthStatus,
AuthorityValue,
BrowseEntry,
BrowseDefinition,
ClaimedTask,
@@ -362,6 +366,9 @@ export const models =
Feature,
Authorization,
Registration,
Vocabulary,
VocabularyEntry,
VocabularyEntryDetail,
ConfigurationProperty
];

View File

@@ -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<BrowseEntry> {
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<BrowseEntry> {
return BrowseEntry;
}
}

View File

@@ -1,21 +1,11 @@
import { HttpClient } from '@angular/common/http';
import { Store } from '@ngrx/store';
import { compare, Operation } from 'fast-json-patch';
import { Observable, of as observableOf } from 'rxjs';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { SortDirection, SortOptions } from '../cache/models/sort-options.model';
import { ObjectCacheService } from '../cache/object-cache.service';
import { CoreState } from '../core.reducers';
import { DSpaceObject } from '../shared/dspace-object.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { Item } from '../shared/item.model';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { ChangeAnalyzer } from './change-analyzer';
import { DataService } from './data.service';
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 { BundleDataService } from './bundle-data.service';

View File

@@ -6,18 +6,18 @@ import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-servic
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
import { fakeAsync, tick } from '@angular/core/testing';
import { ContentSourceRequest, GetRequest, RequestError, UpdateContentSourceRequest } from './request.models';
import { ContentSourceRequest, GetRequest, UpdateContentSourceRequest } from './request.models';
import { ContentSource } from '../shared/content-source.model';
import { of as observableOf } from 'rxjs/internal/observable/of';
import { RequestEntry } from './request.reducer';
import { ErrorResponse, RestResponse } from '../cache/response.models';
import { ErrorResponse } from '../cache/response.models';
import { ObjectCacheService } from '../cache/object-cache.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { Collection } from '../shared/collection.model';
import { PageInfo } from '../shared/page-info.model';
import { PaginatedList } from './paginated-list';
import { createSuccessfulRemoteDataObject } from 'src/app/shared/remote-data.utils';
import { hot, getTestScheduler, cold } from 'jasmine-marbles';
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing';
const url = 'fake-url';

View File

@@ -24,7 +24,7 @@ import { RequestService } from './request.service';
@dataService(COMMUNITY)
export class CommunityDataService extends ComColDataService<Community> {
protected linkPath = 'communities';
protected topLinkPath = 'communities/search/top';
protected topLinkPath = 'search/top';
protected cds = this;
constructor(

View File

@@ -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&param2=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`;

View File

@@ -71,13 +71,17 @@ export abstract class DataService<T extends CacheableObject> 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 getFindAllHref(options: FindListOptions = {}, linkPath?: string, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<string> {
let result$: Observable<string>;
public getFindAllHref(options: FindListOptions = {}, linkPath?: string, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<string> {
let endpoint$: Observable<string>;
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)));
}
/**
@@ -89,18 +93,12 @@ export abstract class DataService<T extends CacheableObject> 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 getSearchByHref(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<string> {
public getSearchByHref(searchMethod: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<T>>): Observable<string> {
let result$: Observable<string>;
const args = [];
result$ = this.getSearchEndpoint(searchMethod);
if (hasValue(options.searchParams)) {
options.searchParams.forEach((param: RequestParam) => {
args.push(`${param.fieldName}=${param.fieldValue}`);
})
}
return result$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args, ...linksToFollow)));
}
@@ -114,7 +112,7 @@ export abstract class DataService<T extends CacheableObject> 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<FollowLinkConfig<T>>): string {
public buildHrefFromFindOptions(href: string, options: FindListOptions, extraArgs: string[] = [], ...linksToFollow: Array<FollowLinkConfig<T>>): string {
let args = [...extraArgs];
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
@@ -130,6 +128,11 @@ export abstract class DataService<T extends CacheableObject> implements UpdateDa
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();

View File

@@ -0,0 +1,54 @@
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';
/**
* An abstract class to extend, responsible for parsing data for an entries response
*/
export abstract class EntriesResponseParsingService<T extends CacheableObject> extends BaseResponseParsingService implements ResponseParsingService {
protected toCache = false;
constructor(
protected objectCache: ObjectCacheService,
) {
super();
}
/**
* Abstract method to implement that must return the dspace serializer Constructor to use during parse
*/
abstract getSerializerModel(): GenericConstructor<T>;
/**
* Parse response
*
* @param request
* @param data
*/
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload)) {
let entries = [];
if (isNotEmpty(data.payload._embedded) && Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
const serializer = new DSpaceSerializer(this.getSerializerModel());
entries = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
}
return new GenericSuccessResponse(entries, 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 }
)
);
}
}
}

View File

@@ -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';
@@ -20,12 +19,13 @@ import { ContentSourceResponseParsingService } from './content-source-response-p
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
import { ProcessFilesResponseParsingService } from './process-files-response-parsing.service';
import { TokenResponseParsingService } from '../auth/token-response-parsing.service';
import { VocabularyEntriesResponseParsingService } from '../submission/vocabularies/vocabulary-entries-response-parsing.service';
/* tslint:disable:max-classes-per-file */
// uuid and handle requests have separate endpoints
export enum IdentifierType {
UUID ='uuid',
UUID = 'uuid',
HANDLE = 'handle'
}
@@ -60,7 +60,7 @@ export class GetRequest extends RestRequest {
public href: string,
public body?: any,
public options?: HttpOptions
) {
) {
super(uuid, href, RestRequestMethod.GET, body, options)
}
}
@@ -71,7 +71,7 @@ export class PostRequest extends RestRequest {
public href: string,
public body?: any,
public options?: HttpOptions
) {
) {
super(uuid, href, RestRequestMethod.POST, body)
}
}
@@ -97,7 +97,7 @@ export class PutRequest extends RestRequest {
public href: string,
public body?: any,
public options?: HttpOptions
) {
) {
super(uuid, href, RestRequestMethod.PUT, body)
}
}
@@ -108,7 +108,7 @@ export class DeleteRequest extends RestRequest {
public href: string,
public body?: any,
public options?: HttpOptions
) {
) {
super(uuid, href, RestRequestMethod.DELETE, body)
}
}
@@ -119,7 +119,7 @@ export class OptionsRequest extends RestRequest {
public href: string,
public body?: any,
public options?: HttpOptions
) {
) {
super(uuid, href, RestRequestMethod.OPTIONS, body)
}
}
@@ -130,7 +130,7 @@ export class HeadRequest extends RestRequest {
public href: string,
public body?: any,
public options?: HttpOptions
) {
) {
super(uuid, href, RestRequestMethod.HEAD, body)
}
}
@@ -143,7 +143,7 @@ export class PatchRequest extends RestRequest {
public href: string,
public body?: any,
public options?: HttpOptions
) {
) {
super(uuid, href, RestRequestMethod.PATCH, body)
}
}
@@ -276,16 +276,6 @@ export class TokenPostRequest extends PostRequest {
}
}
export class IntegrationRequest extends GetRequest {
constructor(uuid: string, href: string) {
super(uuid, href);
}
getResponseParser(): GenericConstructor<ResponseParsingService> {
return IntegrationResponseParsingService;
}
}
/**
* Class representing a submission HTTP GET request object
*/
@@ -425,6 +415,15 @@ export class MyDSpaceRequest extends GetRequest {
public responseMsToLive = 10 * 1000;
}
/**
* Request to get vocabulary entries
*/
export class VocabularyEntriesRequest extends FindListRequest {
getResponseParser(): GenericConstructor<ResponseParsingService> {
return VocabularyEntriesResponseParsingService;
}
}
export class RequestError extends Error {
statusCode: number;
statusText: string;

View File

@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
import { createSelector, select, Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { distinctUntilChanged, filter, map, switchMap, take, tap } from 'rxjs/operators';
import { filter, map, take, tap } from 'rxjs/operators';
import {
GroupRegistryCancelGroupAction,
GroupRegistryEditGroupAction
@@ -21,18 +21,12 @@ import { DataService } from '../data/data.service';
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
import { PaginatedList } from '../data/paginated-list';
import { RemoteData } from '../data/remote-data';
import {
CreateRequest,
DeleteRequest,
FindListOptions,
FindListRequest,
PostRequest
} from '../data/request.models';
import { CreateRequest, DeleteRequest, FindListOptions, FindListRequest, PostRequest } from '../data/request.models';
import { RequestService } from '../data/request.service';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { configureRequest, getResponseFromEntry} from '../shared/operators';
import { getResponseFromEntry } from '../shared/operators';
import { EPerson } from './models/eperson.model';
import { Group } from './models/group.model';
import { dataService } from '../cache/builders/build-decorators';

View File

@@ -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();
}
}

View File

@@ -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[]
) { }
}

View File

@@ -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<CoreState>;
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);
});
});
});

View File

@@ -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<IntegrationModel>(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<IntegrationModel>): 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;
}
}

View File

@@ -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);
});
});
});

View File

@@ -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<IntegrationData> {
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<IntegrationData> {
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<IntegrationData> {
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());
}
}

View File

@@ -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;
}
}

View File

@@ -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');

View File

@@ -1,92 +0,0 @@
import { autoserialize, deserialize, inheritSerialization } from 'cerialize';
import { isNotEmpty } from '../../../shared/empty.util';
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';
import { PLACEHOLDER_PARENT_METADATA } from '../../../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-constants';
/**
* 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;
}
}

View File

@@ -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) {
}
}

View File

@@ -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
}
}

View File

@@ -1,11 +1,16 @@
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 { 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';
@@ -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 {
@@ -125,10 +130,12 @@ 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);
} 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 {
@@ -144,10 +151,10 @@ export class JsonPatchOperationsBuilder {
return operationValue;
}
protected prepareAuthorityValue(value: any) {
let operationValue: any = null;
if (isNotEmpty(value.id)) {
operationValue = new FormFieldMetadataValueObject(value.value, value.language, value.id);
protected prepareAuthorityValue(value: any): FormFieldMetadataValueObject {
let operationValue: FormFieldMetadataValueObject;
if (isNotEmpty(value.authority)) {
operationValue = new FormFieldMetadataValueObject(value.value, value.language, value.authority);
} else {
operationValue = new FormFieldMetadataValueObject(value.value, value.language);
}

View File

@@ -1,9 +1,8 @@
import { async, TestBed } from '@angular/core/testing';
import { getTestScheduler } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing';
import { of as observableOf } from 'rxjs';
import { Store, StoreModule } from '@ngrx/store';
import { catchError } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
import { RequestService } from '../data/request.service';
@@ -22,7 +21,6 @@ import {
StartTransactionPatchOperationsAction
} from './json-patch-operations.actions';
import { RequestEntry } from '../data/request.reducer';
import { catchError } from 'rxjs/operators';
class TestService extends JsonPatchOperationsService<SubmitDataResponseDefinitionObject, SubmissionPatchRequest> {
protected linkPath = '';

View File

@@ -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<string[]> = observableOf([]);
if (isAuthenticated && isLoaded) {
// TODO to enabled again when https://github.com/DSpace/dspace-angular/issues/739 will be resolved
const epersonLang$: Observable<string[]> = 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[] = [];

View File

@@ -1,8 +1,6 @@
import { Observable } from 'rxjs';
import { SubmissionService } from '../../submission/submission.service';
import { RemoteData } from '../data/remote-data';
import { SubmissionObject } from './models/submission-object.model';
import { WorkspaceItem } from './models/workspaceitem.model';
import { SubmissionObjectDataService } from './submission-object-data.service';
import { SubmissionScopeType } from './submission-scope-type';
import { WorkflowItemDataService } from './workflowitem-data.service';

View File

@@ -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) {

View File

@@ -0,0 +1,12 @@
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');
export const VOCABULARY_ENTRY_DETAIL = new ResourceType('vocabularyEntryDetail');

View File

@@ -0,0 +1,39 @@
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;
/**
* The unique id of the entry
*/
@autoserialize
id: string;
/**
* 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
};
}

View File

@@ -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/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';
import { GenericConstructor } from '../../../shared/generic-constructor';
/**
* Model class for a VocabularyEntry
*/
@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<string | GenericConstructor<ListableObject>> {
return [this.constructor as GenericConstructor<ListableObject>];
}
}

View File

@@ -0,0 +1,37 @@
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 query: string = '',
public filter?: string,
public exact?: boolean,
public entryID?: string,
public elementsPerPage?: number,
public currentPage?: number,
public sort?: SortOptions
) {
super();
const searchParams = [];
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.toString()))
}
if (isNotEmpty(entryID)) {
searchParams.push(new RequestParam('entryID', entryID))
}
this.searchParams = searchParams;
}
}

View File

@@ -0,0 +1,21 @@
/**
* Representing vocabulary properties
*/
export class VocabularyOptions {
/**
* The name of the vocabulary
*/
name: string;
/**
* A boolean representing if value is closely related to a vocabulary entry or not
*/
closed: boolean;
constructor(name: string,
closed: boolean = false) {
this.name = name;
this.closed = closed;
}
}

View File

@@ -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
};
}

View File

@@ -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';
describe('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/rest/api/submission/vocabularies/types/entries/first?page=0&size=5'
},
self: {
href: 'https://rest.api/rest/api/submission/vocabularies/types/entries'
},
next: {
href: 'https://rest.api/rest/api/submission/vocabularies/types/entries/next?page=1&size=5'
},
last: {
href: 'https://rest.api/rest/api/submission/vocabularies/types/entries/last?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);
});
});
});

View File

@@ -0,0 +1,26 @@
import { Injectable } from '@angular/core';
import { ObjectCacheService } from '../../cache/object-cache.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 EntriesResponseParsingService<VocabularyEntry> {
protected toCache = false;
constructor(
protected objectCache: ObjectCacheService,
) {
super(objectCache);
}
getSerializerModel(): GenericConstructor<VocabularyEntry> {
return VocabularyEntry;
}
}

View File

@@ -0,0 +1,569 @@
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 { 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, 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';
import { VocabularyOptions } from './models/vocabulary-options.model';
import { VocabularyFindOptions } from './models/vocabulary-find-options.model';
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 hierarchicalVocabulary: 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 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',
value: 'testParent',
otherInformation: {
id: 'authorityId2',
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 vocabularyEntryChildDetail: any = {
authority: 'authoritytestChild1',
display: 'testChild1',
value: 'testChild1',
otherInformation: {
id: 'authoritytestChild1',
hasChildren: 'true',
note: 'Familjeforskning'
},
type: 'vocabularyEntryDetail',
_links: {
self: {
href: 'https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:authoritytestChild1'
},
parent: {
href: 'https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:parent'
},
children: {
href: 'https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:children'
}
}
};
const vocabularyEntryChild2Detail: any = {
authority: 'authoritytestChild2',
display: 'testChild2',
value: 'testChild2',
otherInformation: {
id: 'authoritytestChild2',
hasChildren: 'true',
note: 'Familjeforskning'
},
type: 'vocabularyEntryDetail',
_links: {
self: {
href: 'https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:authoritytestChild2'
},
parent: {
href: 'https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:parent'
},
children: {
href: 'https://rest.api/rest/api/submission/vocabularyEntryDetails/srsc:children'
}
}
};
const endpointURL = `https://rest.api/rest/api/submission/vocabularies`;
const requestURL = `https://rest.api/rest/api/submission/vocabularies/${vocabulary.id}`;
const entryDetailEndpointURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails`;
const entryDetailRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/${hierarchicalVocabulary.id}:testValue`;
const entryDetailParentRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/${hierarchicalVocabulary.id}:testValue/parent`;
const entryDetailChildrenRequestURL = `https://rest.api/rest/api/submission/vocabularyEntryDetails/${hierarchicalVocabulary.id}:testValue/children`;
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
const vocabularyId = 'types';
const metadata = 'dc.type';
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`;
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,
closed: false
}
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 vocabularyRD$ = createSuccessfulRemoteDataObject$(vocabulary);
const vocabularyEntriesRD = createSuccessfulRemoteDataObject$(paginatedListEntries);
const vocabularyEntryDetailParentRD = createSuccessfulRemoteDataObject(vocabularyEntryParentDetail);
const vocabularyEntryChildrenRD = createSuccessfulRemoteDataObject(childrenPaginatedList);
const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList);
const getRequestEntries$ = (successful: boolean) => {
return observableOf({
response: { isSuccessful: successful, payload: arrayEntries } as any
} as RequestEntry)
};
objectCache = {} as ObjectCacheService;
const notificationsService = {} as NotificationsService;
const http = {} as HttpClient;
const comparator = {} as any;
const comparatorEntry = {} as any;
function initTestService() {
return new VocabularyService(
requestService,
rdbService,
objectCache,
halService,
notificationsService,
http,
comparator,
comparatorEntry
);
}
describe('vocabularies endpoint', () => {
beforeEach(() => {
scheduler = getTestScheduler();
halService = jasmine.createSpyObj('halService', {
getEndpoint: cold('a', { a: endpointURL })
});
});
afterEach(() => {
service = null;
});
describe('', () => {
beforeEach(() => {
responseCacheEntry = new RequestEntry();
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
responseCacheEntry.completed = true;
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();
expect((service as any).vocabularyDataService.findById).toHaveBeenCalledWith(vocabularyId);
});
it('should return a RemoteData<Vocabulary> 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<Vocabulary> 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<PaginatedList<Vocabulary>>', () => {
const result = service.findAllVocabularies();
const expected = cold('a|', {
a: paginatedListRD
});
expect(result).toBeObservable(expected);
});
});
});
describe('', () => {
beforeEach(() => {
requestService = getMockRequestService(getRequestEntries$(true));
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);
scheduler.schedule(() => service.getVocabularyEntries(vocabularyOptions, pageInfo).subscribe());
scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected);
});
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
scheduler.schedule(() => service.getVocabularyEntries(vocabularyOptions, pageInfo));
scheduler.flush();
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', () => {
scheduler.schedule(() => service.getVocabularyEntriesByValue('test', false, vocabularyOptions, pageInfo));
scheduler.flush();
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', () => {
scheduler.schedule(() => service.getVocabularyEntryByValue('test', vocabularyOptions));
scheduler.flush();
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', () => {
scheduler.schedule(() => service.getVocabularyEntryByID('test', vocabularyOptions));
scheduler.flush();
expect(rdbService.toRemoteDataObservable).toHaveBeenCalled();
});
});
});
});
describe('vocabularyEntryDetails endpoint', () => {
beforeEach(() => {
scheduler = getTestScheduler();
halService = jasmine.createSpyObj('halService', {
getEndpoint: cold('a', { a: entryDetailEndpointURL })
});
responseCacheEntry = new RequestEntry();
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
responseCacheEntry.completed = true;
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: vocabularyEntryDetailParentRD
}),
buildList: hot('a|', {
a: vocabularyEntryChildrenRD
}),
});
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, 'findAllByHref').and.callThrough();
spyOn((service as any).vocabularyEntryDetailDataService, 'searchBy').and.callThrough();
spyOn((service as any).vocabularyEntryDetailDataService, 'getSearchByHref').and.returnValue(observableOf(searchRequestURL));
spyOn((service as any).vocabularyEntryDetailDataService, 'getFindAllHref').and.returnValue(observableOf(entryDetailChildrenRequestURL));
spyOn((service as any).vocabularyEntryDetailDataService, 'getBrowseEndpoint').and.returnValue(observableOf(entryDetailEndpointURL));
});
afterEach(() => {
service = null;
});
describe('findEntryDetailByHref', () => {
it('should proxy the call to vocabularyDataService.findEntryDetailByHref', () => {
scheduler.schedule(() => service.findEntryDetailByHref(entryDetailRequestURL));
scheduler.flush();
expect((service as any).vocabularyEntryDetailDataService.findByHref).toHaveBeenCalledWith(entryDetailRequestURL);
});
it('should return a RemoteData<VocabularyEntryDetail> for the object with the given URL', () => {
const result = service.findEntryDetailByHref(entryDetailRequestURL);
const expected = cold('a|', {
a: vocabularyEntryDetailParentRD
});
expect(result).toBeObservable(expected);
});
});
describe('findEntryDetailById', () => {
it('should proxy the call to vocabularyDataService.findVocabularyById', () => {
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<VocabularyEntryDetail> for the object with the given id', () => {
const result = service.findEntryDetailById('testValue', hierarchicalVocabulary.id);
const expected = cold('a|', {
a: vocabularyEntryDetailParentRD
});
expect(result).toBeObservable(expected);
});
});
describe('getEntryDetailParent', () => {
it('should proxy the call to vocabularyDataService.getEntryDetailParent', () => {
scheduler.schedule(() => service.getEntryDetailParent('testValue', hierarchicalVocabulary.id).subscribe());
scheduler.flush();
expect((service as any).vocabularyEntryDetailDataService.findByHref).toHaveBeenCalledWith(entryDetailParentRequestURL);
});
it('should return a RemoteData<VocabularyEntryDetail> for the object with the given URL', () => {
const result = service.getEntryDetailParent('testValue', hierarchicalVocabulary.id);
const expected = cold('a|', {
a: vocabularyEntryDetailParentRD
});
expect(result).toBeObservable(expected);
});
});
describe('getEntryDetailChildren', () => {
it('should proxy the call to vocabularyDataService.getEntryDetailChildren', () => {
const options: VocabularyFindOptions = new VocabularyFindOptions(
null,
null,
null,
null,
pageInfo.elementsPerPage,
pageInfo.currentPage
);
scheduler.schedule(() => service.getEntryDetailChildren('testValue', hierarchicalVocabulary.id, pageInfo).subscribe());
scheduler.flush();
expect((service as any).vocabularyEntryDetailDataService.findAllByHref).toHaveBeenCalledWith(entryDetailChildrenRequestURL, options);
});
it('should return a RemoteData<PaginatedList<ResourcePolicy>> for the object with the given URL', () => {
const result = service.getEntryDetailChildren('testValue', hierarchicalVocabulary.id, new PageInfo());
const expected = cold('a|', {
a: vocabularyEntryChildrenRD
});
expect(result).toBeObservable(expected);
});
});
describe('searchByTop', () => {
it('should proxy the call to vocabularyEntryDetailDataService.searchBy', () => {
const options: VocabularyFindOptions = new VocabularyFindOptions(
null,
null,
null,
null,
pageInfo.elementsPerPage,
pageInfo.currentPage
);
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<PaginatedList<ResourcePolicy>> for the search', () => {
const result = service.searchTopEntries('srsc', pageInfo);
const expected = cold('a|', {
a: vocabularyEntryChildrenRD
});
expect(result).toBeObservable(expected);
});
});
describe('clearSearchTopRequests', () => {
it('should remove requests on the data service\'s endpoint', (done) => {
service.clearSearchTopRequests();
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(`search/${(service as any).searchTopMethod}`);
done();
});
});
});
});

View File

@@ -0,0 +1,389 @@
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 { Vocabulary } from './models/vocabulary.model';
import { VOCABULARY } from './models/vocabularies.resource-type';
import { VocabularyEntry } from './models/vocabulary-entry.model';
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util';
import {
configureRequest,
filterSuccessfulResponses,
getFirstSucceededRemoteDataPayload,
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 */
/**
* A private DataService implementation to delegate specific methods to.
*/
class VocabularyDataServiceImpl extends DataService<Vocabulary> {
protected linkPath = 'vocabularies';
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: ChangeAnalyzer<Vocabulary>) {
super();
}
}
/**
* A private DataService implementation to delegate specific methods to.
*/
class VocabularyEntryDetailDataServiceImpl extends DataService<VocabularyEntryDetail> {
protected linkPath = 'vocabularyEntryDetails';
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: ChangeAnalyzer<VocabularyEntryDetail>) {
super();
}
}
/**
* A service responsible for fetching/sending data from/to the REST API on the vocabularies endpoint
*/
@Injectable()
@dataService(VOCABULARY)
export class VocabularyService {
protected searchByMetadataAndCollectionMethod = 'byMetadataAndCollection';
protected searchTopMethod = 'top';
private vocabularyDataService: VocabularyDataServiceImpl;
private vocabularyEntryDetailDataService: VocabularyEntryDetailDataServiceImpl;
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparatorVocabulary: DefaultChangeAnalyzer<Vocabulary>,
protected comparatorEntry: DefaultChangeAnalyzer<VocabularyEntryDetail>) {
this.vocabularyDataService = new VocabularyDataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparatorVocabulary);
this.vocabularyEntryDetailDataService = new VocabularyEntryDetailDataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparatorEntry);
}
/**
* 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<RemoteData<Vocabulary>>}
* Return an observable that emits vocabulary object
*/
findVocabularyByHref(href: string, ...linksToFollow: Array<FollowLinkConfig<Vocabulary>>): Observable<RemoteData<any>> {
return this.vocabularyDataService.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 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<RemoteData<Vocabulary>>}
* Return an observable that emits vocabulary object
*/
findVocabularyById(name: string, ...linksToFollow: Array<FollowLinkConfig<Vocabulary>>): Observable<RemoteData<Vocabulary>> {
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 linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved
* @return {Observable<RemoteData<PaginatedList<Vocabulary>>>}
* Return an observable that emits object list
*/
findAllVocabularies(options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<Vocabulary>>): Observable<RemoteData<PaginatedList<Vocabulary>>> {
return this.vocabularyDataService.findAll(options, ...linksToFollow);
}
/**
* Return the {@link VocabularyEntry} list for a given {@link Vocabulary}
*
* @param vocabularyOptions The {@link VocabularyOptions} for the request to which the entries belong
* @param pageInfo The {@link PageInfo} for the request
* @return {Observable<RemoteData<PaginatedList<VocabularyEntry>>>}
* Return an observable that emits object list
*/
getVocabularyEntries(vocabularyOptions: VocabularyOptions, pageInfo: PageInfo): Observable<RemoteData<PaginatedList<VocabularyEntry>>> {
const options: VocabularyFindOptions = new VocabularyFindOptions(
null,
null,
null,
null,
pageInfo.elementsPerPage,
pageInfo.currentPage
);
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)
)
}
/**
* Return the {@link VocabularyEntry} list for a given value
*
* @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<RemoteData<PaginatedList<VocabularyEntry>>>}
* Return an observable that emits object list
*/
getVocabularyEntriesByValue(value: string, exact: boolean, vocabularyOptions: VocabularyOptions, pageInfo: PageInfo): Observable<RemoteData<PaginatedList<VocabularyEntry>>> {
const options: VocabularyFindOptions = new VocabularyFindOptions(
null,
value,
exact,
null,
pageInfo.elementsPerPage,
pageInfo.currentPage
);
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)
)
}
/**
* 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<RemoteData<PaginatedList<VocabularyEntry>>>}
* Return an observable that emits {@link VocabularyEntry} object
*/
getVocabularyEntryByValue(value: string, vocabularyOptions: VocabularyOptions): Observable<VocabularyEntry> {
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<RemoteData<PaginatedList<VocabularyEntry>>>}
* Return an observable that emits {@link VocabularyEntry} object
*/
getVocabularyEntryByID(ID: string, vocabularyOptions: VocabularyOptions): Observable<VocabularyEntry> {
const pageInfo = new PageInfo()
const options: VocabularyFindOptions = new VocabularyFindOptions(
null,
null,
null,
ID,
pageInfo.elementsPerPage,
pageInfo.currentPage
);
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),
getFirstSucceededRemoteListPayload(),
map((list: VocabularyEntry[]) => {
if (isNotEmpty(list)) {
return list[0]
} else {
return null;
}
})
);
}
/**
* 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<RemoteData<VocabularyEntryDetail>>}
* Return an observable that emits vocabulary object
*/
findEntryDetailByHref(href: string, ...linksToFollow: Array<FollowLinkConfig<VocabularyEntryDetail>>): Observable<RemoteData<VocabularyEntryDetail>> {
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 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<RemoteData<VocabularyEntryDetail>>}
* Return an observable that emits VocabularyEntryDetail object
*/
findEntryDetailById(id: string, name: string, ...linksToFollow: Array<FollowLinkConfig<VocabularyEntryDetail>>): Observable<RemoteData<VocabularyEntryDetail>> {
const findId = `${name}:${id}`;
return this.vocabularyEntryDetailDataService.findById(findId, ...linksToFollow);
}
/**
* Returns the parent detail entry for a given detail entry, with a list of {@link FollowLinkConfig},
* to automatically resolve {@link HALLink}s of the object
* @param value The entry value for which to provide parent.
* @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<RemoteData<PaginatedList<VocabularyEntryDetail>>>}
* Return an observable that emits a PaginatedList of VocabularyEntryDetail
*/
getEntryDetailParent(value: string, name: string, ...linksToFollow: Array<FollowLinkConfig<VocabularyEntryDetail>>): Observable<RemoteData<VocabularyEntryDetail>> {
const linkPath = `${name}:${value}/parent`;
return this.vocabularyEntryDetailDataService.getBrowseEndpoint().pipe(
map((href: string) => `${href}/${linkPath}`),
flatMap((href) => this.vocabularyEntryDetailDataService.findByHref(href, ...linksToFollow))
);
}
/**
* Returns the list of children detail entries for a given detail entry, with a list of {@link FollowLinkConfig},
* to automatically resolve {@link HALLink}s of the object
* @param value The entry value for which to provide children list.
* @param name The name of {@link Vocabulary} to which the entry 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
* @return {Observable<RemoteData<PaginatedList<VocabularyEntryDetail>>>}
* Return an observable that emits a PaginatedList of VocabularyEntryDetail
*/
getEntryDetailChildren(value: string, name: string, pageInfo: PageInfo, ...linksToFollow: Array<FollowLinkConfig<VocabularyEntryDetail>>): Observable<RemoteData<PaginatedList<VocabularyEntryDetail>>> {
const linkPath = `${name}:${value}/children`;
const options: VocabularyFindOptions = new VocabularyFindOptions(
null,
null,
null,
null,
pageInfo.elementsPerPage,
pageInfo.currentPage
);
return this.vocabularyEntryDetailDataService.getFindAllHref(options, linkPath).pipe(
flatMap((href) => this.vocabularyEntryDetailDataService.findAllByHref(href, options, ...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 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, pageInfo: PageInfo, ...linksToFollow: Array<FollowLinkConfig<VocabularyEntryDetail>>): Observable<RemoteData<PaginatedList<VocabularyEntryDetail>>> {
const options: VocabularyFindOptions = new VocabularyFindOptions(
null,
null,
null,
null,
pageInfo.elementsPerPage,
pageInfo.currentPage
);
options.searchParams = [new RequestParam('vocabulary', name)];
return this.vocabularyEntryDetailDataService.searchBy(this.searchTopMethod, options, ...linksToFollow)
}
/**
* Clear all search Top Requests
*/
clearSearchTopRequests(): void {
this.requestService.removeByHrefSubstring(`search/${this.searchTopMethod}`);
}
}
/**
* Operator for turning a href into a PaginatedList of VocabularyEntry
* @param requestService
* @param rdb
*/
export const getVocabularyEntriesFor = (requestService: RequestService, rdb: RemoteDataBuildService) =>
(source: Observable<string>): Observable<RemoteData<PaginatedList<VocabularyEntry>>> =>
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<RestRequest>): Observable<RemoteData<PaginatedList<VocabularyEntry>>> => {
const href$ = source.pipe(map((request: RestRequest) => request.href));
const requestEntry$ = href$.pipe(getRequestFromRequestHref(requestService));
const payload$ = requestEntry$.pipe(
filterSuccessfulResponses(),
map((response: GenericSuccessResponse<VocabularyEntry[]>) => new PaginatedList(response.pageInfo, response.payload)),
map((list: PaginatedList<VocabularyEntry>) => Object.assign(list, {
page: list.page ? list.page.map((entry: VocabularyEntry) => Object.assign(new VocabularyEntry(), entry)) : list.page
})),
distinctUntilChanged()
);
return rdb.toRemoteDataObservable(requestEntry$, payload$);
};

View File

@@ -9,7 +9,7 @@ import { DataService } from '../data/data.service';
import { RequestService } from '../data/request.service';
import { WorkflowItem } from './models/workflowitem.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { DeleteByIDRequest, FindListOptions } from '../data/request.models';
import { DeleteByIDRequest } from '../data/request.models';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';

View File

@@ -8,7 +8,6 @@ import { CoreState } from '../core.reducers';
import { DataService } from '../data/data.service';
import { RequestService } from '../data/request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { FindListOptions } from '../data/request.models';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';

View File

@@ -3,7 +3,7 @@ import { ExternalSourceEntry } from '../../../../../core/shared/external-source-
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
import { ViewMode } from '../../../../../core/shared/view-mode.model';
import { Context } from '../../../../../core/shared/context.model';
import { Component, Inject, OnInit } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { Metadata } from '../../../../../core/shared/metadata.utils';
import { MetadataValue } from '../../../../../core/shared/metadata.models';

View File

@@ -13,12 +13,13 @@ import {
import { findIndex } from 'lodash';
import { AuthorityValue } from '../../core/integration/models/authority.value';
import { VocabularyEntry } from '../../core/submission/vocabularies/models/vocabulary-entry.model';
import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model';
import { ConfidenceType } from '../../core/integration/models/confidence-type';
import { ConfidenceType } from '../../core/shared/confidence-type';
import { isNotEmpty, isNull } from '../empty.util';
import { ConfidenceIconConfig } from '../../../config/submission-config.interface';
import { environment } from '../../../environments/environment';
import { VocabularyEntryDetail } from '../../core/submission/vocabularies/models/vocabulary-entry-detail.model';
/**
* Directive to add to the element a bootstrap utility class based on metadata confidence value
@@ -31,7 +32,7 @@ export class AuthorityConfidenceStateDirective implements OnChanges, AfterViewIn
/**
* The metadata value
*/
@Input() authorityValue: AuthorityValue | FormFieldMetadataValueObject | string;
@Input() authorityValue: VocabularyEntry | FormFieldMetadataValueObject | string;
/**
* A boolean representing if to show html icon if authority value is empty
@@ -65,7 +66,6 @@ export class AuthorityConfidenceStateDirective implements OnChanges, AfterViewIn
/**
* Initialize instance variables
*
* @param {GlobalConfig} EnvConfig
* @param {ElementRef} elem
* @param {Renderer2} renderer
*/
@@ -114,7 +114,8 @@ export class AuthorityConfidenceStateDirective implements OnChanges, AfterViewIn
private getConfidenceByValue(value: any): ConfidenceType {
let confidence: ConfidenceType = ConfidenceType.CF_UNSET;
if (isNotEmpty(value) && value instanceof AuthorityValue && value.hasAuthority()) {
if (isNotEmpty(value) && (value instanceof VocabularyEntry || value instanceof VocabularyEntryDetail)
&& value.hasAuthority()) {
confidence = ConfidenceType.CF_ACCEPTED;
}

View File

@@ -11,7 +11,7 @@ import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-
import { createTestComponent } from '../testing/utils.test';
import { AuthorityConfidenceStateDirective } from '../authority-confidence/authority-confidence-state.directive';
import { TranslateModule } from '@ngx-translate/core';
import { ConfidenceType } from '../../core/integration/models/confidence-type';
import { ConfidenceType } from '../../core/shared/confidence-type';
import { SortablejsModule } from 'ngx-sortablejs';
import { environment } from '../../../environments/environment';

View File

@@ -1,7 +1,7 @@
import { isObject, uniqueId } from 'lodash';
import { hasValue, isNotEmpty } from '../../empty.util';
import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model';
import { ConfidenceType } from '../../../core/integration/models/confidence-type';
import { ConfidenceType } from '../../../core/shared/confidence-type';
import { PLACEHOLDER_PARENT_METADATA } from '../../form/builder/ds-dynamic-form-ui/ds-dynamic-form-constants';
export interface ChipsItemIcon {

View File

@@ -4,7 +4,7 @@ import { ChipsItem, ChipsItemIcon } from './chips-item.model';
import { hasValue, isNotEmpty } from '../../empty.util';
import { MetadataIconConfig } from '../../../../config/submission-config.interface';
import { FormFieldMetadataValueObject } from '../../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';
import { PLACEHOLDER_PARENT_METADATA } from '../../form/builder/ds-dynamic-form-ui/ds-dynamic-form-constants';
export class Chips {
@@ -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;
}

View File

@@ -44,15 +44,15 @@ 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';
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';
@@ -77,11 +77,9 @@ import { FormBuilderService } from '../form-builder.service';
describe('DsDynamicFormControlContainerComponent test suite', () => {
const authorityOptions: AuthorityOptions = {
closed: false,
metadata: 'list',
const vocabularyOptions: VocabularyOptions = {
name: 'type_programme',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
closed: false
};
const formModel = [
new DynamicCheckboxModel({ id: 'checkbox' }),
@@ -104,10 +102,10 @@ 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', hasSelectableMetadata: false }),
new DynamicOneboxModel({ id: 'typeahead', metadataFields: [], repeatable: false, submissionId: '1234', hasSelectableMetadata: false }),
new DynamicScrollableDropdownModel({
id: 'scrollableDropdown',
authorityOptions: authorityOptions,
vocabularyOptions: vocabularyOptions,
metadataFields: [],
repeatable: false,
submissionId: '1234',
@@ -116,12 +114,12 @@ describe('DsDynamicFormControlContainerComponent test suite', () => {
new DynamicTagModel({ id: 'tag', metadataFields: [], repeatable: false, submissionId: '1234', hasSelectableMetadata: false }),
new DynamicListCheckboxGroupModel({
id: 'checkboxList',
authorityOptions: authorityOptions,
vocabularyOptions: vocabularyOptions,
repeatable: true
}),
new DynamicListRadioGroupModel({
id: 'radioList',
authorityOptions: authorityOptions,
vocabularyOptions: vocabularyOptions,
repeatable: false
}),
new DynamicRelationGroupModel({
@@ -319,7 +317,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);

View File

@@ -58,7 +58,7 @@ import {
import { TranslateService } from '@ngx-translate/core';
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';
@@ -70,7 +70,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';
@@ -89,7 +89,13 @@ 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, getFirstSucceededRemoteDataPayload, getPaginatedListPayload, getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
import {
getAllSucceededRemoteData,
getFirstSucceededRemoteDataPayload,
getPaginatedListPayload,
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';
@@ -110,6 +116,7 @@ import { paginatedRelationsToItems } from '../../../../+item-page/simple/item-ty
import { RelationshipOptions } from '../models/relationship-options.model';
import { FormBuilderService } from '../form-builder.service';
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './ds-dynamic-form-constants';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type<DynamicFormControl> | null {
switch (model.type) {
@@ -145,8 +152,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;
@@ -297,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) {

View File

@@ -1,9 +1,12 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ExistingMetadataListElementComponent, Reorderable, ReorderableRelationship } from './existing-metadata-list-element.component';
import {
ExistingMetadataListElementComponent,
ReorderableRelationship
} from './existing-metadata-list-element.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service';
import { select, Store } from '@ngrx/store';
import { Store } from '@ngrx/store';
import { Item } from '../../../../../core/shared/item.model';
import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model';
import { RelationshipOptions } from '../../models/relationship-options.model';
@@ -11,7 +14,6 @@ import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils
import { RemoveRelationshipAction } from '../relation-lookup-modal/relationship.actions';
import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model';
import { of as observableOf } from 'rxjs';
import { RelationshipService } from '../../../../../core/data/relationship.service';
describe('ExistingMetadataListElementComponent', () => {
let component: ExistingMetadataListElementComponent;

View File

@@ -3,7 +3,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ExistingRelationListElementComponent } from './existing-relation-list-element.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service';
import { select, Store } from '@ngrx/store';
import { Store } from '@ngrx/store';
import { Item } from '../../../../../core/shared/item.model';
import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model';
import { RelationshipOptions } from '../../models/relationship-options.model';

View File

@@ -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]
});

View File

@@ -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<number>();
@Output() remove = new EventEmitter<number>();
@@ -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);

View File

@@ -1,15 +1,19 @@
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';
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';
import { MetadataValue } from '../../../../../core/shared/metadata.models';
export interface DsDynamicInputModelConfig extends DynamicInputModelConfig {
authorityOptions?: AuthorityOptions;
vocabularyOptions?: VocabularyOptions;
languageCodes?: LanguageCode[];
language?: string;
place?: number;
@@ -19,13 +23,13 @@ export interface DsDynamicInputModelConfig extends DynamicInputModelConfig {
metadataFields: string[];
submissionId: string;
hasSelectableMetadata: boolean;
metadataValue?: MetadataValue;
metadataValue?: FormFieldMetadataValueObject;
}
export class DsDynamicInputModel extends DynamicInputModel {
@serializable() authorityOptions: AuthorityOptions;
@serializable() vocabularyOptions: VocabularyOptions;
@serializable() private _languageCodes: LanguageCode[];
@serializable() private _language: string;
@serializable() languageUpdates: Subject<string>;
@@ -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);
@@ -50,7 +54,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)) {
@@ -67,11 +71,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 {
@@ -92,7 +96,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;
}
}

View File

@@ -0,0 +1,134 @@
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<any> = new EventEmitter<any>();
@Output() abstract change: EventEmitter<any> = new EventEmitter<any>();
@Output() abstract focus: EventEmitter<any> = new EventEmitter<any>();
public abstract pageInfo: PageInfo;
protected 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<FormFieldMetadataValueObject> {
let initValue$: Observable<FormFieldMetadataValueObject>;
if (isNotEmpty(this.model.value) && (this.model.value instanceof FormFieldMetadataValueObject)) {
let initEntry$: Observable<VocabularyEntry>;
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)) {
// Integrate FormFieldMetadataValueObject with retrieved information
return new FormFieldMetadataValueObject(
initEntry.value,
null,
initEntry.authority,
initEntry.display,
(this.model.value as any).place,
null,
initEntry.otherInformation || null
);
} else {
return this.model.value as any;
}
}));
} else if (isNotEmpty(this.model.value) && (this.model.value instanceof VocabularyEntry)) {
initValue$ = observableOf(
new FormFieldMetadataValueObject(
this.model.value.value,
null,
this.model.value.authority,
this.model.value.display,
0,
null,
this.model.value.otherInformation || null
)
);
} 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
});
}
}

View File

@@ -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,41 +19,41 @@ 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[];
@serializable() _value: VocabularyEntry[];
isListGroup = true;
valueUpdates: Subject<any>;
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<any>();
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);
return this.vocabularyOptions && hasValue(this.vocabularyOptions.name);
}
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
}
}

View File

@@ -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<any> {
authorityOptions: AuthorityOptions;
vocabularyOptions: VocabularyOptions;
groupLength?: number;
repeatable: boolean;
value?: any;
@@ -16,7 +16,7 @@ export interface DynamicListModelConfig extends DynamicRadioGroupModelConfig<any
export class DynamicListRadioGroupModel extends DynamicRadioGroupModel<any> {
@serializable() authorityOptions: AuthorityOptions;
@serializable() vocabularyOptions: VocabularyOptions;
@serializable() repeatable: boolean;
@serializable() groupLength: number;
isListGroup = true;
@@ -24,13 +24,13 @@ export class DynamicListRadioGroupModel extends DynamicRadioGroupModel<any> {
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);
}
}

View File

@@ -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,10 @@ export const LIST_TEST_GROUP = new FormGroup({
});
export const LIST_CHECKBOX_TEST_MODEL_CONFIG = {
authorityOptions: {
closed: false,
metadata: 'listCheckbox',
vocabularyOptions: {
name: 'type_programme',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as AuthorityOptions,
closed: false
} as VocabularyOptions,
disabled: false,
id: 'listCheckbox',
label: 'Programme',
@@ -52,12 +50,10 @@ export const LIST_CHECKBOX_TEST_MODEL_CONFIG = {
};
export const LIST_RADIO_TEST_MODEL_CONFIG = {
authorityOptions: {
closed: false,
metadata: 'listRadio',
vocabularyOptions: {
name: 'type_programme',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as AuthorityOptions,
closed: false
} as VocabularyOptions,
disabled: false,
id: 'listRadio',
label: 'Programme',
@@ -77,7 +73,7 @@ describe('DsDynamicListComponent test suite', () => {
let html;
let modelValue;
const authorityServiceStub = new AuthorityServiceStub();
const vocabularyServiceStub = new VocabularyServiceStub();
// async beforeEach
beforeEach(async(() => {
@@ -99,9 +95,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 +143,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 +179,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 +190,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 +225,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 +249,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 +260,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();
});
});
});

View File

@@ -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 { 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';
import { PageInfo } from '../../../../../../core/shared/page-info.model';
export interface ListItem {
id: string,
@@ -23,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;
@@ -39,10 +44,9 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
public items: ListItem[][] = [];
protected optionsList: AuthorityValue[];
protected searchOptions: IntegrationSearchOptions;
protected optionsList: VocabularyEntry[];
constructor(private authorityService: AuthorityService,
constructor(private vocabularyService: VocabularyService,
private cdr: ChangeDetectorRef,
private formBuilderService: FormBuilderService,
protected layoutService: DynamicFormLayoutService,
@@ -51,39 +55,46 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
super(layoutService, validationService);
}
/**
* Initialize the component, setting up the field options
*/
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,
'',
1000, // Max elements
1);// Current Page
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) {
// 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);
}
@@ -93,17 +104,25 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
this.change.emit(event);
}
protected setOptionsFromAuthority() {
if (this.model.authorityOptions.name && this.model.authorityOptions.name.length > 0) {
/**
* 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;
this.authorityService.getEntriesByName(this.searchOptions).subscribe((authorities: IntegrationData) => {
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<VocabularyEntry>) => {
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));
@@ -137,9 +156,4 @@ export class DsDynamicListComponent extends DynamicFormControlComponent implemen
}
}
protected hasAuthorityOptions() {
return (hasValue(this.model.authorityOptions.scope)
&& hasValue(this.model.authorityOptions.name)
&& hasValue(this.model.authorityOptions.metadata));
}
}

View File

@@ -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();">
</div>
@@ -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();">
</div>
<div class="col-auto text-center">

View File

@@ -2,33 +2,32 @@
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 { of as observableOf } from 'rxjs';
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, DynamicLookupModelConfig } 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: DynamicLookupModelConfig = {
authorityOptions: {
closed: false,
metadata: 'lookup',
vocabularyOptions: {
name: 'RPAuthority',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as AuthorityOptions,
closed: false
} as VocabularyOptions,
disabled: false,
errorMessages: { required: 'Required field.' },
id: 'lookup',
@@ -47,12 +46,10 @@ let LOOKUP_TEST_MODEL_CONFIG: DynamicLookupModelConfig = {
};
let LOOKUP_NAME_TEST_MODEL_CONFIG = {
authorityOptions: {
closed: false,
metadata: 'lookup-name',
vocabularyOptions: {
name: 'RPAuthority',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as AuthorityOptions,
closed: false
} as VocabularyOptions,
disabled: false,
errorMessages: { required: 'Required field.' },
id: 'lookupName',
@@ -78,12 +75,10 @@ let LOOKUP_TEST_GROUP = new FormGroup({
describe('Dynamic Lookup component', () => {
function init() {
LOOKUP_TEST_MODEL_CONFIG = {
authorityOptions: {
closed: false,
metadata: 'lookup',
vocabularyOptions: {
name: 'RPAuthority',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as AuthorityOptions,
closed: false
} as VocabularyOptions,
disabled: false,
errorMessages: { required: 'Required field.' },
id: 'lookup',
@@ -102,12 +97,10 @@ describe('Dynamic Lookup component', () => {
};
LOOKUP_NAME_TEST_MODEL_CONFIG = {
authorityOptions: {
closed: false,
metadata: 'lookup-name',
vocabularyOptions: {
name: 'RPAuthority',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as AuthorityOptions,
closed: false
} as VocabularyOptions,
disabled: false,
errorMessages: { required: 'Required field.' },
id: 'lookupName',
@@ -137,12 +130,11 @@ describe('Dynamic Lookup component', () => {
let testFixture: ComponentFixture<TestComponent>;
let lookupFixture: ComponentFixture<DsDynamicLookupComponent>;
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 +154,7 @@ describe('Dynamic Lookup component', () => {
providers: [
ChangeDetectorRef,
DsDynamicLookupComponent,
{ provide: AuthorityService, useValue: authorityService },
{ provide: VocabularyService, useValue: vocabularyServiceStub },
{ provide: DynamicFormLayoutService, useValue: {} },
{ provide: DynamicFormValidationService, useValue: {} }
],
@@ -247,7 +239,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 +247,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 +274,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 +283,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();
@@ -312,7 +302,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');
@@ -321,9 +317,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;
@@ -389,26 +428,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();
@@ -433,6 +472,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();
});
@@ -440,10 +486,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;

View File

@@ -1,33 +1,31 @@
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 {
DynamicFormControlComponent,
DynamicFormLayoutService,
DynamicFormValidationService
} from '@ng-dynamic-forms/core';
import { DynamicFormLayoutService, 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 { hasValue, isEmpty, isNotEmpty, isNull, isUndefined } from '../../../../../empty.util';
import { IntegrationData } from '../../../../../../core/integration/integration-data';
import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service';
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 { 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';
import { DsDynamicVocabularyComponent } from '../dynamic-vocabulary.component';
/**
* 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;
@@ -43,42 +41,262 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem
public pageInfo: PageInfo;
public optionsList: any;
protected searchOptions: IntegrationSearchOptions;
protected subs: Subscription[] = [];
constructor(private authorityService: AuthorityService,
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 `<input>` 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 IntegrationSearchOptions(
this.model.authorityOptions.scope,
this.model.authorityOptions.name,
this.model.authorityOptions.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);
}
}));
}
/**
* Check if model value has an authority
*/
public hasAuthorityValue() {
return hasValue(this.model.value)
&& typeof this.model.value === 'object'
&& 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() {
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;
}
/**
* 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) {
if (isNotEmpty(this.getCurrentValue())) {
const currentValue = new FormFieldMetadataValueObject(this.getCurrentValue());
if (!this.editMode) {
this.updateModel(currentValue);
}
} else {
this.remove();
}
}
}
/**
* Load more result entries
*/
public onScroll() {
if (!this.loading && this.pageInfo.currentPage <= this.pageInfo.totalPages) {
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.setCurrentValue('');
}
}
}
/**
* Reset the model value
*/
public remove() {
this.group.markAsPristine();
this.dispatchUpdate(null)
}
/**
* Saves all changes
*/
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();
}
/**
* Converts a stream of text values from the `<input>` element to the stream of the array of items
* to display in the result list.
*/
public search() {
this.optionsList = null;
this.updatePageInfo(this.model.maxOptions, 1);
this.loading = true;
this.subs.push(this.vocabularyService.getVocabularyEntriesByValue(
this.getCurrentValue(),
false,
this.model.vocabularyOptions,
this.pageInfo
).pipe(
getFirstSucceededRemoteDataPayload(),
catchError(() =>
observableOf(new PaginatedList(
new PageInfo(),
[]
))
),
distinctUntilChanged())
.subscribe((list: PaginatedList<VocabularyEntry>) => {
this.optionsList = list.page;
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();
this.search();
}
}
ngOnDestroy() {
this.subs
.filter((sub) => hasValue(sub))
.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((formValue: FormFieldMetadataValueObject) => this.setDisplayInputValue(formValue.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 || '';
}
this.cdr.detectChanges();
}
}
/**
* Gets the current text present in the input field(s)
*/
protected getCurrentValue(): string {
let result = '';
if (!this.isLookupName()) {
@@ -96,6 +314,9 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem
return result;
}
/**
* Clear text present in the input field(s)
*/
protected resetFields() {
this.firstInputValue = '';
if (this.isLookupName()) {
@@ -103,173 +324,12 @@ export class DsDynamicLookupComponent extends DynamicFormControlComponent implem
}
}
protected setInputsValue(value) {
if (hasValue(value)) {
let displayValue = value;
if (value instanceof FormFieldMetadataValueObject || value instanceof AuthorityValue) {
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;
}
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());
}
}

View File

@@ -20,7 +20,7 @@
</ul>
</ng-template>
<div class="position-relative right-addon">
<div *ngIf="!(isHierarchicalVocabulary() | async)" class="position-relative right-addon">
<i *ngIf="searching" class="fas fa-circle-notch fa-spin fa-2x fa-fw text-primary position-absolute mt-1 p-0" aria-hidden="true"></i>
<i *ngIf="!searching"
dsAuthorityConfidenceState
@@ -49,3 +49,20 @@
<div class="invalid-feedback" *ngIf="searchFailed">Sorry, suggestions could not be loaded.</div>
</div>
<input *ngIf="(isHierarchicalVocabulary() | async)"
class="form-control custom-select"
[attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[name]="model.name"
[placeholder]="model.placeholder"
[readonly]="model.readOnly"
[type]="model.inputType"
[value]="currentValue?.display"
(focus)="onFocus($event)"
(change)="onChange($event)"
(click)="openTree($event)"
(keydown)="$event.preventDefault()"
(keypress)="$event.preventDefault()"
(keyup)="$event.preventDefault()">

View File

@@ -16,3 +16,8 @@
color: $dropdown-link-hover-color !important;
background-color: $dropdown-link-hover-bg !important;
}
.treeview .modal-body {
max-height: 85vh !important;
overflow-y: auto;
}

View File

@@ -0,0 +1,462 @@
// 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 { 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';
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
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 { 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';
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';
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<any> = new Promise((resolve, reject) => resolve(true));
}
function init() {
ONEBOX_TEST_GROUP = new FormGroup({
onebox: new FormControl(),
});
ONEBOX_TEST_MODEL_CONFIG = {
vocabularyOptions: {
closed: false,
name: 'vocabulary'
} as VocabularyOptions,
disabled: false,
id: 'onebox',
label: 'Conference',
minChars: 3,
name: 'onebox',
placeholder: 'Conference',
readOnly: false,
required: false,
repeatable: false,
value: undefined
};
}
describe('DsDynamicOneboxComponent test suite', () => {
let scheduler: TestScheduler;
let testComp: TestComponent;
let oneboxComponent: DsDynamicOneboxComponent;
let testFixture: ComponentFixture<TestComponent>;
let oneboxCompFixture: ComponentFixture<DsDynamicOneboxComponent>;
let vocabularyServiceStub: any;
let modalService: any;
let html;
let modal;
const vocabulary = {
id: 'vocabulary',
name: 'vocabulary',
scrollable: true,
hierarchical: false,
preloadLevel: 0,
type: 'vocabulary',
_links: {
self: {
url: 'self'
},
entries: {
url: 'entries'
}
}
}
const hierarchicalVocabulary = {
id: 'hierarchicalVocabulary',
name: 'hierarchicalVocabulary',
scrollable: true,
hierarchical: true,
preloadLevel: 2,
type: 'vocabulary',
_links: {
self: {
url: 'self'
},
entries: {
url: 'entries'
}
}
}
// async beforeEach
beforeEach(() => {
vocabularyServiceStub = new VocabularyServiceStub();
modal = jasmine.createSpyObj('modal',
{
open: jasmine.createSpy('open'),
close: jasmine.createSpy('close'),
dismiss: jasmine.createSpy('dismiss'),
}
);
init();
TestBed.configureTestingModule({
imports: [
DynamicFormsCoreModule,
DynamicFormsNGBootstrapUIModule,
FormsModule,
NgbModule,
ReactiveFormsModule,
TranslateModule.forRoot(),
CdkTreeModule
],
declarations: [
DsDynamicOneboxComponent,
TestComponent,
AuthorityConfidenceStateDirective,
ObjNgFor,
VocabularyTreeviewComponent
], // declare the test component
providers: [
ChangeDetectorRef,
DsDynamicOneboxComponent,
{ provide: VocabularyService, useValue: vocabularyServiceStub },
{ provide: DynamicFormLayoutService, useValue: {} },
{ provide: DynamicFormValidationService, useValue: {} },
{ provide: NgbModal, useValue: modal }
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents();
});
describe('', () => {
// synchronous beforeEach
beforeEach(() => {
html = `
<ds-dynamic-onebox [bindId]="bindId"
[group]="group"
[model]="model"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-onebox>`;
spyOn(vocabularyServiceStub, 'findVocabularyById').and.returnValue(createSuccessfulRemoteDataObject$(vocabulary));
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
afterEach(() => {
testFixture.destroy();
});
it('should create DsDynamicOneboxComponent', inject([DsDynamicOneboxComponent], (app: DsDynamicOneboxComponent) => {
expect(app).toBeDefined();
}));
});
describe('Has not hierarchical vocabulary', () => {
beforeEach(() => {
spyOn(vocabularyServiceStub, 'findVocabularyById').and.returnValue(createSuccessfulRemoteDataObject$(vocabulary));
});
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();
});
afterEach(() => {
oneboxCompFixture.destroy();
oneboxComponent = null;
});
it('should init component properly', () => {
expect(oneboxComponent.currentValue).not.toBeDefined();
});
it('should search when 3+ characters typed', fakeAsync(() => {
spyOn((oneboxComponent as any).vocabularyService, 'getVocabularyEntriesByValue').and.callThrough();
oneboxComponent.search(observableOf('test')).subscribe();
tick(300);
oneboxCompFixture.detectChanges();
expect((oneboxComponent as any).vocabularyService.getVocabularyEntriesByValue).toHaveBeenCalled();
}));
it('should set model.value on input type when VocabularyOptions.closed is false', () => {
const inputDe = oneboxCompFixture.debugElement.query(By.css('input.form-control'));
const inputElement = inputDe.nativeElement;
inputElement.value = 'test value';
inputElement.dispatchEvent(new Event('input'));
expect(oneboxComponent.inputValue).toEqual(new FormFieldMetadataValueObject('test value'))
});
it('should not set model.value on input type when VocabularyOptions.closed is true', () => {
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(oneboxComponent.model.value).not.toBeDefined();
});
it('should emit blur Event onBlur when popup is closed', () => {
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(oneboxComponent.blur, 'emit');
spyOn(oneboxComponent.instance, 'isPopupOpen').and.returnValue(true);
const input = oneboxCompFixture.debugElement.query(By.css('input'));
input.nativeElement.blur();
expect(oneboxComponent.blur.emit).not.toHaveBeenCalled();
});
it('should emit change Event onBlur when VocabularyOptions.closed is false and inputValue is changed', () => {
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', () => {
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', () => {
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(oneboxComponent.focus, 'emit');
oneboxComponent.onFocus(new Event('focus'));
expect(oneboxComponent.focus.emit).toHaveBeenCalled();
});
});
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(), {
authority: null,
value: 'test',
display: 'testDisplay'
}));
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(() => {
oneboxCompFixture.destroy();
oneboxComponent = null;
});
it('should init component properly', fakeAsync(() => {
tick();
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', () => {
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(() => {
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((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(() => {
oneboxCompFixture.destroy();
oneboxComponent = null;
});
it('should init component properly', fakeAsync(() => {
tick();
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', () => {
oneboxComponent.currentValue = null;
spyOn(oneboxComponent.change, 'emit');
oneboxComponent.onChange(new Event('change'));
expect(oneboxComponent.change.emit).toHaveBeenCalled();
expect(oneboxComponent.model.value).toBeNull();
});
});
});
describe('Has hierarchical vocabulary', () => {
beforeEach(() => {
scheduler = getTestScheduler();
spyOn(vocabularyServiceStub, 'findVocabularyById').and.returnValue(createSuccessfulRemoteDataObject$(hierarchicalVocabulary));
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', () => {
beforeEach(() => {
oneboxComponent.group = ONEBOX_TEST_GROUP;
oneboxComponent.model = new DynamicOneboxModel(ONEBOX_TEST_MODEL_CONFIG);
oneboxCompFixture.detectChanges();
});
afterEach(() => {
oneboxCompFixture.destroy();
oneboxComponent = null;
});
it('should init component properly', fakeAsync(() => {
tick();
expect(oneboxComponent.currentValue).not.toBeDefined();
}));
it('should open tree properly', (done) => {
scheduler.schedule(() => oneboxComponent.openTree(new Event('click')));
scheduler.flush();
expect((oneboxComponent as any).modalService.open).toHaveBeenCalled();
done();
});
});
describe('when init model value is not empty', () => {
beforeEach(() => {
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((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(() => {
oneboxCompFixture.destroy();
oneboxComponent = null;
});
it('should init component properly', fakeAsync(() => {
tick();
expect(oneboxComponent.currentValue).toEqual(new FormFieldMetadataValueObject('test', null, null, 'testDisplay'));
expect((oneboxComponent as any).vocabularyService.getVocabularyEntryByValue).toHaveBeenCalled();
}));
it('should open tree properly', (done) => {
scheduler.schedule(() => oneboxComponent.openTree(new Event('click')));
scheduler.flush();
expect((oneboxComponent as any).modalService.open).toHaveBeenCalled();
done();
});
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
group: FormGroup = ONEBOX_TEST_GROUP;
model = new DynamicOneboxModel(ONEBOX_TEST_MODEL_CONFIG);
}
/* tslint:enable:max-classes-per-file */

View File

@@ -0,0 +1,278 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
import {
catchError,
debounceTime,
distinctUntilChanged,
filter,
map,
merge,
switchMap,
take,
tap
} from 'rxjs/operators';
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 { 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';
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';
import { DsDynamicVocabularyComponent } from '../dynamic-vocabulary.component';
import { Vocabulary } from '../../../../../../core/submission/vocabularies/models/vocabulary.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'],
templateUrl: './dynamic-onebox.component.html'
})
export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent implements OnInit {
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicOneboxModel;
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
@Output() change: EventEmitter<any> = new EventEmitter<any>();
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
@ViewChild('instance', { static: false }) instance: NgbTypeahead;
pageInfo: PageInfo = new PageInfo();
searching = false;
searchFailed = false;
hideSearchingWhenUnsubscribed$ = new Observable(() => () => this.changeSearchingStatus(false));
click$ = new Subject<string>();
currentValue: any;
inputValue: any;
preloadLevel: number;
private vocabulary$: Observable<Vocabulary>;
private isHierarchicalVocabulary$: Observable<boolean>;
private subs: Subscription[] = [];
constructor(protected vocabularyService: VocabularyService,
protected cdr: ChangeDetectorRef,
protected layoutService: DynamicFormLayoutService,
protected modalService: NgbModal,
protected validationService: DynamicFormValidationService
) {
super(vocabularyService, layoutService, validationService);
}
/**
* Converts an item from the result list to a `string` to display in the `<input>` field.
*/
formatter = (x: { display: string }) => {
return (typeof x === 'object') ? x.display : x
};
/**
* Converts a stream of text values from the `<input>` element to the stream of the array of items
* to display in the onebox popup.
*/
search = (text$: Observable<string>) => {
return text$.pipe(
merge(this.click$),
debounceTime(300),
distinctUntilChanged(),
tap(() => this.changeSearchingStatus(true)),
switchMap((term) => {
if (term === '' || term.length < this.model.minChars) {
return observableOf({ list: [] });
} else {
return this.vocabularyService.getVocabularyEntriesByValue(
term,
false,
this.model.vocabularyOptions,
this.pageInfo).pipe(
getFirstSucceededRemoteDataPayload(),
tap(() => this.searchFailed = false),
catchError(() => {
this.searchFailed = true;
return observableOf(new PaginatedList(
new PageInfo(),
[]
));
}));
}
}),
map((list: PaginatedList<VocabularyEntry>) => list.page),
tap(() => this.changeSearchingStatus(false)),
merge(this.hideSearchingWhenUnsubscribed$)
)
};
/**
* Initialize the component, setting up the init form value
*/
ngOnInit() {
if (this.model.value) {
this.setCurrentValue(this.model.value, true);
}
this.vocabulary$ = this.vocabularyService.findVocabularyById(this.model.vocabularyOptions.name).pipe(
getFirstSucceededRemoteDataPayload(),
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) => {
this.setCurrentValue(this.model.value);
}));
}
/**
* Changes the searching status
* @param status
*/
changeSearchingStatus(status: boolean) {
this.searching = status;
this.cdr.detectChanges();
}
/**
* Checks if configured vocabulary is Hierarchical or not
*/
isHierarchicalVocabulary(): Observable<boolean> {
return this.isHierarchicalVocabulary$;
}
/**
* Update the input value with a FormFieldMetadataValueObject
* @param event
*/
onInput(event) {
if (!this.model.vocabularyOptions.closed && isNotEmpty(event.target.value)) {
this.inputValue = new FormFieldMetadataValueObject(event.target.value);
}
}
/**
* Emits a blur event containing a given value.
* @param event The value to emit.
*/
onBlur(event: Event) {
if (!this.instance.isPopupOpen()) {
if (!this.model.vocabularyOptions.closed && isNotEmpty(this.inputValue)) {
if (isNotNull(this.inputValue) && this.model.value !== this.inputValue) {
this.dispatchUpdate(this.inputValue);
}
this.inputValue = null;
}
this.blur.emit(event);
} else {
// prevent on blur propagation if typeahed suggestions are showed
event.preventDefault();
event.stopImmediatePropagation();
// set focus on input again, this is to avoid to lose changes when no suggestion is selected
(event.target as HTMLInputElement).focus();
}
}
/**
* Updates model value with the current value
* @param event The change event.
*/
onChange(event: Event) {
event.stopPropagation();
if (isEmpty(this.currentValue)) {
this.dispatchUpdate(null);
}
}
/**
* Updates current value and model value with the selected value.
* @param event The value to set.
*/
onSelectItem(event: NgbTypeaheadSelectItemEvent) {
this.inputValue = null;
this.setCurrentValue(event.item);
this.dispatchUpdate(event.item);
}
/**
* Open modal to show tree for hierarchical vocabulary
* @param event The click event fired
*/
openTree(event) {
event.preventDefault();
event.stopImmediatePropagation();
this.subs.push(this.vocabulary$.pipe(
map((vocabulary: Vocabulary) => vocabulary.preloadLevel),
take(1)
).subscribe((preloadLevel) => {
const modalRef: NgbModalRef = this.modalService.open(VocabularyTreeviewComponent, { size: 'lg', windowClass: 'treeview' });
modalRef.componentInstance.vocabularyOptions = this.model.vocabularyOptions;
modalRef.componentInstance.preloadLevel = preloadLevel;
modalRef.componentInstance.selectedItem = this.currentValue ? this.currentValue : '';
modalRef.result.then((result: VocabularyEntryDetail) => {
if (result) {
this.currentValue = result;
this.dispatchUpdate(result);
}
}, () => {
return;
});
}))
}
/**
* Callback functions for whenClickOnConfidenceNotAccepted event
*/
public whenClickOnConfidenceNotAccepted(confidence: ConfidenceType) {
if (!this.model.readOnly) {
this.click$.next(this.formatter(this.currentValue));
}
}
/**
* Sets the current value with the given value.
* @param value The value to set.
* @param init Representing if is init value or not.
*/
setCurrentValue(value: any, init = false): void {
let result: string;
if (init) {
this.getInitValueFromModel()
.subscribe((formValue: FormFieldMetadataValueObject) => {
this.currentValue = formValue;
this.cdr.detectChanges();
});
} else {
if (isEmpty(value)) {
result = '';
} else {
result = value.value;
}
this.currentValue = result;
this.cdr.detectChanges();
}
}
ngOnDestroy(): void {
this.subs
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -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);

View File

@@ -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'
}]
@@ -129,7 +128,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]

View File

@@ -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,14 +23,16 @@ 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 { 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 { PLACEHOLDER_PARENT_METADATA } from '../../ds-dynamic-form-constants';
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'],
@@ -65,9 +57,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,
@@ -178,6 +170,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);
@@ -236,20 +234,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.findEntryDetailById(
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 {
@@ -316,10 +310,4 @@ export class DsDynamicRelationGroupComponent extends DynamicFormControlComponent
}
}
ngOnDestroy(): void {
this.subs
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -1,7 +1,8 @@
<div #sdRef="ngbDropdown" ngbDropdown class="input-group w-100">
<input #inputElement class="form-control"
<div #sdRef="ngbDropdown" ngbDropdown class="w-100">
<input ngbDropdownToggle class="form-control custom-select"
[attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[name]="model.name"
[readonly]="model.readOnly"
[type]="model.inputType"
@@ -10,11 +11,6 @@
(click)="$event.stopPropagation(); openDropdown(sdRef);"
(focus)="onFocus($event)"
(keypress)="$event.preventDefault()">
<button #buttonElement aria-describedby="collectionControlsMenuLabel"
class="ds-form-input-btn btn btn-outline-primary"
ngbDropdownToggle
[disabled]="model.readOnly"
(click)="onToggle(sdRef); $event.stopPropagation();"></button>
<div ngbDropdownMenu
class="dropdown-menu scrollable-dropdown-menu w-100"
@@ -30,7 +26,7 @@
[scrollWindow]="false">
<button class="dropdown-item disabled" *ngIf="optionsList && optionsList.length == 0">{{'form.no-results' | translate}}</button>
<button class="dropdown-item collection-item text-truncate" *ngFor="let listEntry of optionsList" (click)="onSelect(listEntry)" title="{{ listEntry.display }}">
<button class="dropdown-item collection-item text-truncate" *ngFor="let listEntry of optionsList" (click)="onSelect(listEntry); sdRef.close()" title="{{ listEntry.display }}">
{{inputFormatter(listEntry)}}
</button>
<div class="scrollable-dropdown-loading text-center" *ngIf="loading"><p>{{'form.loading' | translate}}</p></div>

View File

@@ -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,12 @@ 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,
name: 'common_iso_languages'
} as VocabularyOptions,
disabled: false,
errorMessages: {required: 'Required field.'},
errorMessages: { required: 'Required field.' },
id: 'dropdown',
label: 'Language',
maxOptions: 10,
@@ -53,7 +51,7 @@ describe('Dynamic Dynamic Scrollable Dropdown component', () => {
let html;
let modelValue;
const authorityServiceStub = new AuthorityServiceStub();
const vocabularyServiceStub = new VocabularyServiceStub();
// async beforeEach
beforeEach(async(() => {
@@ -75,9 +73,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]
});
@@ -122,15 +120,12 @@ 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', () => {
const de = scrollableDropdownFixture.debugElement.query(By.css('button.ds-form-input-btn'));
const de = scrollableDropdownFixture.debugElement.query(By.css('input.custom-select'));
const btnEl = de.nativeElement;
const deMenu = scrollableDropdownFixture.debugElement.query(By.css('div.scrollable-dropdown-menu'));
@@ -155,9 +150,9 @@ 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 de: any = scrollableDropdownFixture.debugElement.query(By.css('input.custom-select'));
let btnEl = de.nativeElement;
btnEl.click();
@@ -193,7 +188,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();
@@ -205,12 +200,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);
});
});
});

View File

@@ -2,28 +2,29 @@ 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, map, tap } 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 { 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 { isEmpty } from '../../../../../empty.util';
import { VocabularyService } from '../../../../../../core/submission/vocabularies/vocabulary.service';
import { getFirstSucceededRemoteDataPayload } from '../../../../../../core/shared/operators';
import { PaginatedList } from '../../../../../../core/data/paginated-list';
import { DsDynamicVocabularyComponent } from '../dynamic-vocabulary.component';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
/**
* Component representing a dropdown input field
*/
@Component({
selector: 'ds-dynamic-scrollable-dropdown',
styleUrls: ['./dynamic-scrollable-dropdown.component.scss'],
templateUrl: './dynamic-scrollable-dropdown.component.html'
})
export class DsDynamicScrollableDropdownComponent extends DynamicFormControlComponent implements OnInit {
export class DsDynamicScrollableDropdownComponent extends DsDynamicVocabularyComponent implements OnInit {
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicScrollableDropdownModel;
@@ -37,39 +38,38 @@ export class DsDynamicScrollableDropdownComponent extends DynamicFormControlComp
public pageInfo: PageInfo;
public optionsList: any;
protected searchOptions: IntegrationSearchOptions;
constructor(private authorityService: AuthorityService,
private cdr: ChangeDetectorRef,
constructor(protected vocabularyService: VocabularyService,
protected cdr: ChangeDetectorRef,
protected layoutService: DynamicFormLayoutService,
protected validationService: DynamicFormValidationService
) {
super(layoutService, validationService);
super(vocabularyService, layoutService, validationService);
}
/**
* Initialize the component, setting up the init form value
*/
ngOnInit() {
this.searchOptions = new IntegrationSearchOptions(
this.model.authorityOptions.scope,
this.model.authorityOptions.name,
this.model.authorityOptions.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.updatePageInfo(this.model.maxOptions, 1)
this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, this.pageInfo).pipe(
getFirstSucceededRemoteDataPayload(),
catchError(() => observableOf(new PaginatedList(
new PageInfo(),
[]
))
))
.subscribe((list: PaginatedList<VocabularyEntry>) => {
this.optionsList = list.page;
if (this.model.value) {
this.setCurrentValue(this.model.value);
this.setCurrentValue(this.model.value, true);
}
this.pageInfo = object.pageInfo;
this.updatePageInfo(
list.pageInfo.elementsPerPage,
list.pageInfo.currentPage,
list.pageInfo.totalElements,
list.pageInfo.totalPages
);
this.cdr.detectChanges();
});
@@ -77,75 +77,90 @@ export class DsDynamicScrollableDropdownComponent extends DynamicFormControlComp
.subscribe((value) => {
this.setCurrentValue(value);
});
}
inputFormatter = (x: AuthorityValue): string => x.display || x.value;
/**
* Converts an item from the result list to a `string` to display in the `<input>` field.
*/
inputFormatter = (x: VocabularyEntry): string => x.display || x.value;
/**
* Opens dropdown menu
* @param sdRef The reference of the NgbDropdown.
*/
openDropdown(sdRef: NgbDropdown) {
if (!this.model.readOnly) {
this.group.markAsUntouched();
sdRef.open();
}
}
/**
* Loads any new entries
*/
onScroll() {
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.updatePageInfo(
this.pageInfo.elementsPerPage,
this.pageInfo.currentPage + 1,
this.pageInfo.totalElements,
this.pageInfo.totalPages
);
this.vocabularyService.getVocabularyEntries(this.model.vocabularyOptions, this.pageInfo).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<VocabularyEntry>) => {
this.optionsList = this.optionsList.concat(list.page);
this.updatePageInfo(
list.pageInfo.elementsPerPage,
list.pageInfo.currentPage,
list.pageInfo.totalElements,
list.pageInfo.totalPages
);
this.cdr.detectChanges();
})
}
}
onBlur(event: Event) {
this.blur.emit(event);
}
onFocus(event) {
this.focus.emit(event);
}
/**
* Emits a change event and set the current value with the given value.
* @param event The value to emit.
*/
onSelect(event) {
this.group.markAsDirty();
this.model.valueUpdates.next(event);
this.change.emit(event);
this.dispatchUpdate(event);
this.setCurrentValue(event);
}
onToggle(sdRef: NgbDropdown) {
if (sdRef.isOpen()) {
this.focus.emit(event);
} else {
this.blur.emit(event);
}
}
/**
* Sets the current value with the given value.
* @param value The value to set.
* @param init Representing if is init value or not.
*/
setCurrentValue(value: any, init = false): void {
let result: Observable<string>;
setCurrentValue(value): void {
let result: string;
if (isUndefined(value) || isNull(value)) {
result = '';
} else if (typeof value === 'string') {
result = value;
if (init) {
result = this.getInitValueFromModel().pipe(
map((formValue: FormFieldMetadataValueObject) => formValue.display)
);
} else {
for (const item of this.optionsList) {
if (value.value === (item as any).value) {
result = this.inputFormatter(item);
break;
}
if (isEmpty(value)) {
result = observableOf('');
} else if (typeof value === 'string') {
result = observableOf(value);
} else {
result = observableOf(value.display)
}
}
this.currentValue = observableOf(result);
this.currentValue = result;
}
}

View File

@@ -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;
}

View File

@@ -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,10 @@ function init() {
});
TAG_TEST_MODEL_CONFIG = {
authorityOptions: {
vocabularyOptions: {
closed: false,
metadata: 'tag',
name: 'common_iso_languages',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as AuthorityOptions,
name: 'common_iso_languages'
} as VocabularyOptions,
disabled: false,
id: 'tag',
label: 'Keywords',
@@ -75,7 +72,7 @@ describe('DsDynamicTagComponent test suite', () => {
// async beforeEach
beforeEach(async(() => {
const authorityServiceStub = new AuthorityServiceStub();
const vocabularyServiceStub = new VocabularyServiceStub();
init();
TestBed.configureTestingModule({
imports: [
@@ -92,7 +89,7 @@ describe('DsDynamicTagComponent test suite', () => {
providers: [
ChangeDetectorRef,
DsDynamicTagComponent,
{ provide: AuthorityService, useValue: authorityServiceStub },
{ provide: VocabularyService, useValue: vocabularyServiceStub },
{ provide: DynamicFormLayoutService, useValue: {} },
{ provide: DynamicFormValidationService, useValue: {} }
],
@@ -124,7 +121,7 @@ describe('DsDynamicTagComponent test suite', () => {
}));
});
describe('when authorityOptions are set', () => {
describe('when vocabularyOptions are set', () => {
describe('and init model value is empty', () => {
beforeEach(() => {
@@ -143,25 +140,23 @@ describe('DsDynamicTagComponent test suite', () => {
it('should init component properly', () => {
chips = new Chips([], 'display');
expect(tagComp.chips.getChipsItems()).toEqual(chips.getChipsItems());
expect(tagComp.searchOptions).toBeDefined();
});
it('should search when 3+ characters typed', fakeAsync(() => {
spyOn((tagComp as any).authorityService, 'getEntriesByName').and.callThrough();
spyOn((tagComp as any).vocabularyService, 'getVocabularyEntriesByValue').and.callThrough();
tagComp.search(observableOf('test')).subscribe(() => {
expect((tagComp as any).authorityService.getEntriesByName).toHaveBeenCalled();
expect((tagComp as any).vocabularyService.getVocabularyEntriesByValue).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
}),
@@ -233,13 +228,12 @@ describe('DsDynamicTagComponent test suite', () => {
it('should init component properly', () => {
chips = new Chips(modelValue, 'display');
expect(tagComp.chips.getChipsItems()).toEqual(chips.getChipsItems());
expect(tagComp.searchOptions).toBeDefined();
});
});
});
describe('when authorityOptions are not set', () => {
describe('when vocabularyOptions are not set', () => {
describe('and init model value is empty', () => {
beforeEach(() => {
@@ -247,7 +241,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();
});
@@ -260,7 +254,6 @@ describe('DsDynamicTagComponent test suite', () => {
it('should init component properly', () => {
chips = new Chips([], 'display');
expect(tagComp.chips.getChipsItems()).toEqual(chips.getChipsItems());
expect(tagComp.searchOptions).not.toBeDefined();
});
it('should add an item on ENTER or key press is \',\' or \';\'', fakeAsync(() => {

View File

@@ -1,29 +1,33 @@
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 {
DynamicFormControlComponent,
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 { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
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 { 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';
import { DsDynamicVocabularyComponent } from '../dynamic-vocabulary.component';
/**
* Component representing a tag input field
*/
@Component({
selector: 'ds-dynamic-tag',
styleUrls: ['./dynamic-tag.component.scss'],
templateUrl: './dynamic-tag.component.html'
})
export class DsDynamicTagComponent extends DynamicFormControlComponent implements OnInit {
export class DsDynamicTagComponent extends DsDynamicVocabularyComponent implements OnInit {
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicTagModel;
@@ -32,19 +36,34 @@ export class DsDynamicTagComponent extends DynamicFormControlComponent implement
@Output() change: EventEmitter<any> = new EventEmitter<any>();
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
@ViewChild('instance', {static: false}) instance: NgbTypeahead;
@ViewChild('instance', { static: false }) instance: NgbTypeahead;
chips: Chips;
hasAuthority: boolean;
searching = false;
searchOptions: IntegrationSearchOptions;
searchFailed = false;
hideSearchingWhenUnsubscribed = new Observable(() => () => this.changeSearchingStatus(false));
currentValue: any;
public pageInfo: PageInfo;
constructor(protected vocabularyService: VocabularyService,
private cdr: ChangeDetectorRef,
protected layoutService: DynamicFormLayoutService,
protected validationService: DynamicFormValidationService
) {
super(vocabularyService, layoutService, validationService);
}
/**
* Converts an item from the result list to a `string` to display in the `<input>` field.
*/
formatter = (x: { display: string }) => x.display;
/**
* Converts a stream of text values from the `<input>` element to the stream of the array of items
* to display in the typeahead popup.
*/
search = (text$: Observable<string>) =>
text$.pipe(
debounceTime(300),
@@ -52,45 +71,29 @@ 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.getVocabularyEntriesByValue(term, false, this.model.vocabularyOptions, new PageInfo()).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<VocabularyEntry>) => 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);
}
/**
* Initialize the component, setting up the init form value
*/
ngOnInit() {
this.hasAuthority = this.model.authorityOptions && hasValue(this.model.authorityOptions.name);
if (this.hasAuthority) {
this.searchOptions = new IntegrationSearchOptions(
this.model.authorityOptions.scope,
this.model.authorityOptions.name,
this.model.authorityOptions.metadata);
}
this.hasAuthority = this.model.vocabularyOptions && hasValue(this.model.vocabularyOptions.name);
this.chips = new Chips(
this.model.value,
@@ -103,17 +106,24 @@ export class DsDynamicTagComponent extends DynamicFormControlComponent implement
const items = this.chips.getChipsItems();
// Does not emit change if model value is equal to the current value
if (!isEqual(items, this.model.value)) {
this.model.valueUpdates.next(items);
this.change.emit(event);
this.dispatchUpdate(items);
}
});
}
/**
* Changes the searching status
* @param status
*/
changeSearchingStatus(status: boolean) {
this.searching = status;
this.cdr.detectChanges();
}
/**
* Mark form group as dirty on input
* @param event
*/
onInput(event) {
if (event.data) {
this.group.markAsDirty();
@@ -121,6 +131,10 @@ export class DsDynamicTagComponent extends DynamicFormControlComponent implement
this.cdr.detectChanges();
}
/**
* Emits a blur event containing a given value and add all tags to chips.
* @param event The value to emit.
*/
onBlur(event: Event) {
if (isNotEmpty(this.currentValue) && !this.instance.isPopupOpen()) {
this.addTagsToChips();
@@ -128,10 +142,10 @@ export class DsDynamicTagComponent extends DynamicFormControlComponent implement
this.blur.emit(event);
}
onFocus(event) {
this.focus.emit(event);
}
/**
* Updates model value with the selected value and add a new tag to chips.
* @param event The value to set.
*/
onSelectItem(event: NgbTypeaheadSelectItemEvent) {
this.chips.add(event.item);
// this.group.controls[this.model.id].setValue(this.model.value);
@@ -139,25 +153,34 @@ export class DsDynamicTagComponent extends DynamicFormControlComponent implement
setTimeout(() => {
// Reset the input text after x ms, mandatory or the formatter overwrite it
this.currentValue = null;
this.setCurrentValue(null);
this.cdr.detectChanges();
}, 50);
}
updateModel(event) {
this.model.valueUpdates.next(this.chips.getChipsItems());
this.change.emit(event);
/* this.model.valueUpdates.next(this.chips.getChipsItems());
this.change.emit(event);*/
this.dispatchUpdate(this.chips.getChipsItems());
}
/**
* Add a new tag with typed text when typing 'Enter' or ',' or ';'
* @param event the keyUp event
*/
onKeyUp(event) {
if (event.keyCode === 13 || event.keyCode === 188) {
event.preventDefault();
// Key: Enter or ',' or ';'
// Key: 'Enter' or ',' or ';'
this.addTagsToChips();
event.stopPropagation();
}
}
/**
* Prevent propagation of a key event in case of return key is pressed
* @param event the key event
*/
preventEventsPropagation(event) {
event.stopPropagation();
if (event.keyCode === 13) {
@@ -165,8 +188,17 @@ export class DsDynamicTagComponent extends DynamicFormControlComponent implement
}
}
/**
* 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) {
this.currentValue = value;
}
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(',');
@@ -187,7 +219,7 @@ export class DsDynamicTagComponent extends DynamicFormControlComponent implement
// this.currentValue = '';
setTimeout(() => {
// Reset the input text after x ms, mandatory or the formatter overwrite it
this.currentValue = null;
this.setCurrentValue(null);
this.cdr.detectChanges();
}, 50);
this.updateModel(event);

View File

@@ -1,274 +0,0 @@
// 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 { By } from '@angular/platform-browser';
import { of as observableOf } from 'rxjs';
import { 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';
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 { DsDynamicTypeaheadComponent } from './dynamic-typeahead.component';
import { DynamicTypeaheadModel } from './dynamic-typeahead.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';
import { ObjNgFor } from '../../../../../utils/object-ngfor.pipe';
export let TYPEAHEAD_TEST_GROUP;
export let TYPEAHEAD_TEST_MODEL_CONFIG;
function init() {
TYPEAHEAD_TEST_GROUP = new FormGroup({
typeahead: new FormControl(),
});
TYPEAHEAD_TEST_MODEL_CONFIG = {
authorityOptions: {
closed: false,
metadata: 'typeahead',
name: 'EVENTAuthority',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as AuthorityOptions,
disabled: false,
id: 'typeahead',
label: 'Conference',
minChars: 3,
name: 'typeahead',
placeholder: 'Conference',
readOnly: false,
required: false,
repeatable: false,
value: undefined
};
}
describe('DsDynamicTypeaheadComponent test suite', () => {
let testComp: TestComponent;
let typeaheadComp: DsDynamicTypeaheadComponent;
let testFixture: ComponentFixture<TestComponent>;
let typeaheadFixture: ComponentFixture<DsDynamicTypeaheadComponent>;
let html;
// async beforeEach
beforeEach(async(() => {
const authorityServiceStub = new AuthorityServiceStub();
init();
TestBed.configureTestingModule({
imports: [
DynamicFormsCoreModule,
DynamicFormsNGBootstrapUIModule,
FormsModule,
NgbModule,
ReactiveFormsModule,
TranslateModule.forRoot()
],
declarations: [
DsDynamicTypeaheadComponent,
TestComponent,
AuthorityConfidenceStateDirective,
ObjNgFor
], // declare the test component
providers: [
ChangeDetectorRef,
DsDynamicTypeaheadComponent,
{ provide: AuthorityService, useValue: authorityServiceStub },
{ provide: DynamicFormLayoutService, useValue: {} },
{ provide: DynamicFormValidationService, useValue: {} }
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
}));
describe('', () => {
// synchronous beforeEach
beforeEach(() => {
html = `
<ds-dynamic-typeahead [bindId]="bindId"
[group]="group"
[model]="model"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-typeahead>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
afterEach(() => {
testFixture.destroy();
});
it('should create DsDynamicTypeaheadComponent', inject([DsDynamicTypeaheadComponent], (app: DsDynamicTypeaheadComponent) => {
expect(app).toBeDefined();
}));
});
describe('', () => {
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 search when 3+ characters typed', fakeAsync(() => {
spyOn((typeaheadComp as any).authorityService, 'getEntriesByName').and.callThrough();
typeaheadComp.search(observableOf('test')).subscribe();
tick(300);
typeaheadFixture.detectChanges();
expect((typeaheadComp as any).authorityService.getEntriesByName).toHaveBeenCalled();
}));
it('should set model.value on input type when AuthorityOptions.closed is false', () => {
const inputDe = typeaheadFixture.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'))
});
it('should not set model.value on input type when AuthorityOptions.closed is true', () => {
typeaheadComp.model.authorityOptions.closed = true;
typeaheadFixture.detectChanges();
const inputDe = typeaheadFixture.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();
});
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();
});
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'));
input.nativeElement.blur();
expect(typeaheadComp.blur.emit).not.toHaveBeenCalled();
});
it('should emit change Event onBlur when AuthorityOptions.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();
});
it('should not emit change Event onBlur when AuthorityOptions.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();
});
it('should not emit change Event onBlur when AuthorityOptions.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();
});
it('should emit focus Event onFocus', () => {
spyOn(typeaheadComp.focus, 'emit');
typeaheadComp.onFocus(new Event('focus'));
expect(typeaheadComp.focus.emit).toHaveBeenCalled();
});
});
describe('and 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);
(typeaheadComp.model as any).value = new FormFieldMetadataValueObject('test', null, 'test001');
typeaheadFixture.detectChanges();
});
afterEach(() => {
typeaheadFixture.destroy();
typeaheadComp = null;
});
it('should init component properly', () => {
expect(typeaheadComp.currentValue).toEqual(new FormFieldMetadataValueObject('test', null, 'test001'));
});
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();
});
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
group: FormGroup = TYPEAHEAD_TEST_GROUP;
model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG);
}

View File

@@ -1,156 +0,0 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { FormGroup } from '@angular/forms';
import {
DynamicFormControlComponent,
DynamicFormLayoutService,
DynamicFormValidationService
} from '@ng-dynamic-forms/core';
import { catchError, debounceTime, distinctUntilChanged, filter, map, merge, switchMap, tap } from 'rxjs/operators';
import { Observable, of as observableOf, Subject } from 'rxjs';
import { NgbTypeahead, NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap';
import { AuthorityService } from '../../../../../../core/integration/authority.service';
import { DynamicTypeaheadModel } from './dynamic-typeahead.model';
import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-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';
@Component({
selector: 'ds-dynamic-typeahead',
styleUrls: ['./dynamic-typeahead.component.scss'],
templateUrl: './dynamic-typeahead.component.html'
})
export class DsDynamicTypeaheadComponent extends DynamicFormControlComponent implements OnInit {
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicTypeaheadModel;
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
@Output() change: EventEmitter<any> = new EventEmitter<any>();
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
@ViewChild('instance', {static: false}) instance: NgbTypeahead;
searching = false;
searchOptions: IntegrationSearchOptions;
searchFailed = false;
hideSearchingWhenUnsubscribed$ = new Observable(() => () => this.changeSearchingStatus(false));
click$ = new Subject<string>();
currentValue: any;
inputValue: any;
formatter = (x: { display: string }) => {
return (typeof x === 'object') ? x.display : x
};
search = (text$: Observable<string>) => {
return text$.pipe(
merge(this.click$),
debounceTime(300),
distinctUntilChanged(),
tap(() => this.changeSearchingStatus(true)),
switchMap((term) => {
if (term === '' || term.length < this.model.minChars) {
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
};
}),
tap(() => this.searchFailed = false),
catchError(() => {
this.searchFailed = true;
return observableOf({list: []});
}));
}
}),
map((results) => results.list),
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.group.get(this.model.id).valueChanges.pipe(
filter((value) => this.currentValue !== value))
.subscribe((value) => {
this.currentValue = value;
});
}
changeSearchingStatus(status: boolean) {
this.searching = status;
this.cdr.detectChanges();
}
onInput(event) {
if (!this.model.authorityOptions.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 (isNotNull(this.inputValue) && this.model.value !== this.inputValue) {
this.model.valueUpdates.next(this.inputValue);
this.change.emit(this.inputValue);
}
this.inputValue = null;
}
this.blur.emit(event);
} else {
// prevent on blur propagation if typeahed suggestions are showed
event.preventDefault();
event.stopImmediatePropagation();
// set focus on input again, this is to avoid to lose changes when no suggestion is selected
(event.target as HTMLInputElement).focus();
}
}
onChange(event: Event) {
event.stopPropagation();
if (isEmpty(this.currentValue)) {
this.model.valueUpdates.next(null);
this.change.emit(null);
}
}
onFocus(event) {
this.focus.emit(event);
}
onSelectItem(event: NgbTypeaheadSelectItemEvent) {
this.inputValue = null;
this.currentValue = event.item;
this.model.valueUpdates.next(event.item);
this.change.emit(event.item);
}
public whenClickOnConfidenceNotAccepted(confidence: ConfidenceType) {
if (!this.model.readOnly) {
this.click$.next(this.formatter(this.currentValue));
}
}
}

View File

@@ -1,4 +1,4 @@
import { BehaviorSubject, never, Observable, of as observableOf } from 'rxjs';
import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
import { RelationshipEffects } from './relationship.effects';
import { async, TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';

View File

@@ -34,9 +34,9 @@ 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 { 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,11 +78,9 @@ describe('FormBuilderService test suite', () => {
]
});
const authorityOptions: AuthorityOptions = {
closed: false,
metadata: 'list',
const vocabularyOptions: VocabularyOptions = {
name: 'type_programme',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
closed: false
};
testModel = [
@@ -195,15 +193,15 @@ describe('FormBuilderService test suite', () => {
new DynamicColorPickerModel({ id: 'testColorPicker' }),
new DynamicTypeaheadModel({ id: 'testTypeahead', repeatable: false, metadataFields: [], submissionId: '1234', hasSelectableMetadata: false }),
new DynamicOneboxModel({ id: 'testOnebox', repeatable: false, metadataFields: [], submissionId: '1234', hasSelectableMetadata: false }),
new DynamicScrollableDropdownModel({ id: 'testScrollableDropdown', authorityOptions: authorityOptions, repeatable: false, metadataFields: [], submissionId: '1234', hasSelectableMetadata: false }),
new DynamicScrollableDropdownModel({ id: 'testScrollableDropdown', vocabularyOptions: vocabularyOptions, repeatable: false, metadataFields: [], submissionId: '1234', hasSelectableMetadata: false }),
new DynamicTagModel({ id: 'testTag', repeatable: false, metadataFields: [], submissionId: '1234', hasSelectableMetadata: false }),
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 +216,7 @@ describe('FormBuilderService test suite', () => {
mandatoryMessage: 'Required field!',
repeatable: false,
selectableMetadata: [{
authority: 'RPAuthority',
controlledVocabulary: 'RPAuthority',
closed: false,
metadata: 'dc.contributor.author'
}]
@@ -232,7 +230,7 @@ describe('FormBuilderService test suite', () => {
mandatory: 'false',
repeatable: false,
selectableMetadata: [{
authority: 'OUAuthority',
controlledVocabulary: 'OUAuthority',
closed: false,
metadata: 'local.contributor.affiliation'
}]
@@ -290,7 +288,7 @@ describe('FormBuilderService test suite', () => {
selectableMetadata: [
{
metadata: 'journal',
authority: 'JOURNALAuthority',
controlledVocabulary: 'JOURNALAuthority',
closed: false
}
],
@@ -370,7 +368,7 @@ describe('FormBuilderService test suite', () => {
selectableMetadata: [
{
metadata: 'conference',
authority: 'EVENTAuthority',
controlledVocabulary: 'EVENTAuthority',
closed: false
}
],
@@ -439,7 +437,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', () => {
@@ -455,7 +453,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')]
@@ -468,11 +466,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', () => {

View File

@@ -1,5 +1,5 @@
import { hasValue, isEmpty, isNotEmpty, isNotNull } from '../../../empty.util';
import { ConfidenceType } from '../../../../core/integration/models/confidence-type';
import { ConfidenceType } from '../../../../core/shared/confidence-type';
import { MetadataValueInterface, VIRTUAL_METADATA_PREFIX } from '../../../../core/shared/metadata.models';
import { PLACEHOLDER_PARENT_METADATA } from '../ds-dynamic-form-ui/ds-dynamic-form-constants';
@@ -7,6 +7,9 @@ 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;
@@ -15,7 +18,6 @@ export class FormFieldMetadataValueObject implements MetadataValueInterface {
authority: string;
confidence: ConfidenceType;
place: number;
closed: boolean;
label: string;
otherInformation: OtherInformation;
@@ -33,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;
@@ -49,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;
}

View File

@@ -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;
}

View File

@@ -12,7 +12,7 @@ describe('DateFieldParser test suite', () => {
const parserOptions: ParserOptions = {
readOnly: false,
submissionScope: null,
authorityUuid: null
collectionUUID: null
};
beforeEach(() => {

View File

@@ -12,7 +12,7 @@ describe('DisabledFieldParser test suite', () => {
const parserOptions: ParserOptions = {
readOnly: false,
submissionScope: null,
authorityUuid: null
collectionUUID: null
};
beforeEach(() => {

View File

@@ -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())

View File

@@ -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);
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.`);
}
}
}

View File

@@ -1,16 +1,20 @@
import { Inject, InjectionToken } from '@angular/core';
import { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../../empty.util';
import { FormFieldModel } from '../models/form-field.model';
import { uniqueId } from 'lodash';
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 { hasValue, 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 { 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<string> = new InjectionToken<string>('submissionId');
export const CONFIG_DATA: InjectionToken<FormFieldModel> = new InjectionToken<FormFieldModel>('configData');
@@ -105,6 +109,52 @@ export abstract class FieldParser {
}
}
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].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.metadataValue = fieldValue;
modelConfig.language = fieldValue.language;
modelConfig.place = fieldValue.place;
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])) {
@@ -148,8 +198,8 @@ export abstract class FieldParser {
fieldIds.forEach((id) => {
if (this.initFormValues.hasOwnProperty(id)) {
const valueObj: FormFieldMetadataValueObject = Object.assign(new FormFieldMetadataValueObject(), this.initFormValues[id][innerIndex]);
// Set metadata name, used for Qualdrop fields
valueObj.metadata = id;
// valueObj.value = this.initFormValues[id][innerIndex];
values.push(valueObj);
}
});
@@ -238,14 +288,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;
}
@@ -291,51 +333,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.metadataValue = fieldValue;
modelConfig.language = fieldValue.language;
modelConfig.place = fieldValue.place;
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;
}
}

View File

@@ -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
}
],

View File

@@ -1,19 +1,17 @@
import { FieldParser } from './field-parser';
import { isNotEmpty } from '../../../empty.util';
import { IntegrationSearchOptions } from '../../../../core/integration/models/integration-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;
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 +24,7 @@ export class ListFieldParser extends FieldParser {
}
});
}
this.setAuthorityOptions(listModelConfig, this.parserOptions.authorityUuid);
this.setVocabularyOptions(listModelConfig);
}
let listModel;

View File

@@ -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
}
],

View File

@@ -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.setValues(lookupModelConfig, fieldValue, true);

View File

@@ -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
}
],

View File

@@ -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.setValues(lookupModelConfig, fieldValue, true);

View File

@@ -14,7 +14,7 @@ describe('NameFieldParser test suite', () => {
const parserOptions: ParserOptions = {
readOnly: false,
submissionScope: 'testScopeUUID',
authorityUuid: null
collectionUUID: null
};
beforeEach(() => {

View File

@@ -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';
@@ -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
}
],
@@ -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);
});
});

View File

@@ -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 {
@@ -75,12 +75,12 @@ export class OneboxFieldParser extends FieldParser {
inputSelectGroup.group.push(new DsDynamicInputModel(inputModelConfig, clsInput));
return new DynamicQualdropModel(inputSelectGroup, clsGroup);
} else if (this.configData.selectableMetadata[0].authority) {
const typeaheadModelConfig: DsDynamicTypeaheadModelConfig = this.initModel(null, label);
this.setAuthorityOptions(typeaheadModelConfig, this.parserOptions.authorityUuid);
this.setValues(typeaheadModelConfig, fieldValue, true);
} else if (this.configData.selectableMetadata[0].controlledVocabulary) {
const oneboxModelConfig: DsDynamicOneboxModelConfig = this.initModel(null, label);
this.setVocabularyOptions(oneboxModelConfig);
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);

View File

@@ -1,5 +1,5 @@
export interface ParserOptions {
readOnly: boolean;
submissionScope: string;
authorityUuid: string
collectionUUID: string
}

View File

@@ -12,7 +12,7 @@ describe('RelationGroupFieldParser test suite', () => {
const parserOptions: ParserOptions = {
readOnly: false,
submissionScope: 'testScopeUUID',
authorityUuid: 'WORKSPACE'
collectionUUID: 'WORKSPACE'
};
beforeEach(() => {

View File

@@ -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;

View File

@@ -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
}
],

View File

@@ -1,21 +1,12 @@
import { Injectable, Injector } from '@angular/core';
import {
DYNAMIC_FORM_CONTROL_TYPE_ARRAY,
DynamicFormGroupModelConfig
} from '@ng-dynamic-forms/core';
import { DYNAMIC_FORM_CONTROL_TYPE_ARRAY, DynamicFormGroupModelConfig } from '@ng-dynamic-forms/core';
import { uniqueId } from 'lodash';
import { IntegrationSearchOptions } from '../../../../core/integration/models/integration-options.model';
import { isEmpty } from '../../../empty.util';
import { DynamicRowGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-group-model';
import { FormFieldModel } from '../models/form-field.model';
import {
CONFIG_DATA,
FieldParser,
INIT_FORM_VALUES,
PARSER_OPTIONS,
SUBMISSION_ID
} from './field-parser';
import { CONFIG_DATA, FieldParser, INIT_FORM_VALUES, PARSER_OPTIONS, SUBMISSION_ID } from './field-parser';
import { ParserFactory } from './parser-factory';
import { ParserOptions } from './parser-options';
import { ParserType } from './parser-type';
@@ -48,8 +39,6 @@ export class RowParser {
group: [],
};
const authorityOptions = new IntegrationSearchOptions(scopeUUID);
const scopedFields: FormFieldModel[] = this.filterScopedFields(rowData.fields, submissionScope);
const layoutDefaultGridClass = ' col-sm-' + Math.trunc(12 / scopedFields.length);
@@ -58,7 +47,7 @@ export class RowParser {
const parserOptions: ParserOptions = {
readOnly: readOnly,
submissionScope: submissionScope,
authorityUuid: authorityOptions.uuid
collectionUUID: scopeUUID
};
// Iterate over row's fields

Some files were not shown because too many files have changed in this diff Show More