Merge branch 'master' into search-features

Conflicts:
	package.json
	src/app/shared/shared.module.ts
	yarn.lock
This commit is contained in:
lotte
2018-08-02 14:21:28 +02:00
158 changed files with 13203 additions and 53 deletions

View File

@@ -3,6 +3,7 @@ import * as fromRouter from '@ngrx/router-store';
import { headerReducer, HeaderState } from './header/header.reducer';
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
import { formReducer, FormState } from './shared/form/form.reducer';
import {
SearchSidebarState,
sidebarReducer
@@ -18,6 +19,7 @@ export interface AppState {
router: fromRouter.RouterReducerState;
hostWindow: HostWindowState;
header: HeaderState;
forms: FormState;
notifications: NotificationsState;
searchSidebar: SearchSidebarState;
searchFilter: SearchFiltersState;
@@ -28,6 +30,7 @@ export const appReducers: ActionReducerMap<AppState> = {
router: fromRouter.routerReducer,
hostWindow: hostWindowReducer,
header: headerReducer,
forms: formReducer,
notifications: notificationsReducer,
searchSidebar: sidebarReducer,
searchFilter: filterReducer,

View File

@@ -4,6 +4,7 @@ import { PageInfo } from '../shared/page-info.model';
import { ConfigObject } from '../shared/config/config.model';
import { FacetValue } from '../../+search-page/search-service/facet-value.model';
import { SearchFilterConfig } from '../../+search-page/search-service/search-filter-config.model';
import { IntegrationModel } from '../integration/models/integration.model';
import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model';
import { MetadataSchema } from '../metadata/metadataschema.model';
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';
@@ -13,10 +14,12 @@ import { AuthStatus } from '../auth/models/auth-status.model';
/* tslint:disable:max-classes-per-file */
export class RestResponse {
public toCache = true;
constructor(
public isSuccessful: boolean,
public statusCode: string,
) { }
) {
}
}
export class DSOSuccessResponse extends RestResponse {
@@ -154,6 +157,7 @@ export class ConfigSuccessResponse extends RestResponse {
export class AuthStatusResponse extends RestResponse {
public toCache = false;
constructor(
public response: AuthStatus,
public statusCode: string
@@ -162,4 +166,14 @@ export class AuthStatusResponse extends RestResponse {
}
}
export class IntegrationSuccessResponse extends RestResponse {
constructor(
public dataDefinition: IntegrationModel[],
public statusCode: string,
public pageInfo?: PageInfo
) {
super(true, statusCode);
}
}
/* tslint:enable:max-classes-per-file */

View File

@@ -8,6 +8,7 @@ import { CommonModule } from '@angular/common';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
import { coreEffects } from './core.effects';
import { coreReducers } from './core.reducers';
@@ -23,6 +24,8 @@ import { DSOResponseParsingService } from './data/dso-response-parsing.service';
import { PIDService } from './data/pid.service';
import { SearchResponseParsingService } from './data/search-response-parsing.service';
import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service';
import { FormBuilderService } from '../shared/form/builder/form-builder.service';
import { FormService } from '../shared/form/form.service';
import { HostWindowService } from '../shared/host-window.service';
import { ItemDataService } from './data/item-data.service';
import { MetadataService } from './metadata/metadata.service';
@@ -41,6 +44,8 @@ import { RouteService } from '../shared/services/route.service';
import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service';
import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
import { SubmissionSectionsConfigService } from './config/submission-sections-config.service';
import { AuthorityService } from './integration/authority.service';
import { IntegrationResponseParsingService } from './integration/integration-response-parsing.service';
import { UUIDService } from './shared/uuid.service';
import { AuthenticatedGuard } from './auth/authenticated.guard';
import { AuthRequestService } from './auth/auth-request.service';
@@ -57,6 +62,7 @@ import { MetadataschemaParsingService } from './data/metadataschema-parsing.serv
import { RegistryMetadatafieldsResponseParsingService } from './data/registry-metadatafields-response-parsing.service';
import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service';
import { NotificationsService } from '../shared/notifications/notifications.service';
import { UploaderService } from '../shared/uploader/uploader.service';
const IMPORTS = [
CommonModule,
@@ -81,6 +87,11 @@ const PROVIDERS = [
CollectionDataService,
DSOResponseParsingService,
DSpaceRESTv2Service,
DynamicFormLayoutService,
DynamicFormService,
DynamicFormValidationService,
FormBuilderService,
FormService,
HALEndpointService,
HostWindowService,
ItemDataService,
@@ -110,6 +121,9 @@ const PROVIDERS = [
SubmissionDefinitionsConfigService,
SubmissionFormsConfigService,
SubmissionSectionsConfigService,
AuthorityService,
IntegrationResponseParsingService,
UploaderService,
UUIDService,
PIDService,
// register AuthInterceptor as HttpInterceptor

View File

@@ -27,7 +27,7 @@ export class ConfigResponseParsingService extends BaseResponseParsingService imp
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && data.statusCode === '200') {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '201' || data.statusCode === '200' || data.statusCode === 'OK')) {
const configDefinition = this.process<ConfigObject,ConfigType>(data.payload, request.href);
return new ConfigSuccessResponse(configDefinition, data.statusCode, this.processPageInfo(data.payload));
} else {

View File

@@ -1,7 +1,7 @@
import { Inject, Injectable, Injector } from '@angular/core';
import { Request } from '@angular/http';
import { RequestArgs } from '@angular/http/src/interfaces';
import { Actions, Effect } from '@ngrx/effects';
import { Actions, Effect, ofType } from '@ngrx/effects';
// tslint:disable-next-line:import-blacklist
import { Observable } from 'rxjs';
@@ -18,32 +18,40 @@ import { RequestEntry } from './request.reducer';
import { RequestService } from './request.service';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { NormalizedObjectFactory } from '../cache/models/normalized-object-factory';
import { catchError, flatMap, map, take, tap } from 'rxjs/operators';
export const addToResponseCacheAndCompleteAction = (request: RestRequest, responseCache: ResponseCacheService, envConfig: GlobalConfig) =>
(source: Observable<ErrorResponse>): Observable<RequestCompleteAction> =>
source.pipe(
tap((response: RestResponse) => responseCache.add(request.href, response, envConfig.cache.msToLive)),
map((response: RestResponse) => new RequestCompleteAction(request.uuid))
);
@Injectable()
export class RequestEffects {
@Effect() execute = this.actions$
.ofType(RequestActionTypes.EXECUTE)
.flatMap((action: RequestExecuteAction) => {
return this.requestService.getByUUID(action.payload)
.take(1);
})
.map((entry: RequestEntry) => entry.request)
.flatMap((request: RestRequest) => {
@Effect() execute = this.actions$.ofType(RequestActionTypes.EXECUTE).pipe(
flatMap((action: RequestExecuteAction) => {
return this.requestService.getByUUID(action.payload).pipe(
take(1)
);
}),
map((entry: RequestEntry) => entry.request),
flatMap((request: RestRequest) => {
let body;
if (isNotEmpty(request.body)) {
const serializer = new DSpaceRESTv2Serializer(NormalizedObjectFactory.getConstructor(request.body.type));
body = serializer.serialize(request.body);
}
return this.restApi.request(request.method, request.href, body, request.options)
.map((data: DSpaceRESTV2Response) =>
this.injector.get(request.getResponseParser()).parse(request, data))
.do((response: RestResponse) => this.responseCache.add(request.href, response, this.EnvConfig.cache.msToLive))
.map((response: RestResponse) => new RequestCompleteAction(request.uuid))
.catch((error: RequestError) => Observable.of(new ErrorResponse(error))
.do((response: RestResponse) => this.responseCache.add(request.href, response, this.EnvConfig.cache.msToLive))
.map((response: RestResponse) => new RequestCompleteAction(request.uuid)));
});
return this.restApi.request(request.method, request.href, body, request.options).pipe(
map((data: DSpaceRESTV2Response) => this.injector.get(request.getResponseParser()).parse(request, data)),
addToResponseCacheAndCompleteAction(request, this.responseCache, this.EnvConfig),
catchError((error: RequestError) => Observable.of(new ErrorResponse(error)).pipe(
addToResponseCacheAndCompleteAction(request, this.responseCache, this.EnvConfig)
))
);
})
);
constructor(
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig,

View File

@@ -11,6 +11,7 @@ import { ConfigResponseParsingService } from './config-response-parsing.service'
import { AuthResponseParsingService } from '../auth/auth-response-parsing.service';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { HttpHeaders } from '@angular/common/http';
import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service';
/* tslint:disable:max-classes-per-file */
@@ -212,6 +213,15 @@ export class AuthGetRequest extends GetRequest {
}
}
export class IntegrationRequest extends GetRequest {
constructor(uuid: string, href: string) {
super(uuid, href);
}
getResponseParser(): GenericConstructor<ResponseParsingService> {
return IntegrationResponseParsingService;
}
}
export class RequestError extends Error {
statusText: string;
}

View File

@@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import { ResponseCacheService } from '../cache/response-cache.service';
import { RequestService } from '../data/request.service';
import { IntegrationService } from './integration.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
@Injectable()
export class AuthorityService extends IntegrationService {
protected linkPath = 'authorities';
protected browseEndpoint = 'entries';
constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected halService: HALEndpointService) {
super();
}
}

View File

@@ -0,0 +1,12 @@
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

@@ -0,0 +1,17 @@
import { GenericConstructor } from '../shared/generic-constructor';
import { IntegrationType } from './intergration-type';
import { AuthorityValueModel } from './models/authority-value.model';
import { IntegrationModel } from './models/integration.model';
export class IntegrationObjectFactory {
public static getConstructor(type): GenericConstructor<IntegrationModel> {
switch (type) {
case IntegrationType.Authority: {
return AuthorityValueModel;
}
default: {
return undefined;
}
}
}
}

View File

@@ -0,0 +1,196 @@
import { ErrorResponse, IntegrationSuccessResponse } from '../cache/response-cache.models';
import { ObjectCacheService } from '../cache/object-cache.service';
import { GlobalConfig } from '../../../config/global-config.interface';
import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { IntegrationResponseParsingService } from './integration-response-parsing.service';
import { IntegrationRequest } from '../data/request.models';
import { AuthorityValueModel } from './models/authority-value.model';
describe('IntegrationResponseParsingService', () => {
let service: IntegrationResponseParsingService;
const EnvConfig = {} as GlobalConfig;
const store = {} as Store<CoreState>;
const objectCacheService = new ObjectCacheService(store);
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}`;
beforeEach(() => {
service = new IntegrationResponseParsingService(EnvConfig, objectCacheService);
});
describe('parse', () => {
const validRequest = new IntegrationRequest('69f375b5-19f4-4453-8c7a-7dc5c55aafbb', entriesEndpoint);
const 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: 'https://rest.api/integration/authorities/type/entries'
}
},
statusCode: '200'
};
const invalidResponse1 = {
payload: {},
statusCode: '200'
};
const 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: '200'
};
const definitions = [
Object.assign(new AuthorityValueModel(), {
display: 'One',
id: 'One',
otherInformation: {},
value: 'One'
}),
Object.assign(new AuthorityValueModel(), {
display: 'Two',
id: 'Two',
otherInformation: {},
value: 'Two'
}),
Object.assign(new AuthorityValueModel(), {
display: 'Three',
id: 'Three',
otherInformation: {},
value: 'Three'
}),
Object.assign(new AuthorityValueModel(), {
display: 'Four',
id: 'Four',
otherInformation: {},
value: 'Four'
}),
Object.assign(new AuthorityValueModel(), {
display: 'Five',
id: 'Five',
otherInformation: {},
value: 'Five'
})
];
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

@@ -0,0 +1,47 @@
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-cache.models';
import { isNotEmpty } from '../../shared/empty.util';
import { IntegrationObjectFactory } from './integration-object-factory';
import { BaseResponseParsingService } from '../data/base-response-parsing.service';
import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface';
import { ObjectCacheService } from '../cache/object-cache.service';
import { IntegrationModel } from './models/integration.model';
import { IntegrationType } from './intergration-type';
@Injectable()
export class IntegrationResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
protected objectFactory = IntegrationObjectFactory;
protected toCache = false;
constructor(
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
protected objectCache: ObjectCacheService,
) {
super();
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) {
const dataDefinition = this.process<IntegrationModel,IntegrationType>(data.payload, request.href);
return new IntegrationSuccessResponse(dataDefinition[Object.keys(dataDefinition)[0]], data.statusCode, this.processPageInfo(data.payload.page));
} else {
return new ErrorResponse(
Object.assign(
new Error('Unexpected response from Integration endpoint'),
{statusText: data.statusCode}
)
);
}
}
}

View File

@@ -0,0 +1,83 @@
import { cold, getTestScheduler } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/Rx';
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { ResponseCacheService } from '../cache/response-cache.service';
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';
const LINK_NAME = 'authorities';
const BROWSE = 'entries';
class TestService extends IntegrationService {
protected linkPath = LINK_NAME;
protected browseEndpoint = BROWSE;
constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected halService: HALEndpointService) {
super();
}
}
describe('IntegrationService', () => {
let scheduler: TestScheduler;
let service: TestService;
let responseCache: ResponseCacheService;
let requestService: RequestService;
let halService: any;
let findOptions: IntegrationSearchOptions;
const name = 'type';
const metadata = 'dc.type';
const query = '';
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}`;
findOptions = new IntegrationSearchOptions(uuid, name, metadata);
function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService {
return jasmine.createSpyObj('responseCache', {
get: cold('c-', {
c: {response: {isSuccessful}}
})
});
}
function initTestService(): TestService {
return new TestService(
responseCache,
requestService,
halService
);
}
beforeEach(() => {
responseCache = initMockResponseCacheService(true);
requestService = getMockRequestService();
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);
});
});
});

View File

@@ -0,0 +1,85 @@
import { Observable } from 'rxjs/Observable';
import { RequestService } from '../data/request.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { ErrorResponse, IntegrationSuccessResponse, RestResponse } from '../cache/response-cache.models';
import { GetRequest, IntegrationRequest } from '../data/request.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
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';
export abstract class IntegrationService {
protected request: IntegrationRequest;
protected abstract responseCache: ResponseCacheService;
protected abstract requestService: RequestService;
protected abstract linkPath: string;
protected abstract browseEndpoint: string;
protected abstract halService: HALEndpointService;
protected getData(request: GetRequest): Observable<IntegrationData> {
const [successResponse, errorResponse] = this.responseCache.get(request.href)
.map((entry: ResponseCacheEntry) => entry.response)
.partition((response: RestResponse) => response.isSuccessful);
return Observable.merge(
errorResponse.flatMap((response: ErrorResponse) =>
Observable.throw(new Error(`Couldn't retrieve the integration data`))),
successResponse
.filter((response: IntegrationSuccessResponse) => isNotEmpty(response))
.map((response: IntegrationSuccessResponse) => new IntegrationData(response.pageInfo, response.dataDefinition))
.distinctUntilChanged());
}
protected getIntegrationHref(endpoint, options: IntegrationSearchOptions = new IntegrationSearchOptions()): string {
let result;
const args = [];
if (hasValue(options.name)) {
result = `${endpoint}/${options.name}/${this.browseEndpoint}`;
} 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;
}
public getEntriesByName(options: IntegrationSearchOptions): Observable<IntegrationData> {
return this.halService.getEndpoint(this.linkPath)
.map((endpoint: string) => this.getIntegrationHref(endpoint, options))
.filter((href: string) => isNotEmpty(href))
.distinctUntilChanged()
.map((endpointURL: string) => new IntegrationRequest(this.requestService.generateRequestId(), endpointURL))
.do((request: GetRequest) => this.requestService.configure(request))
.flatMap((request: GetRequest) => this.getData(request))
.distinctUntilChanged();
}
}

View File

@@ -0,0 +1,4 @@
export enum IntegrationType {
Authority = 'authority'
}

View File

@@ -0,0 +1,16 @@
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

@@ -0,0 +1,20 @@
import { IntegrationModel } from './integration.model';
import { autoserialize } from 'cerialize';
export class AuthorityValueModel extends IntegrationModel {
@autoserialize
id: string;
@autoserialize
display: string;
@autoserialize
value: string;
@autoserialize
otherInformation: any;
@autoserialize
language: string;
}

View File

@@ -0,0 +1,14 @@
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

@@ -0,0 +1,12 @@
import { autoserialize } from 'cerialize';
export abstract class IntegrationModel {
@autoserialize
public type: string;
@autoserialize
public _links: {
[name: string]: string
}
}

View File

@@ -0,0 +1,23 @@
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { ConfigObject } from './config.model';
import { SubmissionSectionModel } from './config-submission-section.model';
@inheritSerialization(ConfigObject)
export class ConfigAuthorityModel extends ConfigObject {
@autoserialize
id: string;
@autoserialize
display: string;
@autoserialize
value: string;
@autoserialize
otherInformation: any;
@autoserialize
language: string;
}

View File

@@ -6,6 +6,7 @@ import { SubmissionFormsModel } from './config-submission-forms.model';
import { SubmissionDefinitionsModel } from './config-submission-definitions.model';
import { ConfigType } from './config-type';
import { ConfigObject } from './config.model';
import { ConfigAuthorityModel } from './config-authority.model';
export class ConfigObjectFactory {
public static getConstructor(type): GenericConstructor<ConfigObject> {
@@ -22,6 +23,9 @@ export class ConfigObjectFactory {
case ConfigType.SubmissionSections: {
return SubmissionSectionModel
}
case ConfigType.Authority: {
return ConfigAuthorityModel
}
default: {
return undefined;
}

View File

@@ -1,10 +1,14 @@
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { autoserialize, inheritSerialization } from 'cerialize';
import { ConfigObject } from './config.model';
import { FormFieldModel } from '../../../shared/form/builder/models/form-field.model';
export interface FormRowModel {
fields: FormFieldModel[];
}
@inheritSerialization(ConfigObject)
export class SubmissionFormsModel extends ConfigObject {
@autoserialize
fields: any[];
rows: FormRowModel[];
}

View File

@@ -10,5 +10,6 @@ export enum ConfigType {
SubmissionForm = 'submissionform',
SubmissionForms = 'submissionforms',
SubmissionSections = 'submissionsections',
SubmissionSection = 'submissionsection'
SubmissionSection = 'submissionsection',
Authority = 'authority'
}

View File

@@ -0,0 +1,13 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
export const shrinkInOut = trigger('shrinkInOut', [
state('in', style({height: '100%', opacity: 1})),
transition('* => void', [
style({height: '!', opacity: 1}),
animate(200, style({height: 0, opacity: 0}))
]),
transition('void => *', [
style({height: 0, opacity: 0}),
animate(200, style({height: '*', opacity: 1}))
])
]);

View File

@@ -0,0 +1,34 @@
<div [className]="'float-left w-100 ' + wrapperClass">
<ul class="nav nav-pills d-flex flex-column flex-sm-row" [sortablejs]="chips.getChips()" [sortablejsOptions]="options">
<ng-container *ngFor="let c of chips.getChips(); let i = index">
<ng-template #tipContent>{{tipText}}</ng-template>
<li class="nav-item mr-2 mb-1"
(dragstart)="onDragStart(i)"
(dragend)="onDragEnd(i)">
<a class="flex-sm-fill text-sm-center nav-link active"
href="#"
[ngClass]="{'chip-selected disabled': (editable && c.editMode) || dragged == i}"
(click)="chipsSelected($event, i);">
<span>
<ng-container *ngIf="c.hasIcons()">
<i *ngFor="let icon of c.icons; let l = last"
[ngbTooltip]="tipContent"
triggers="manual"
#t="ngbTooltip"
class="fa {{icon.style}}"
[class.mr-1]="!l"
[class.mr-2]="l"
aria-hidden="true"
(dragstart)="tooltip.close();"
(mouseover)="showTooltip(t, i, icon.metadata)"
(mouseout)="t.close()"></i>
</ng-container>
<p class="chip-label text-truncate d-table-cell">{{c.display}}</p><i class="fa fa-times ml-2" (click)="removeChips($event, i)"></i>
</span>
</a>
</li>
</ng-container>
<ng-content></ng-content>
</ul>
</div>

View File

@@ -0,0 +1,9 @@
@import "../../../styles/variables";
.chip-selected {
background-color: map-get($theme-colors, info) !important;
}
.chip-label {
max-width: 10rem;
}

View File

@@ -0,0 +1,227 @@
// Load the implementations that should be tested
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick, } from '@angular/core/testing';
import 'rxjs/add/observable/of';
import { Chips } from './models/chips.model';
import { UploaderService } from '../uploader/uploader.service';
import { ChipsComponent } from './chips.component';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { SortablejsModule } from 'angular-sortablejs';
import { By } from '@angular/platform-browser';
import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model';
import { createTestComponent, hasClass } from '../testing/utils';
describe('ChipsComponent test suite', () => {
let testComp: TestComponent;
let chipsComp: ChipsComponent;
let testFixture: ComponentFixture<TestComponent>;
let chipsFixture: ComponentFixture<ChipsComponent>;
let html;
let chips: Chips;
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
NgbModule.forRoot(),
SortablejsModule.forRoot({animation: 150}),
],
declarations: [
ChipsComponent,
TestComponent,
], // declare the test component
providers: [
ChangeDetectorRef,
ChipsComponent,
UploaderService
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
}));
describe('', () => {
// synchronous beforeEach
beforeEach(() => {
html = `
<ds-chips
*ngIf="chips.hasItems()"
[chips]="chips"
[editable]="editable"
(selected)="onChipSelected($event)"></ds-chips>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
it('should create Chips Component', inject([ChipsComponent], (app: ChipsComponent) => {
expect(app).toBeDefined();
}));
});
describe('when has items as string', () => {
beforeEach(() => {
chips = new Chips(['a', 'b', 'c']);
chipsFixture = TestBed.createComponent(ChipsComponent);
chipsComp = chipsFixture.componentInstance; // TruncatableComponent test instance
chipsComp.editable = true;
chipsComp.chips = chips;
chipsFixture.detectChanges();
});
afterEach(() => {
chipsFixture.destroy();
chipsComp = null;
});
it('should set edit mode when a chip item is selected', fakeAsync(() => {
spyOn(chipsComp.selected, 'emit');
chipsComp.chipsSelected(new Event('click'), 1);
chipsFixture.detectChanges();
tick();
const item = chipsComp.chips.getChipByIndex(1);
expect(item.editMode).toBe(true);
expect(chipsComp.selected.emit).toHaveBeenCalledWith(1);
}));
it('should not set edit mode when a chip item is selected and editable is false', fakeAsync(() => {
chipsComp.editable = false;
spyOn(chipsComp.selected, 'emit');
chipsComp.chipsSelected(new Event('click'), 1);
chipsFixture.detectChanges();
tick();
const item = chipsComp.chips.getChipByIndex(1);
expect(item.editMode).toBe(false);
expect(chipsComp.selected.emit).not.toHaveBeenCalledWith(1);
}));
it('should emit when a chip item is removed and editable is true', fakeAsync(() => {
spyOn(chipsComp.chips, 'remove');
const item = chipsComp.chips.getChipByIndex(1);
chipsComp.removeChips(new Event('click'), 1);
chipsFixture.detectChanges();
tick();
expect(chipsComp.chips.remove).toHaveBeenCalledWith(item);
}));
it('should save chips item index when drag and drop start', fakeAsync(() => {
const de = chipsFixture.debugElement.query(By.css('li.nav-item'));
de.triggerEventHandler('dragstart', null);
expect(chipsComp.dragged).toBe(0);
}));
it('should update chips item order when drag and drop end', fakeAsync(() => {
spyOn(chipsComp.chips, 'updateOrder');
const de = chipsFixture.debugElement.query(By.css('li.nav-item'));
de.triggerEventHandler('dragend', null);
expect(chipsComp.dragged).toBe(-1);
expect(chipsComp.chips.updateOrder).toHaveBeenCalled();
}));
});
describe('when has items as object', () => {
beforeEach(() => {
const item = {
mainField: new FormFieldMetadataValueObject('main test', null, 'test001'),
relatedField: new FormFieldMetadataValueObject('related test', null, 'test002'),
otherRelatedField: new FormFieldMetadataValueObject('other related test')
};
const iconsConfig = [
{
name: 'mainField',
config: {
withAuthority:{
style: 'fa-user'
}
}
},
{
name: 'relatedField',
config: {
withAuthority:{
style: 'fa-user-alt'
},
withoutAuthority:{
style: 'fa-user-alt text-muted'
}
}
},
{
name: 'otherRelatedField',
config: {
withAuthority:{
style: 'fa-user-alt'
},
withoutAuthority:{
style: 'fa-user-alt text-muted'
}
}
},
{
name: 'default',
config: {}
}
];
chips = new Chips([item], 'display', 'mainField', iconsConfig);
chipsFixture = TestBed.createComponent(ChipsComponent);
chipsComp = chipsFixture.componentInstance; // TruncatableComponent test instance
chipsComp.editable = true;
chipsComp.chips = chips;
chipsFixture.detectChanges();
});
it('should show icon for every field that has a configured icon', () => {
const de = chipsFixture.debugElement.query(By.css('li.nav-item'));
const icons = de.queryAll(By.css('i.fa'));
expect(icons.length).toBe(4);
});
it('should has text-muted on icon style when field value had not authority', () => {
const de = chipsFixture.debugElement.query(By.css('li.nav-item'));
const icons = de.queryAll(By.css('i.fa'));
expect(hasClass(icons[2].nativeElement, 'text-muted')).toBeTruthy();
});
it('should show tooltip on mouse over an icon', () => {
const de = chipsFixture.debugElement.query(By.css('li.nav-item'));
const icons = de.queryAll(By.css('i.fa'));
icons[0].triggerEventHandler('mouseover', null);
expect(chipsComp.tipText).toBe('main test')
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
public chips = new Chips(['a', 'b', 'c']);
public editable = true;
}

View File

@@ -0,0 +1,95 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, } from '@angular/core';
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
import { SortablejsOptions } from 'angular-sortablejs';
import { isObject } from 'lodash';
import { Chips } from './models/chips.model';
import { ChipsItem } from './models/chips-item.model';
import { UploaderService } from '../uploader/uploader.service';
@Component({
selector: 'ds-chips',
styleUrls: ['./chips.component.scss'],
templateUrl: './chips.component.html',
})
export class ChipsComponent implements OnChanges {
@Input() chips: Chips;
@Input() wrapperClass: string;
@Input() editable = true;
@Output() selected: EventEmitter<number> = new EventEmitter<number>();
@Output() remove: EventEmitter<number> = new EventEmitter<number>();
@Output() change: EventEmitter<any> = new EventEmitter<any>();
options: SortablejsOptions;
dragged = -1;
tipText: string;
constructor(private cdr: ChangeDetectorRef, private uploaderService: UploaderService) {
this.options = {
animation: 300,
chosenClass: 'm-0',
dragClass: 'm-0',
filter: '.chips-sort-ignore',
ghostClass: 'm-0'
};
}
ngOnChanges(changes: SimpleChanges) {
if (changes.chips && !changes.chips.isFirstChange()) {
this.chips = changes.chips.currentValue;
}
}
chipsSelected(event: Event, index: number) {
event.preventDefault();
if (this.editable) {
this.chips.getChips().forEach((item: ChipsItem, i: number) => {
if (i === index) {
item.setEditMode();
} else {
item.unsetEditMode();
}
});
this.selected.emit(index);
}
}
removeChips(event: Event, index: number) {
event.preventDefault();
event.stopPropagation();
// Can't remove if this element is in editMode
if (!this.chips.getChipByIndex(index).editMode) {
this.chips.remove(this.chips.getChipByIndex(index));
}
}
onDragStart(index) {
this.uploaderService.overrideDragOverPage();
this.dragged = index;
}
onDragEnd(event) {
this.uploaderService.allowDragOverPage();
this.dragged = -1;
this.chips.updateOrder();
}
showTooltip(tooltip: NgbTooltip, index, field?) {
tooltip.close();
const item = this.chips.getChipByIndex(index);
if (!item.editMode && this.dragged === -1) {
if (field) {
this.tipText = (isObject(item.item[field])) ? item.item[field].display : item.item[field];
} else {
this.tipText = item.display;
}
this.cdr.detectChanges();
tooltip.open();
}
}
}

View File

@@ -0,0 +1,78 @@
import { ChipsItem, ChipsItemIcon } from './chips-item.model';
import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model';
describe('ChipsItem model test suite', () => {
let item: ChipsItem;
beforeEach(() => {
item = new ChipsItem('a');
});
it('should init ChipsItem object properly', () => {
expect(item.item).toBe('a');
expect(item.display).toBe('a');
expect(item.editMode).toBe(false);
expect(item.icons).toEqual([]);
});
it('should update item', () => {
item.updateItem('b');
expect(item.item).toBe('b');
});
it('should set editMode', () => {
item.setEditMode();
expect(item.editMode).toBe(true);
});
it('should unset editMode', () => {
item.unsetEditMode();
expect(item.editMode).toBe(false);
});
it('should update icons', () => {
const icons: ChipsItemIcon[] = [{metadata: 'test', hasAuthority: false, style: 'fa fa-plus'}];
item.updateIcons(icons);
expect(item.icons).toEqual(icons);
});
it('should return true if has icons', () => {
const icons: ChipsItemIcon[] = [{metadata: 'test', hasAuthority: false, style: 'fa fa-plus'}];
item.updateIcons(icons);
const hasIcons = item.hasIcons();
expect(hasIcons).toBe(true);
});
it('should return false if has not icons', () => {
const hasIcons = item.hasIcons();
expect(hasIcons).toBe(false);
});
it('should set display property with a different fieldToDisplay', () => {
item = new ChipsItem(
{
label: 'A',
value: 'a'
},
'label');
expect(item.display).toBe('A');
});
it('should set display property with a different objToDisplay', () => {
item = new ChipsItem(
{
toDisplay: new FormFieldMetadataValueObject('a', null, 'a'),
otherProperty: 'other'
},
'value', 'toDisplay');
expect(item.display).toBe('a');
});
});

View File

@@ -0,0 +1,72 @@
import { uniqueId, isObject } from 'lodash';
import { isNotEmpty } from '../../empty.util';
export interface ChipsItemIcon {
metadata: string;
hasAuthority: boolean;
style: string;
tooltip?: any;
}
export class ChipsItem {
public id: string;
public display: string;
public item: any;
public editMode?: boolean;
public icons?: ChipsItemIcon[];
private fieldToDisplay: string;
private objToDisplay: string;
constructor(item: any,
fieldToDisplay: string = 'display',
objToDisplay?: string,
icons?: ChipsItemIcon[],
editMode?: boolean) {
this.id = uniqueId();
this.item = item;
this.fieldToDisplay = fieldToDisplay;
this.objToDisplay = objToDisplay;
this.setDisplayText();
this.editMode = editMode || false;
this.icons = icons || [];
}
hasIcons(): boolean {
return isNotEmpty(this.icons);
}
setEditMode(): void {
this.editMode = true;
}
updateIcons(icons: ChipsItemIcon[]): void {
this.icons = icons;
}
updateItem(item: any): void {
this.item = item;
this.setDisplayText();
}
unsetEditMode(): void {
this.editMode = false;
}
private setDisplayText(): void {
let value = this.item;
if (isObject(this.item)) {
// Check If displayField is in an internal object
const obj = this.objToDisplay ? this.item[this.objToDisplay] : this.item;
if (isObject(obj) && obj) {
value = obj[this.fieldToDisplay] || obj.value;
} else {
value = obj;
}
}
this.display = value;
}
}

View File

@@ -0,0 +1,126 @@
import { Chips } from './chips.model';
import { ChipsItem } from './chips-item.model';
import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model';
describe('Chips model test suite', () => {
let items: any[];
let item: ChipsItem;
let chips: Chips;
beforeEach(() => {
items = ['a', 'b', 'c'];
chips = new Chips(items);
});
it('should init Chips object properly', () => {
expect(chips.getChipsItems()).toEqual(items);
expect(chips.displayField).toBe('display');
expect(chips.displayObj).toBe(undefined);
expect(chips.iconsConfig).toEqual([]);
});
it('should add an element to items', () => {
items = ['a', 'b', 'c', 'd'];
chips.add('d');
expect(chips.getChipsItems()).toEqual(items);
});
it('should remove an element from items', () => {
items = ['a', 'c'];
item = chips.getChipByIndex(1);
chips.remove(item);
expect(chips.getChipsItems()).toEqual(items);
});
it('should update an item', () => {
items = ['a', 'd', 'c'];
const id = chips.getChipByIndex(1).id;
chips.update(id, 'd');
expect(chips.getChipsItems()).toEqual(items);
});
it('should update items order', () => {
items = ['a', 'c', 'b'];
const chipsItems = chips.getChips();
const b = chipsItems[1];
chipsItems[1] = chipsItems[2];
chipsItems[2] = b;
chips.updateOrder();
expect(chips.getChipsItems()).toEqual(items);
});
it('should set a different displayField', () => {
items = [
{
label: 'A',
value: 'a'
},
{
label: 'B',
value: 'b'
},
{
label: 'C',
value: 'c'
},
];
chips = new Chips(items, 'label');
expect(chips.displayField).toBe('label');
expect(chips.getChipsItems()).toEqual(items);
});
it('should set a different displayObj', () => {
items = [
{
toDisplay: new FormFieldMetadataValueObject('a', null, 'a'),
otherProperty: 'a'
},
{
toDisplay: new FormFieldMetadataValueObject('a', null, 'a'),
otherProperty: 'a'
},
{
toDisplay: new FormFieldMetadataValueObject('a', null, 'a'),
otherProperty: 'a'
},
];
chips = new Chips(items, 'value', 'toDisplay');
expect(chips.displayField).toBe('value');
expect(chips.displayObj).toBe('toDisplay');
expect(chips.getChipsItems()).toEqual(items);
});
it('should set iconsConfig', () => {
items = [
{
toDisplay: new FormFieldMetadataValueObject('a', null, 'a'),
otherProperty: 'a'
},
{
toDisplay: new FormFieldMetadataValueObject('a', null, 'a'),
otherProperty: 'a'
},
{
toDisplay: new FormFieldMetadataValueObject('a', null, 'a'),
otherProperty: 'a'
},
];
const iconsConfig = [{
name: 'toDisplay',
config: {
withAuthority:{
style: 'fa-user'
},
withoutAuthority:{
style: 'fa-user text-muted'
}
}
}];
chips = new Chips(items, 'value', 'toDisplay', iconsConfig);
expect(chips.displayField).toBe('value');
expect(chips.displayObj).toBe('toDisplay');
expect(chips.iconsConfig).toEqual(iconsConfig);
expect(chips.getChipsItems()).toEqual(items);
});
});

View File

@@ -0,0 +1,159 @@
import { findIndex, isEqual, isObject } from 'lodash';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { ChipsItem, ChipsItemIcon } from './chips-item.model';
import { hasValue, isNotEmpty } from '../../empty.util';
export interface IconsConfig {
withAuthority?: {
style: string;
};
withoutAuthority?: {
style: string;
};
}
export interface MetadataIconsConfig {
name: string;
config: IconsConfig;
}
export class Chips {
chipsItems: BehaviorSubject<ChipsItem[]>;
displayField: string;
displayObj: string;
iconsConfig: MetadataIconsConfig[];
private _items: ChipsItem[];
constructor(items: any[] = [],
displayField: string = 'display',
displayObj?: string,
iconsConfig?: MetadataIconsConfig[]) {
this.displayField = displayField;
this.displayObj = displayObj;
this.iconsConfig = iconsConfig || [];
if (Array.isArray(items)) {
this.setInitialItems(items);
}
}
public add(item: any): void {
const icons = this.getChipsIcons(item);
const chipsItem = new ChipsItem(item, this.displayField, this.displayObj, icons);
const duplicatedIndex = findIndex(this._items, {display: chipsItem.display.trim()});
if (duplicatedIndex === -1 || !isEqual(item, this.getChipByIndex(duplicatedIndex).item)) {
this._items.push(chipsItem);
this.chipsItems.next(this._items);
}
}
public getChipById(id): ChipsItem {
const index = findIndex(this._items, {id: id});
return this.getChipByIndex(index);
}
public getChipByIndex(index): ChipsItem {
if (this._items.length > 0 && this._items[index]) {
return this._items[index];
} else {
return null;
}
}
public getChips(): ChipsItem[] {
return this._items;
}
/**
* To use to get items before to store it
* @returns {any[]}
*/
public getChipsItems(): any[] {
const out = [];
this._items.forEach((item) => {
out.push(item.item);
});
return out;
}
public hasItems(): boolean {
return this._items.length > 0;
}
public remove(chipsItem: ChipsItem): void {
const index = findIndex(this._items, {id: chipsItem.id});
this._items.splice(index, 1);
this.chipsItems.next(this._items);
}
public update(id: string, item: any): void {
const chipsItemTarget = this.getChipById(id);
const icons = this.getChipsIcons(item);
chipsItemTarget.updateItem(item);
chipsItemTarget.updateIcons(icons);
chipsItemTarget.unsetEditMode();
this.chipsItems.next(this._items);
}
public updateOrder(): void {
this.chipsItems.next(this._items);
}
private getChipsIcons(item) {
const icons = [];
const defaultConfigIndex: number = findIndex(this.iconsConfig, {name: 'default'});
const defaultConfig: IconsConfig = (defaultConfigIndex !== -1) ? this.iconsConfig[defaultConfigIndex].config : undefined;
let config: IconsConfig;
let configIndex: number;
let value: any;
Object.keys(item)
.forEach((metadata) => {
value = item[metadata];
configIndex = findIndex(this.iconsConfig, {name: metadata});
config = (configIndex !== -1) ? this.iconsConfig[configIndex].config : defaultConfig;
if (hasValue(value) && isNotEmpty(config)) {
let icon: ChipsItemIcon;
const hasAuthority: boolean = !!(isObject(value) && ((value.hasOwnProperty('authority') && value.authority) || (value.hasOwnProperty('id') && value.id)));
// Set icons
if ((this.displayObj && this.displayObj === metadata && hasAuthority)
|| (this.displayObj && this.displayObj !== metadata)) {
icon = {
metadata,
hasAuthority: hasAuthority,
style: (hasAuthority) ? config.withAuthority.style : config.withoutAuthority.style
};
}
if (icon) {
icons.push(icon);
}
}
});
return icons;
}
/**
* Sets initial items, used in edit mode
*/
private setInitialItems(items: any[]): void {
this._items = [];
items.forEach((item) => {
const icons = this.getChipsIcons(item);
const chipsItem = new ChipsItem(item, this.displayField, this.displayObj, icons);
this._items.push(chipsItem);
});
this.chipsItems = new BehaviorSubject<ChipsItem[]>(this._items);
}
}

View File

@@ -0,0 +1,19 @@
import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
export function dateToGMTString(date: Date | NgbDateStruct) {
let year = ((date instanceof Date) ? date.getFullYear() : date.year).toString();
let month = ((date instanceof Date) ? date.getMonth() + 1 : date.month).toString();
let day = ((date instanceof Date) ? date.getDate() : date.day).toString();
let hour = ((date instanceof Date) ? date.getHours() : 0).toString();
let min = ((date instanceof Date) ? date.getMinutes() : 0).toString();
let sec = ((date instanceof Date) ? date.getSeconds() : 0).toString();
year = (year.length === 1) ? '0' + year : year;
month = (month.length === 1) ? '0' + month : month;
day = (day.length === 1) ? '0' + day : day;
hour = (hour.length === 1) ? '0' + hour : hour;
min = (min.length === 1) ? '0' + min : min;
sec = (sec.length === 1) ? '0' + sec : sec;
return `${year}-${month}-${day}T${hour}:${min}:${sec}Z`;
}

View File

@@ -0,0 +1,461 @@
<div [class.form-group]="(type !== 6 && asBootstrapFormGroup) || getClass('element', 'container').includes('form-group')"
[formGroup]="group"
[ngClass]="[getClass('element', 'container'), getClass('grid', 'container')]">
<label *ngIf="type !== 3 && model.label"
[for]="model.id"
[innerHTML]="(model.required && model.label) ? (model.label | translate) + ' *' : (model.label | translate)"
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]"></label>
<ng-container *ngTemplateOutlet="templates[0]?.templateRef; context: model"></ng-container>
<div [ngClass]="{'form-row': model.hasLanguages }">
<div [ngClass]="getClass('grid', 'control')">
<ng-container [ngSwitch]="type">
<!-- FORM ARRAY ------------------------------------------------------------------------------------------->
<div *ngSwitchCase="1"
[dynamicId]="bindId && model.id"
[formArrayName]="model.id"
[ngClass]="getClass('element', 'control')">
<div *ngFor="let groupModel of model.groups; let idx = index" role="group"
[formGroupName]="idx" [ngClass]="[getClass('element', 'group'), getClass('grid', 'group')]">
<ds-dynamic-form-control *ngFor="let _model of groupModel.group"
[bindId]="false"
[formId]="formId"
[context]="groupModel"
[group]="control.at(idx)"
[hasErrorMessaging]="_model.hasErrorMessages"
[hidden]="_model.hidden"
[layout]="layout"
[model]="_model"
[templates]="templateList"
[ngClass]="[getClass('element', 'host', _model), getClass('grid', 'host', _model)]"
(dfBlur)="onBlur($event)"
(dfChange)="onValueChange($event)"
(dfFocus)="onFocus($event)"></ds-dynamic-form-control>
<ng-container *ngTemplateOutlet="templates[2]?.templateRef; context: groupModel"></ng-container>
</div>
</div>
<!-- CALENDAR --------------------------------------------------------------------------------------------->
<ngb-datepicker *ngSwitchCase="2"
[displayMonths]="getAdditional('displayMonths', 1)"
[dynamicId]="bindId && model.id"
[firstDayOfWeek]="getAdditional('firstDayOfWeek', 1)"
[formControlName]="model.id"
[maxDate]="model.max"
[minDate]="model.min"
[navigation]="getAdditional('navigation', 'select')"
[ngClass]="getClass('element', 'control')"
[outsideDays]="getAdditional('outsideDays', 'visible')"
[showWeekdays]="getAdditional('showWeekdays', true)"
[showWeekNumbers]="getAdditional('showWeekNumbers', false)"
[startDate]="model.focusedDate"
(select)="onValueChange($event)"></ngb-datepicker>
<!-- CHECKBOX --------------------------------------------------------------------------------------------->
<div *ngSwitchCase="3" class="custom-control custom-checkbox" [class.disabled]="model.disabled">
<input type="checkbox" class="custom-control-input"
[checked]="model.checked"
[class.is-invalid]="showErrorMessages"
[id]="bindId && model.id"
[dynamicId]="bindId && model.id"
[formControlName]="model.id"
[indeterminate]="model.indeterminate"
[name]="model.name"
[ngClass]="getClass('element', 'control')"
[required]="model.required"
[tabindex]="model.tabIndex"
[value]="model.value"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)" >
<label class="custom-control-label" [for]="bindId && model.id">
<span [innerHTML]="model.label"
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]">
</span>
</label>
</div>
<!-- CHECKBOX GROUP --------------------------------------------------------------------------------------->
<div *ngSwitchCase="4" class="btn-group btn-group-toggle" data-toggle="buttons"
[dynamicId]="bindId && model.id"
[formGroupName]="model.id"
[ngClass]="getClass('element', 'control')">
<label *ngFor="let checkboxModel of model.group" ngbButtonLabel
[hidden]="checkboxModel.hidden"
[ngClass]="getClass('element', 'control', checkboxModel)">
<input type="checkbox" ngbButton
[checked]="checkboxModel.checked"
[id]="bindId && checkboxModel.id"
[dynamicId]="bindId && checkboxModel.id"
[formControlName]="checkboxModel.id"
[indeterminate]="checkboxModel.indeterminate"
[name]="checkboxModel.name"
[required]="checkboxModel.required"
[tabindex]="checkboxModel.tabIndex"
[value]="checkboxModel.value"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"/>
<span [ngClass]="getClass('element', 'label', checkboxModel)"
[innerHTML]="checkboxModel.label"></span></label>
</div>
<!-- DATEPICKER ------------------------------------------------------------------------------------------->
<div *ngSwitchCase="5" class="input-group">
<input ngbDatepicker class="form-control" #datepicker="ngbDatepicker"
[class.is-invalid]="showErrorMessages"
[displayMonths]="getAdditional('displayMonths', 1)"
[dynamicId]="bindId && model.id"
[firstDayOfWeek]="getAdditional('firstDayOfWeek', 1)"
[formControlName]="model.id"
[maxDate]="model.max"
[minDate]="model.min"
[name]="model.name"
[navigation]="getAdditional('navigation', 'select')"
[ngClass]="getClass('element', 'control')"
[outsideDays]="getAdditional('outsideDays', 'visible')"
[placeholder]="(model.placeholder | translate)"
[placement]="getAdditional('placement', 'bottom-left')"
[showWeekdays]="getAdditional('showWeekdays', true)"
[showWeekNumbers]="getAdditional('showWeekNumbers', false)"
[startDate]="model.focusedDate"
(dateSelect)="onValueChange($event)"
(blur)="onBlur($event)"
(focus)="onFocus($event)">
<div class="input-group-append">
<button class="btn btn-outline-secondary"
type="button"
[class.disabled]="model.disabled"
[disabled]="model.disabled"
(click)="datepicker.toggle()">
<i *ngIf="model.toggleIcon" class="{{model.toggleIcon}}" aria-hidden="true"></i>
<span *ngIf="model.toggleLabel">{{ model.toggleLabel }}</span>
</button>
</div>
</div>
<!-- FORM GROUP ------------------------------------------------------------------------------------------->
<div *ngSwitchCase="6" role="group"
[dynamicId]="bindId && model.id"
[formGroupName]="model.id"
[ngClass]="getClass('element','control')">
<ds-dynamic-form-control *ngFor="let _model of model.group"
[asBootstrapFormGroup]="true"
[formId]="formId"
[group]="control"
[hasErrorMessaging]="_model.hasErrorMessages"
[hidden]="_model.hidden"
[layout]="layout"
[model]="_model"
[templates]="templateList"
[ngClass]="[getClass('element', 'host', _model), getClass('grid', 'host', _model)]"
(dfBlur)="onBlur($event)"
(dfChange)="onValueChange($event)"
(dfFocus)="onFocus($event)"></ds-dynamic-form-control>
</div>
<!-- INPUT ------------------------------------------------------------------------------------------------>
<div *ngSwitchCase="7" [class.input-group]="model.prefix || model.suffix">
<div *ngIf="model.prefix" class="input-group-prepend">
<span class="input-group-text" [innerHTML]="model.prefix"></span>
</div>
<ng-container *ngTemplateOutlet="inputTemplate;
context:{bindId: bindId, model: model, showErrorMessages: showErrorMessages}">
</ng-container>
<div *ngIf="model.suffix" class="input-group-append">
<span class="input-group-text" [innerHTML]="model.suffix"></span>
</div>
<datalist *ngIf="model.list" [id]="model.listId">
<option *ngFor="let option of model.list" [value]="option">
</datalist>
</div>
<!-- RADIO GROUP ------------------------------------------------------------------------------------------>
<div *ngSwitchCase="8" ngbRadioGroup class="btn-group btn-group-toggle" role="radiogroup"
[dynamicId]="bindId && model.id"
[formControlName]="model.id"
[ngClass]="getClass('element', 'control')"
[tabindex]="model.tabIndex"
(change)="onValueChange($event)">
<legend *ngIf="model.legend" [innerHTML]="model.legend"></legend>
<label *ngFor="let option of model.options$ | async" ngbButtonLabel
[ngClass]="[getClass('element', 'option'), getClass('grid', 'option')]">
<input type="radio" ngbButton
[disabled]="option.disabled"
[name]="model.name"
[value]="option.value"
(blur)="onBlur($event)"
(focus)="onFocus($event)"/><span [innerHTML]="option.label"></span>
</label>
</div>
<!-- SELECT ----------------------------------------------------------------------------------------------->
<ng-container *ngSwitchCase="9">
<ng-container *ngTemplateOutlet="selectTemplate;
context:{bindId: bindId, model: model, showErrorMessages: showErrorMessages}">
</ng-container>
</ng-container>
<!-- TEXTAREA --------------------------------------------------------------------------------------------->
<textarea *ngSwitchCase="10" class="form-control"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[cols]="model.cols"
[formControlName]="model.id"
[maxlength]="model.maxLength"
[minlength]="model.minLength"
[name]="model.name"
[ngClass]="getClass('element', 'control')"
[placeholder]="(model.placeholder | translate)"
[readonly]="model.readOnly"
[required]="model.required"
[rows]="model.rows"
[spellcheck]="model.spellCheck"
[tabindex]="model.tabIndex"
[wrap]="model.wrap"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></textarea>
<!-- TIMEPICKER ------------------------------------------------------------------------------------------->
<ngb-timepicker *ngSwitchCase="11"
[dynamicId]="bindId && model.id"
[formControlName]="model.id"
[hourStep]="getAdditional('hourStep', 1)"
[meridian]="model.meridian"
[minuteStep]="getAdditional('minuteStep', 1)"
[ngClass]="getClass('element', 'control')"
[seconds]="model.showSeconds"
[secondStep]="getAdditional('secondStep', 1)"
[size]="getAdditional('size', 'medium')"
[spinners]="getAdditional('spinners', true)"></ngb-timepicker>
<ng-container *ngSwitchCase="12">
<ng-container *ngTemplateOutlet="typeaheadTemplate;
context:{bindId: bindId, model: model, showErrorMessages: showErrorMessages}">
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="13">
<ng-container *ngTemplateOutlet="scrollableDropdownTemplate;
context:{bindId: bindId, model: model, showErrorMessages: showErrorMessages}">
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="14">
<ng-container *ngTemplateOutlet="tagTemplate;
context:{bindId: bindId, model: model, showErrorMessages: showErrorMessages}">
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="15">
<ng-container *ngTemplateOutlet="listTemplate;
context:{bindId: bindId, model: model, showErrorMessages: showErrorMessages}">
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="16">
<ds-dynamic-group [model]="model"
[formId]="formId"
[group]="group"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-group>
</ng-container>
<ng-container *ngSwitchCase="17">
<ds-date-picker
[bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-date-picker>
</ng-container>
<ng-container *ngSwitchCase="18">
<ds-dynamic-lookup
[bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"
></ds-dynamic-lookup>
</ng-container>
<ng-container *ngSwitchCase="19">
<ds-dynamic-lookup
[bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"
></ds-dynamic-lookup>
</ng-container>
<small *ngIf="model.hint" class="text-muted" [innerHTML]="model.hint"
[ngClass]="getClass('element', 'hint')"></small>
<div *ngIf="showErrorMessages" [ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]">
<small *ngFor="let message of errorMessages" class="invalid-feedback d-block">{{ message | translate:model.validators }}</small>
</div>
</ng-container>
<ng-template #inputTemplate let-bindId="bindId" let-model="model"
let-showErrorMessages="showErrorMessages">
<input [attr.accept]="model.accept"
[attr.list]="model.listId"
[attr.max]="model.max"
[attr.min]="model.min"
[attr.multiple]="model.multiple"
[attr.step]="model.step"
[autocomplete]="model.autoComplete"
[autofocus]="model.autoFocus"
[class.form-control]="model.inputType !== 'file'"
[class.form-control-file]="model.inputType === 'file'"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[formControlName]="model.id"
[maxlength]="model.maxLength"
[minlength]="model.minLength"
[name]="model.name"
[ngClass]="getClass('element', 'control')"
[pattern]="model.pattern"
[placeholder]="(model.placeholder | translate)"
[readonly]="model.readOnly"
[required]="model.required"
[spellcheck]="model.spellCheck"
[tabindex]="model.tabIndex"
[textMask]="{mask: (model.mask || false), showMask: model.mask && !(model.placeholder)}"
[type]="model.inputType"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"/>
</ng-template>
<ng-template #selectTemplate let-bindId="bindId" let-model="model"
let-showErrorMessages="showErrorMessages">
<select class="form-control"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[formControlName]="model.id"
[name]="model.name"
[ngClass]="getClass('element', 'control')"
[required]="model.required"
[tabindex]="model.tabIndex"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)">
<option *ngFor="let option of model.options$ | async"
[disabled]="option.disabled"
[ngValue]="option.value">{{ option.label }}</option>
</select>
</ng-template>
<ng-template #typeaheadTemplate let-bindId="bindId" let-model="model"
let-showErrorMessages="showErrorMessages">
<ds-dynamic-typeahead [bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-typeahead>
</ng-template>
<ng-template #scrollableDropdownTemplate let-bindId="bindId" let-model="model"
let-showErrorMessages="showErrorMessages">
<ds-dynamic-scrollable-dropdown [bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-scrollable-dropdown>
</ng-template>
<ng-template #tagTemplate let-bindId="bindId" let-model="model"
let-showErrorMessages="showErrorMessages">
<ds-dynamic-tag [bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-tag>
</ng-template>
<ng-template #listTemplate let-bindId="bindId" let-model="model"
let-showErrorMessages="showErrorMessages">
<ds-dynamic-list [bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-list>
</ng-template>
</div>
<div *ngIf="model.languageCodes && model.languageCodes.length > 0" class="col-xs-2">
<select
#language="ngModel"
[disabled]="model.readOnly"
[(ngModel)]="model.language"
class="form-control"
(blur)="onBlur($event)"
(change)="onChangeLanguage($event)"
[ngModelOptions]="{standalone: true}"
required>
<!--<option [value]="null" disabled>Language</option>-->
<option *ngFor="let lang of model.languageCodes" [value]="lang.code">{{lang.display}}</option>
</select>
</div>
</div>
<ng-container *ngTemplateOutlet="templates[1]?.templateRef; context: model"></ng-container>
<ng-content></ng-content>
</div>

View File

@@ -0,0 +1,281 @@
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement, SimpleChange } from '@angular/core';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TextMaskModule } from 'angular2-text-mask';
import {
DynamicCheckboxGroupModel,
DynamicCheckboxModel,
DynamicColorPickerModel,
DynamicDatePickerModel,
DynamicEditorModel,
DynamicFileUploadModel,
DynamicFormArrayModel,
DynamicFormControlModel,
DynamicFormGroupModel,
DynamicFormsCoreModule,
DynamicFormService,
DynamicInputModel,
DynamicRadioGroupModel,
DynamicRatingModel,
DynamicSelectModel,
DynamicSliderModel,
DynamicSwitchModel,
DynamicTextAreaModel,
DynamicTimePickerModel
} from '@ng-dynamic-forms/core';
import { DsDynamicFormControlComponent, NGBootstrapFormControlType } from './ds-dynamic-form-control.component';
import { TranslateModule } from '@ngx-translate/core';
import { SharedModule } from '../../../shared.module';
import { DynamicDsDatePickerModel } from './models/date-picker/date-picker.model';
import { DynamicGroupModel } from './models/dynamic-group/dynamic-group.model';
import { DynamicListCheckboxGroupModel } from './models/list/dynamic-list-checkbox-group.model';
import { AuthorityOptions } from '../../../../core/integration/models/authority-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 { DynamicQualdropModel } from './models/ds-dynamic-qualdrop.model';
import { DynamicLookupNameModel } from './models/lookup/dynamic-lookup-name.model';
describe('DsDynamicFormControlComponent test suite', () => {
const authorityOptions: AuthorityOptions = {
closed: false,
metadata: 'list',
name: 'type_programme',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
};
const formModel = [
new DynamicCheckboxModel({id: 'checkbox'}),
new DynamicCheckboxGroupModel({id: 'checkboxGroup', group: []}),
new DynamicColorPickerModel({id: 'colorpicker'}),
new DynamicDatePickerModel({id: 'datepicker'}),
new DynamicEditorModel({id: 'editor'}),
new DynamicFileUploadModel({id: 'upload', url: ''}),
new DynamicFormArrayModel({id: 'formArray', groupFactory: () => []}),
new DynamicFormGroupModel({id: 'formGroup', group: []}),
new DynamicInputModel({id: 'input', maxLength: 51}),
new DynamicRadioGroupModel({id: 'radioGroup'}),
new DynamicRatingModel({id: 'rating'}),
new DynamicSelectModel({id: 'select', options: [{value: 'One'}, {value: 'Two'}], value: 'One'}),
new DynamicSliderModel({id: 'slider'}),
new DynamicSwitchModel({id: 'switch'}),
new DynamicTextAreaModel({id: 'textarea'}),
new DynamicTimePickerModel({id: 'timepicker'}),
new DynamicTypeaheadModel({id: 'typeahead'}),
new DynamicScrollableDropdownModel({id: 'scrollableDropdown', authorityOptions: authorityOptions}),
new DynamicTagModel({id: 'tag'}),
new DynamicListCheckboxGroupModel({id: 'checkboxList', authorityOptions: authorityOptions, repeatable: true}),
new DynamicListRadioGroupModel({id: 'radioList', authorityOptions: authorityOptions, repeatable: false}),
new DynamicGroupModel({
id: 'relationGroup',
formConfiguration: [],
mandatoryField: '',
name: 'relationGroup',
relationFields: [],
scopeUUID: '',
submissionScope: ''
}),
new DynamicDsDatePickerModel({id: 'datepicker'}),
new DynamicLookupModel({id: 'lookup'}),
new DynamicLookupNameModel({id: 'lookupName'}),
new DynamicQualdropModel({id: 'combobox', readOnly: false})
];
const testModel = formModel[8];
let formGroup: FormGroup;
let fixture: ComponentFixture<DsDynamicFormControlComponent>;
let component: DsDynamicFormControlComponent;
let debugElement: DebugElement;
let testElement: DebugElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
FormsModule,
ReactiveFormsModule,
NgbModule.forRoot(),
DynamicFormsCoreModule.forRoot(),
SharedModule,
TranslateModule.forRoot(),
TextMaskModule,
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
}).compileComponents().then(() => {
fixture = TestBed.createComponent(DsDynamicFormControlComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement;
});
}));
beforeEach(inject([DynamicFormService], (service: DynamicFormService) => {
formGroup = service.createFormGroup(formModel);
component.group = formGroup;
component.model = testModel;
component.ngOnChanges({
group: new SimpleChange(null, component.group, true),
model: new SimpleChange(null, component.model, true)
});
fixture.detectChanges();
testElement = debugElement.query(By.css(`input[id='${testModel.id}']`));
}));
it('should initialize correctly', () => {
expect(component.context).toBeNull();
expect(component.control instanceof FormControl).toBe(true);
expect(component.group instanceof FormGroup).toBe(true);
expect(component.model instanceof DynamicFormControlModel).toBe(true);
expect(component.hasErrorMessaging).toBe(false);
expect(component.asBootstrapFormGroup).toBe(true);
expect(component.onControlValueChanges).toBeDefined();
expect(component.onModelDisabledUpdates).toBeDefined();
expect(component.onModelValueUpdates).toBeDefined();
expect(component.blur).toBeDefined();
expect(component.change).toBeDefined();
expect(component.focus).toBeDefined();
expect(component.onValueChange).toBeDefined();
expect(component.onBlur).toBeDefined();
expect(component.onFocus).toBeDefined();
expect(component.isValid).toBe(true);
expect(component.isInvalid).toBe(false);
expect(component.showErrorMessages).toBe(false);
expect(component.type).toBe(NGBootstrapFormControlType.Input);
});
it('should have an input element', () => {
expect(testElement instanceof DebugElement).toBe(true);
});
it('should listen to native blur events', () => {
spyOn(component, 'onBlur');
testElement.triggerEventHandler('blur', null);
expect(component.onBlur).toHaveBeenCalled();
});
it('should listen to native focus events', () => {
spyOn(component, 'onFocus');
testElement.triggerEventHandler('focus', null);
expect(component.onFocus).toHaveBeenCalled();
});
it('should listen to native change event', () => {
spyOn(component, 'onValueChange');
testElement.triggerEventHandler('change', null);
expect(component.onValueChange).toHaveBeenCalled();
});
it('should update model value when control value changes', () => {
spyOn(component, 'onControlValueChanges');
component.control.setValue('test');
expect(component.onControlValueChanges).toHaveBeenCalled();
});
it('should update control value when model value changes', () => {
spyOn(component, 'onModelValueUpdates');
(testModel as DynamicInputModel).valueUpdates.next('test');
expect(component.onModelValueUpdates).toHaveBeenCalled();
});
it('should update control activation when model disabled property changes', () => {
spyOn(component, 'onModelDisabledUpdates');
testModel.disabledUpdates.next(true);
expect(component.onModelDisabledUpdates).toHaveBeenCalled();
});
it('should determine correct form control type', () => {
const testFn = DsDynamicFormControlComponent.getFormControlType;
expect(testFn(formModel[0])).toEqual(NGBootstrapFormControlType.Checkbox);
expect(testFn(formModel[1])).toEqual(NGBootstrapFormControlType.CheckboxGroup);
expect(testFn(formModel[2])).toBeNull();
expect(testFn(formModel[3])).toEqual(NGBootstrapFormControlType.DatePicker);
(formModel[3] as DynamicDatePickerModel).inline = true;
expect(testFn(formModel[3])).toEqual(NGBootstrapFormControlType.Calendar);
expect(testFn(formModel[4])).toBeNull();
expect(testFn(formModel[5])).toBeNull();
expect(testFn(formModel[6])).toEqual(NGBootstrapFormControlType.Array);
expect(testFn(formModel[7])).toEqual(NGBootstrapFormControlType.Group);
expect(testFn(formModel[8])).toEqual(NGBootstrapFormControlType.Input);
expect(testFn(formModel[9])).toEqual(NGBootstrapFormControlType.RadioGroup);
expect(testFn(formModel[10])).toBeNull();
expect(testFn(formModel[11])).toEqual(NGBootstrapFormControlType.Select);
expect(testFn(formModel[12])).toBeNull();
expect(testFn(formModel[13])).toBeNull();
expect(testFn(formModel[14])).toEqual(NGBootstrapFormControlType.TextArea);
expect(testFn(formModel[15])).toEqual(NGBootstrapFormControlType.TimePicker);
expect(testFn(formModel[16])).toEqual(NGBootstrapFormControlType.TypeAhead);
expect(testFn(formModel[17])).toEqual(NGBootstrapFormControlType.ScrollableDropdown);
expect(testFn(formModel[18])).toEqual(NGBootstrapFormControlType.Tag);
expect(testFn(formModel[19])).toEqual(NGBootstrapFormControlType.List);
expect(testFn(formModel[20])).toEqual(NGBootstrapFormControlType.List);
expect(testFn(formModel[21])).toEqual(NGBootstrapFormControlType.Relation);
expect(testFn(formModel[22])).toEqual(NGBootstrapFormControlType.Date);
expect(testFn(formModel[23])).toEqual(NGBootstrapFormControlType.Lookup);
expect(testFn(formModel[24])).toEqual(NGBootstrapFormControlType.LookupName);
expect(testFn(formModel[25])).toEqual(NGBootstrapFormControlType.Group);
});
});

View File

@@ -0,0 +1,178 @@
import {
ChangeDetectorRef,
Component,
ContentChildren,
EventEmitter,
Input,
OnChanges,
Output,
QueryList,
SimpleChanges
} from '@angular/core';
import { FormGroup } from '@angular/forms';
import {
DynamicDatePickerModel,
DynamicFormControlComponent,
DynamicFormControlEvent,
DynamicFormControlModel,
DynamicFormLayout,
DynamicFormLayoutService,
DynamicFormValidationService,
DynamicTemplateDirective,
DYNAMIC_FORM_CONTROL_TYPE_ARRAY,
DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX,
DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX_GROUP,
DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER,
DYNAMIC_FORM_CONTROL_TYPE_GROUP,
DYNAMIC_FORM_CONTROL_TYPE_INPUT,
DYNAMIC_FORM_CONTROL_TYPE_RADIO_GROUP,
DYNAMIC_FORM_CONTROL_TYPE_SELECT,
DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA,
DYNAMIC_FORM_CONTROL_TYPE_TIMEPICKER,
} from '@ng-dynamic-forms/core';
import { DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD } from './models/typeahead/dynamic-typeahead.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_RELATION_GROUP } from './models/dynamic-group/dynamic-group.model';
import { DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER } from './models/date-picker/date-picker.model';
import { DYNAMIC_FORM_CONTROL_TYPE_LOOKUP } from './models/lookup/dynamic-lookup.model';
import { DynamicListCheckboxGroupModel } from './models/list/dynamic-list-checkbox-group.model';
import { DynamicListRadioGroupModel } from './models/list/dynamic-list-radio-group.model';
import { isNotEmpty } from '../../../empty.util';
import { DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME } from './models/lookup/dynamic-lookup-name.model';
export const enum NGBootstrapFormControlType {
Array = 1, // 'ARRAY',
Calendar = 2, // 'CALENDAR',
Checkbox = 3, // 'CHECKBOX',
CheckboxGroup = 4, // 'CHECKBOX_GROUP',
DatePicker = 5, // 'DATEPICKER',
Group = 6, // 'GROUP',
Input = 7, // 'INPUT',
RadioGroup = 8, // 'RADIO_GROUP',
Select = 9, // 'SELECT',
TextArea = 10, // 'TEXTAREA',
TimePicker = 11, // 'TIMEPICKER'
TypeAhead = 12, // 'TYPEAHEAD'
ScrollableDropdown = 13, // 'SCROLLABLE_DROPDOWN'
Tag = 14, // 'TAG'
List = 15, // 'TYPELIST'
Relation = 16, // 'RELATION'
Date = 17, // 'DATE'
Lookup = 18, // LOOKUP
LookupName = 19, // LOOKUP_NAME
}
@Component({
selector: 'ds-dynamic-form-control',
styleUrls: ['../../form.component.scss', './ds-dynamic-form.component.scss'],
templateUrl: './ds-dynamic-form-control.component.html'
})
export class DsDynamicFormControlComponent extends DynamicFormControlComponent implements OnChanges {
@ContentChildren(DynamicTemplateDirective) contentTemplateList: QueryList<DynamicTemplateDirective>;
// tslint:disable-next-line:no-input-rename
@Input('templates') inputTemplateList: QueryList<DynamicTemplateDirective>;
@Input() formId: string;
@Input() asBootstrapFormGroup = true;
@Input() bindId = true;
@Input() context: any | null = null;
@Input() group: FormGroup;
@Input() hasErrorMessaging = false;
@Input() layout: DynamicFormLayout;
@Input() model: any;
/* tslint:disable:no-output-rename */
@Output('dfBlur') blur: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
@Output('dfChange') change: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
@Output('dfFocus') focus: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
/* tslint:enable:no-output-rename */
type: NGBootstrapFormControlType | null;
static getFormControlType(model: DynamicFormControlModel): NGBootstrapFormControlType | null {
switch (model.type) {
case DYNAMIC_FORM_CONTROL_TYPE_ARRAY:
return NGBootstrapFormControlType.Array;
case DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX:
return NGBootstrapFormControlType.Checkbox;
case DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX_GROUP:
return (model instanceof DynamicListCheckboxGroupModel) ? NGBootstrapFormControlType.List : NGBootstrapFormControlType.CheckboxGroup;
case DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER:
const datepickerModel = model as DynamicDatePickerModel;
return datepickerModel.inline ? NGBootstrapFormControlType.Calendar : NGBootstrapFormControlType.DatePicker;
case DYNAMIC_FORM_CONTROL_TYPE_GROUP:
return NGBootstrapFormControlType.Group;
case DYNAMIC_FORM_CONTROL_TYPE_INPUT:
return NGBootstrapFormControlType.Input;
case DYNAMIC_FORM_CONTROL_TYPE_RADIO_GROUP:
return (model instanceof DynamicListRadioGroupModel) ? NGBootstrapFormControlType.List : NGBootstrapFormControlType.RadioGroup;
case DYNAMIC_FORM_CONTROL_TYPE_SELECT:
return NGBootstrapFormControlType.Select;
case DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA:
return NGBootstrapFormControlType.TextArea;
case DYNAMIC_FORM_CONTROL_TYPE_TIMEPICKER:
return NGBootstrapFormControlType.TimePicker;
case DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD:
return NGBootstrapFormControlType.TypeAhead;
case DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN:
return NGBootstrapFormControlType.ScrollableDropdown;
case DYNAMIC_FORM_CONTROL_TYPE_TAG:
return NGBootstrapFormControlType.Tag;
case DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP:
return NGBootstrapFormControlType.Relation;
case DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER:
return NGBootstrapFormControlType.Date;
case DYNAMIC_FORM_CONTROL_TYPE_LOOKUP:
return NGBootstrapFormControlType.Lookup;
case DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME:
return NGBootstrapFormControlType.LookupName;
default:
return null;
}
}
constructor(protected changeDetectorRef: ChangeDetectorRef, protected layoutService: DynamicFormLayoutService,
protected validationService: DynamicFormValidationService) {
super(changeDetectorRef, layoutService, validationService);
}
ngOnChanges(changes: SimpleChanges) {
if (changes) {
super.ngOnChanges(changes);
}
if (changes.model) {
this.type = DsDynamicFormControlComponent.getFormControlType(this.model);
}
}
onChangeLanguage(event) {
if (isNotEmpty((this.model as any).value)) {
this.onValueChange(event);
}
}
}

View File

@@ -0,0 +1,12 @@
<ds-dynamic-form-control *ngFor="let model of formModel; trackBy: trackByFn"
[formId]="formId"
[group]="formGroup"
[hasErrorMessaging]="model.hasErrorMessages"
[hidden]="model.hidden"
[layout]="formLayout"
[model]="model"
[ngClass]="[getClass(model, 'element', 'host'), getClass(model, 'grid', 'host')]"
[templates]="templates"
(dfBlur)="onEvent($event, 'blur')"
(dfChange)="onEvent($event, 'change')"
(dfFocus)="onEvent($event, 'focus')"></ds-dynamic-form-control>

View File

@@ -0,0 +1,5 @@
:host {
display: flex;
flex-direction: column;
justify-content: center;
}

View File

@@ -0,0 +1,47 @@
import {
Component,
ContentChildren,
EventEmitter,
Input,
Output,
QueryList,
ViewChildren
} from '@angular/core';
import { FormGroup } from '@angular/forms';
import {
DynamicFormComponent,
DynamicFormControlEvent,
DynamicFormControlModel,
DynamicFormLayout,
DynamicFormLayoutService,
DynamicFormService,
DynamicTemplateDirective,
} from '@ng-dynamic-forms/core';
import { DsDynamicFormControlComponent } from './ds-dynamic-form-control.component';
import { FormBuilderService } from '../form-builder.service';
@Component({
selector: 'ds-dynamic-form',
templateUrl: './ds-dynamic-form.component.html'
})
export class DsDynamicFormComponent extends DynamicFormComponent {
@Input() formId: string;
@Input() formGroup: FormGroup;
@Input() formModel: DynamicFormControlModel[];
@Input() formLayout: DynamicFormLayout = null;
/* tslint:disable:no-output-rename */
@Output('dfBlur') blur: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
@Output('dfChange') change: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
@Output('dfFocus') focus: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
/* tslint:enable:no-output-rename */
@ContentChildren(DynamicTemplateDirective) templates: QueryList<DynamicTemplateDirective>;
@ViewChildren(DsDynamicFormControlComponent) components: QueryList<DsDynamicFormControlComponent>;
constructor(protected formService: FormBuilderService, protected layoutService: DynamicFormLayoutService) {
super(formService, layoutService);
}
}

View File

@@ -0,0 +1,50 @@
<div class="d-flex">
<ds-number-picker
tabindex="1"
[disabled]="model.disabled"
[min]="minYear"
[max]="maxYear"
[name]="'year'"
[size]="4"
[(ngModel)]="initialYear"
[value]="year"
[invalid]="showErrorMessages"
[placeholder]='yearPlaceholder'
(blur)="onBlur($event)"
(change)="onChange($event)"
(focus)="onFocus($event)"
></ds-number-picker>
<ds-number-picker
tabindex="2"
[min]="minMonth"
[max]="maxMonth"
[name]="'month'"
[size]="6"
[(ngModel)]="initialMonth"
[value]="month"
[placeholder]="monthPlaceholder"
[disabled]="!year || model.disabled"
(blur)="onBlur($event)"
(change)="onChange($event)"
(focus)="onFocus($event)"
></ds-number-picker>
<ds-number-picker
tabindex="3"
[min]="minDay"
[max]="maxDay"
[name]="'day'"
[size]="2"
[(ngModel)]="initialDay"
[value]="day"
[placeholder]="dayPlaceholder"
[disabled]="!month || model.disabled"
(blur)="onBlur($event)"
(change)="onChange($event)"
(focus)="onFocus($event)"
></ds-number-picker>
</div>
<div class="clearfix"></div>

View File

@@ -0,0 +1,3 @@
.col-lg-1 {
width: auto;
}

View File

@@ -0,0 +1,255 @@
// Load the implementations that should be tested
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, inject, TestBed, } from '@angular/core/testing';
import { FormControl, FormGroup } from '@angular/forms';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { DynamicFormValidationService } from '@ng-dynamic-forms/core';
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';
export const DATE_TEST_GROUP = new FormGroup({
date: new FormControl()
});
export const DATE_TEST_MODEL_CONFIG = {
disabled: false,
errorMessages: {required: 'You must enter at least the year.'},
id: 'date',
label: 'Date',
name: 'date',
placeholder: 'Date',
readOnly: false,
required: true,
toggleIcon: 'fa fa-calendar'
};
describe('DsDatePickerComponent test suite', () => {
let testComp: TestComponent;
let dateComp: DsDatePickerComponent;
let testFixture: ComponentFixture<TestComponent>;
let dateFixture: ComponentFixture<DsDatePickerComponent>;
let html;
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
NgbModule.forRoot()
],
declarations: [
DsDatePickerComponent,
TestComponent,
], // declare the test component
providers: [
ChangeDetectorRef,
DsDatePickerComponent,
DynamicFormValidationService,
FormBuilderService,
FormComponent,
FormService
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
}));
describe('', () => {
// synchronous beforeEach
beforeEach(() => {
html = `
<ds-date-picker
[bindId]='bindId'
[group]='group'
[model]='model'
[showErrorMessages]='showErrorMessages'
(blur)='onBlur($event)'
(change)='onValueChange($event)'
(focus)='onFocus($event)'></ds-date-picker>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
it('should create DsDatePickerComponent', inject([DsDatePickerComponent], (app: DsDatePickerComponent) => {
expect(app).toBeDefined();
}));
});
describe('', () => {
describe('when init model value is empty', () => {
beforeEach(() => {
dateFixture = TestBed.createComponent(DsDatePickerComponent);
dateComp = dateFixture.componentInstance; // FormComponent test instance
dateComp.group = DATE_TEST_GROUP;
dateComp.model = new DynamicDsDatePickerModel(DATE_TEST_MODEL_CONFIG);
dateFixture.detectChanges();
});
it('should init component properly', () => {
expect(dateComp.initialYear).toBeDefined();
expect(dateComp.initialMonth).toBeDefined();
expect(dateComp.initialDay).toBeDefined();
expect(dateComp.maxYear).toBeDefined();
expect(dateComp.disabledMonth).toBeTruthy();
expect(dateComp.disabledDay).toBeTruthy();
});
it('should set year and enable month field when year field is entered', () => {
const event = {
field: 'year',
value: '1983'
};
dateComp.onChange(event);
expect(dateComp.year).toEqual('1983');
expect(dateComp.disabledMonth).toBeFalsy();
expect(dateComp.disabledDay).toBeTruthy();
});
it('should set month and enable day field when month field is entered', () => {
const event = {
field: 'month',
value: '11'
};
dateComp.year = '1983';
dateComp.disabledMonth = false;
dateFixture.detectChanges();
dateComp.onChange(event);
expect(dateComp.year).toEqual('1983');
expect(dateComp.month).toEqual('11');
expect(dateComp.disabledMonth).toBeFalsy();
expect(dateComp.disabledDay).toBeFalsy();
});
it('should set day when day field is entered', () => {
const event = {
field: 'day',
value: '18'
};
dateComp.year = '1983';
dateComp.month = '11';
dateComp.disabledMonth = false;
dateComp.disabledDay = false;
dateFixture.detectChanges();
dateComp.onChange(event);
expect(dateComp.year).toEqual('1983');
expect(dateComp.month).toEqual('11');
expect(dateComp.day).toEqual('18');
expect(dateComp.disabledMonth).toBeFalsy();
expect(dateComp.disabledDay).toBeFalsy();
});
it('should emit blur Event onBlur', () => {
spyOn(dateComp.blur, 'emit');
dateComp.onBlur(new Event('blur'));
expect(dateComp.blur.emit).toHaveBeenCalled();
});
it('should emit focus Event onFocus', () => {
spyOn(dateComp.focus, 'emit');
dateComp.onFocus(new Event('focus'));
expect(dateComp.focus.emit).toHaveBeenCalled();
});
});
describe('when init model value is not empty', () => {
beforeEach(() => {
dateFixture = TestBed.createComponent(DsDatePickerComponent);
dateComp = dateFixture.componentInstance; // FormComponent test instance
dateComp.group = DATE_TEST_GROUP;
dateComp.model = new DynamicDsDatePickerModel(DATE_TEST_MODEL_CONFIG);
dateComp.model.value = '1983-11-18';
dateFixture.detectChanges();
});
it('should init component properly', () => {
expect(dateComp.initialYear).toBeDefined();
expect(dateComp.initialMonth).toBeDefined();
expect(dateComp.initialDay).toBeDefined();
expect(dateComp.maxYear).toBeDefined();
expect(dateComp.year).toBe(1983);
expect(dateComp.month).toBe(11);
expect(dateComp.day).toBe(18);
expect(dateComp.disabledMonth).toBeFalsy();
expect(dateComp.disabledDay).toBeFalsy();
});
it('should disable month and day fields when year field is canceled', () => {
const event = {
field: 'year',
value: null
};
dateComp.onChange(event);
expect(dateComp.year).not.toBeDefined();
expect(dateComp.month).not.toBeDefined();
expect(dateComp.day).not.toBeDefined();
expect(dateComp.disabledMonth).toBeTruthy();
expect(dateComp.disabledDay).toBeTruthy();
});
it('should disable day field when month field is canceled', () => {
const event = {
field: 'month',
value: null
};
dateComp.onChange(event);
expect(dateComp.year).toBe(1983);
expect(dateComp.month).not.toBeDefined();
expect(dateComp.day).not.toBeDefined();
expect(dateComp.disabledMonth).toBeFalsy();
expect(dateComp.disabledDay).toBeTruthy();
});
it('should not disable day field when day field is canceled', () => {
const event = {
field: 'day',
value: null
};
dateComp.onChange(event);
expect(dateComp.year).toBe(1983);
expect(dateComp.month).toBe(11);
expect(dateComp.day).not.toBeDefined();
expect(dateComp.disabledMonth).toBeFalsy();
expect(dateComp.disabledDay).toBeFalsy();
});
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
group = DATE_TEST_GROUP;
model = new DynamicDsDatePickerModel(DATE_TEST_MODEL_CONFIG);
showErrorMessages = false;
}

View File

@@ -0,0 +1,172 @@
import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { DynamicDsDatePickerModel } from './date-picker.model';
import { hasNoValue, hasValue, isNotEmpty } from '../../../../../empty.util';
export const DS_DATE_PICKER_SEPARATOR = '-';
@Component({
selector: 'ds-date-picker',
styleUrls: ['./date-picker.component.scss'],
templateUrl: './date-picker.component.html',
})
export class DsDatePickerComponent implements OnInit {
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicDsDatePickerModel;
@Input() showErrorMessages = false;
// @Input()
// minDate;
// @Input()
// maxDate;
@Output() selected = new EventEmitter<number>();
@Output() remove = new EventEmitter<number>();
@Output() blur = new EventEmitter<any>();
@Output() change = new EventEmitter<any>();
@Output() focus = new EventEmitter<any>();
initialYear: number;
initialMonth: number;
initialDay: number;
year: any;
month: any;
day: any;
minYear: 0;
maxYear: number;
minMonth = 1;
maxMonth = 12;
minDay = 1;
maxDay = 31;
yearPlaceholder = 'year';
monthPlaceholder = 'month';
dayPlaceholder = 'day';
disabledMonth = true;
disabledDay = true;
ngOnInit() {
const now = new Date();
this.initialYear = now.getFullYear();
this.initialMonth = now.getMonth() + 1;
this.initialDay = now.getDate();
if (this.model.value && 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);
this.year = this.initialYear;
this.disabledMonth = false;
}
if (values.length > 1) {
this.initialMonth = parseInt(values[1], 10);
this.month = this.initialMonth;
this.disabledDay = false;
}
if (values.length > 2) {
this.initialDay = parseInt(values[2], 10);
this.day = this.initialDay;
}
}
this.maxYear = this.initialYear + 100;
}
onBlur(event) {
this.blur.emit();
}
onChange(event) {
// update year-month-day
switch (event.field) {
case 'year': {
if (event.value !== null) {
this.year = event.value;
} else {
this.year = undefined;
this.month = undefined;
this.day = undefined;
this.disabledMonth = true;
this.disabledDay = true;
}
break;
}
case 'month': {
if (event.value !== null) {
this.month = event.value;
} else {
this.month = undefined;
this.day = undefined;
this.disabledDay = true;
}
break;
}
case 'day': {
if (event.value !== null) {
this.day = event.value;
} else {
this.day = undefined;
}
break;
}
}
// set max for days by month/year
if (!this.disabledDay) {
const month = this.month ? this.month - 1 : 0;
const date = new Date(this.year, month, 1);
this.maxDay = this.getLastDay(date);
if (this.day > this.maxDay) {
this.day = this.maxDay;
}
}
// Manage disable
if (hasValue(this.year) && event.field === 'year') {
this.disabledMonth = false;
} else if (hasValue(this.month) && event.field === 'month') {
this.disabledDay = false;
}
// update value
let value = null;
if (hasValue(this.year)) {
let yyyy = this.year.toString();
while (yyyy.length < 4) {
yyyy = '0' + yyyy;
}
value = yyyy;
}
if (hasValue(this.month)) {
const mm = this.month.toString().length === 1
? '0' + this.month.toString()
: this.month.toString();
value += DS_DATE_PICKER_SEPARATOR + mm;
}
if (hasValue(this.day)) {
const dd = this.day.toString().length === 1
? '0' + this.day.toString()
: this.day.toString();
value += DS_DATE_PICKER_SEPARATOR + dd;
}
this.model.valueUpdates.next(value);
this.change.emit(value);
}
onFocus(event) {
this.focus.emit(event);
}
getLastDay(date: Date) {
// Last Day of the same month (+1 month, -1 day)
date.setMonth(date.getMonth() + 1, 0);
return date.getDate();
}
}

View File

@@ -0,0 +1,21 @@
import { DynamicDateControlModel, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
import { DynamicDateControlModelConfig } from '@ng-dynamic-forms/core/src/model/dynamic-date-control.model';
import { Subject } from 'rxjs/Subject';
export const DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER = 'DATE';
/**
* Dynamic Date Picker Model class
*/
export class DynamicDsDatePickerModel extends DynamicDateControlModel {
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER;
valueUpdates: Subject<any>;
malformedDate: boolean;
hasLanguages = false;
constructor(config: DynamicDateControlModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.malformedDate = false;
}
}

View File

@@ -0,0 +1,52 @@
import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicFormGroupModelConfig, serializable } from '@ng-dynamic-forms/core';
import { isNotEmpty } from '../../../../empty.util';
import { DsDynamicInputModel } from './ds-dynamic-input.model';
import { AuthorityValueModel } from '../../../../../core/integration/models/authority-value.model';
import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model';
export const CONCAT_GROUP_SUFFIX = '_CONCAT_GROUP';
export const CONCAT_FIRST_INPUT_SUFFIX = '_CONCAT_FIRST_INPUT';
export const CONCAT_SECOND_INPUT_SUFFIX = '_CONCAT_SECOND_INPUT';
export interface DynamicConcatModelConfig extends DynamicFormGroupModelConfig {
separator: string;
}
export class DynamicConcatModel extends DynamicFormGroupModel {
@serializable() separator: string;
@serializable() hasLanguages = false;
isCustomGroup = true;
constructor(config: DynamicConcatModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.separator = config.separator + ' ';
}
get value() {
const firstValue = (this.get(0) as DsDynamicInputModel).value;
const secondValue = (this.get(1) as DsDynamicInputModel).value;
if (isNotEmpty(firstValue) && isNotEmpty(secondValue)) {
return new FormFieldMetadataValueObject(firstValue + this.separator + secondValue);
} else {
return null;
}
}
set value(value: string | FormFieldMetadataValueObject) {
let values;
if (typeof value === 'string') {
values = value ? value.split(this.separator) : [null, null];
} else {
values = value ? value.value.split(this.separator) : [null, null];
}
if (values.length > 1) {
(this.get(0) as DsDynamicInputModel).valueUpdates.next(values[0]);
(this.get(1) as DsDynamicInputModel).valueUpdates.next(values[1]);
}
}
}

View File

@@ -0,0 +1,86 @@
import {
DynamicFormControlLayout,
DynamicInputModel,
DynamicInputModelConfig,
serializable
} from '@ng-dynamic-forms/core';
import { Subject } from 'rxjs/Subject';
import { LanguageCode } from '../../models/form-field-language-value.model';
import { AuthorityOptions } from '../../../../../core/integration/models/authority-options.model';
import { hasValue } from '../../../../empty.util';
import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model';
export interface DsDynamicInputModelConfig extends DynamicInputModelConfig {
authorityOptions?: AuthorityOptions;
languageCodes?: LanguageCode[];
language?: string;
value?: any;
}
export class DsDynamicInputModel extends DynamicInputModel {
@serializable() authorityOptions: AuthorityOptions;
@serializable() private _languageCodes: LanguageCode[];
@serializable() private _language: string;
@serializable() languageUpdates: Subject<string>;
constructor(config: DsDynamicInputModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.readOnly = config.readOnly;
this.value = config.value;
this.language = config.language;
if (!this.language) {
// TypeAhead
if (config.value instanceof FormFieldMetadataValueObject) {
this.language = config.value.language;
} else if (Array.isArray(config.value)) {
// Tag of Authority
if (config.value[0].language) {
this.language = config.value[0].language;
}
}
}
this.languageCodes = config.languageCodes;
this.languageUpdates = new Subject<string>();
this.languageUpdates.subscribe((lang: string) => {
this.language = lang;
});
this.authorityOptions = config.authorityOptions;
}
get hasAuthority(): boolean {
return this.authorityOptions && hasValue(this.authorityOptions.name);
}
get hasLanguages(): boolean {
if (this.languageCodes && this.languageCodes.length > 1) {
return true;
} else {
return false;
}
}
get language(): string {
return this._language;
}
set language(language: string) {
this._language = language;
}
get languageCodes(): LanguageCode[] {
return this._languageCodes;
}
set languageCodes(languageCodes: LanguageCode[]) {
this._languageCodes = languageCodes;
if (!this.language || this.language === null || this.language === '') {
this.language = this.languageCodes ? this.languageCodes[0].code : null;
}
}
}

View File

@@ -0,0 +1,65 @@
import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicInputModelConfig, serializable } from '@ng-dynamic-forms/core';
import { DsDynamicInputModel, DsDynamicInputModelConfig } from './ds-dynamic-input.model';
import { Subject } from 'rxjs/Subject';
import { DynamicFormGroupModelConfig } from '@ng-dynamic-forms/core/src/model/form-group/dynamic-form-group.model';
import { LanguageCode } from '../../models/form-field-language-value.model';
export const QUALDROP_GROUP_SUFFIX = '_QUALDROP_GROUP';
export const QUALDROP_METADATA_SUFFIX = '_QUALDROP_METADATA';
export const QUALDROP_VALUE_SUFFIX = '_QUALDROP_VALUE';
export interface DsDynamicQualdropModelConfig extends DynamicFormGroupModelConfig {
languageCodes?: LanguageCode[];
language?: string;
readOnly: boolean;
}
export class DynamicQualdropModel extends DynamicFormGroupModel {
@serializable() private _language: string;
@serializable() private _languageCodes: LanguageCode[];
@serializable() languageUpdates: Subject<string>;
@serializable() hasLanguages = false;
@serializable() readOnly: boolean;
isCustomGroup = true;
constructor(config: DsDynamicQualdropModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.readOnly = config.readOnly;
this.language = config.language;
this.languageCodes = config.languageCodes;
this.languageUpdates = new Subject<string>();
this.languageUpdates.subscribe((lang: string) => {
this.language = lang;
});
}
get value() {
return (this.get(1) as DsDynamicInputModel).value;
}
get qualdropId(): string {
return (this.get(0) as DsDynamicInputModel).value.toString();
}
get language(): string {
return this._language;
}
set language(language: string) {
this._language = language;
}
get languageCodes(): LanguageCode[] {
return this._languageCodes;
}
set languageCodes(languageCodes: LanguageCode[]) {
this._languageCodes = languageCodes;
if (!this.language || this.language === null || this.language === '') {
this.language = this.languageCodes ? this.languageCodes[0].code : null;
}
}
}

View File

@@ -0,0 +1,21 @@
import {
DYNAMIC_FORM_CONTROL_TYPE_ARRAY,
DynamicFormArrayModel, DynamicFormArrayModelConfig, DynamicFormControlLayout,
serializable
} from '@ng-dynamic-forms/core';
import { DYNAMIC_FORM_CONTROL_TYPE_TAG } from './tag/dynamic-tag.model';
export interface DynamicRowArrayModelConfig extends DynamicFormArrayModelConfig {
notRepeteable: boolean;
}
export class DynamicRowArrayModel extends DynamicFormArrayModel {
@serializable() notRepeteable = false;
isRowArray = true;
constructor(config: DynamicRowArrayModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.notRepeteable = config.notRepeteable;
}
}

View File

@@ -0,0 +1,5 @@
import { DynamicFormGroupModel } from '@ng-dynamic-forms/core';
export class DynamicRowGroupModel extends DynamicFormGroupModel {
isRowGroup = true;
}

View File

@@ -0,0 +1,24 @@
import { DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
import { DsDynamicInputModel, DsDynamicInputModelConfig } from './ds-dynamic-input.model';
export interface DsDynamicTextAreaModelConfig extends DsDynamicInputModelConfig {
cols?: number;
rows?: number;
wrap?: string;
}
export class DsDynamicTextAreaModel extends DsDynamicInputModel {
@serializable() cols: number;
@serializable() rows: number;
@serializable() wrap: string;
@serializable() type = DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA;
constructor(config: DsDynamicTextAreaModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.cols = config.cols;
this.rows = config.rows;
this.wrap = config.wrap;
}
}

View File

@@ -0,0 +1,72 @@
<a *ngIf="!(formCollapsed | async)"
class="close position-relative"
ngbTooltip="{{'form.group-collapse-help' | translate}}"
placement="left">
<span class="fa fa-angle-up fa-fw fa-2x"
aria-hidden="true"
(click)="collapseForm()"></span>
</a>
<a *ngIf="(formCollapsed | async)"
class="close position-relative"
ngbTooltip="{{'form.group-expand-help' | translate}}"
placement="left">
<span class="fa fa-angle-down fa-fw fa-2x"
aria-hidden="true"
(click)="expandForm()"></span>
</a>
<div class="pt-2" [ngClass]="{'border-top': !showErrorMessages, 'border border-danger': showErrorMessages}">
<div *ngIf="!(formCollapsed | async)" class="pl-2 row" @shrinkInOut>
<ds-form #formRef="formComponent"
class="col-sm-12 col-md-8 col-lg-9 col-xl-10 pl-0"
[formId]="formId"
[formModel]="formModel"
[displaySubmit]="false"
[emitChange]="false"
(dfBlur)="onBlur($event)"
(dfFocus)="onFocus($event)"></ds-form>
<div *ngIf="!(formCollapsed | async)" class="col p-0 m-0 d-flex justify-content-center align-items-center">
<button type="button"
class="btn btn-link"
[disabled]="isMandatoryFieldEmpty()"
(click)="save()">
<i class="fa fa-save text-primary fa-2x"
aria-hidden="true"></i>
</button>
<button type="button"
class="btn btn-link"
[disabled]="!editMode"
(click)="delete()">
<i class="fa fa-trash text-danger fa-2x"
aria-hidden="true"></i>
</button>
<button type="button"
class="btn btn-link"
[disabled]="isMandatoryFieldEmpty()"
(click)="clear()">
<i class="fa fa-undo fa-2x"
aria-hidden="true"></i>
</button>
</div>
<div class="clearfix"></div>
</div>
<div class="d-flex">
<div *ngIf="!chips.hasItems()">
<input type="text"
class="border-0 form-control-plaintext tag-input mt-1 mb-1 pl-2 text-muted"
readonly
tabindex="-1"
value="{{'form.no-value' | translate}}">
</div>
<ds-chips
*ngIf="chips.hasItems()"
[chips]="chips"
[editable]="true"
(selected)="onChipSelected($event)"></ds-chips>
</div>
</div>

View File

@@ -0,0 +1,3 @@
.close {
top: -2.5rem;
}

View File

@@ -0,0 +1,315 @@
// Load the implementations that should be tested
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 { DynamicFormValidationService } from '@ng-dynamic-forms/core';
import { Store } from '@ngrx/store';
import { TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { DsDynamicGroupComponent } from './dynamic-group.components';
import { DynamicGroupModel, DynamicGroupModelConfig } from './dynamic-group.model';
import { FormRowModel, SubmissionFormsModel } from '../../../../../../core/shared/config/config-submission-forms.model';
import { FormFieldModel } from '../../../models/form-field.model';
import { FormBuilderService } from '../../../form-builder.service';
import { FormService } from '../../../../form.service';
import { GLOBAL_CONFIG } from '../../../../../../../config';
import { FormComponent } from '../../../../form.component';
import { AppState } from '../../../../../../app.reducer';
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';
export const FORM_GROUP_TEST_MODEL_CONFIG = {
disabled: false,
errorMessages: {required: 'You must specify at least one author.'},
formConfiguration: [{
fields: [{
hints: 'Enter the name of the author.',
input: {type: 'onebox'},
label: 'Author',
languageCodes: [],
mandatory: 'true',
mandatoryMessage: 'Required field!',
repeatable: false,
selectableMetadata: [{
authority: 'RPAuthority',
closed: false,
metadata: 'dc.contributor.author'
}],
} as FormFieldModel]
} as FormRowModel, {
fields: [{
hints: 'Enter the affiliation of the author.',
input: {type: 'onebox'},
label: 'Affiliation',
languageCodes: [],
mandatory: 'false',
repeatable: false,
selectableMetadata: [{
authority: 'OUAuthority',
closed: false,
metadata: 'local.contributor.affiliation'
}]
} as FormFieldModel]
} as FormRowModel],
id: 'dc_contributor_author',
label: 'Authors',
mandatoryField: 'dc.contributor.author',
name: 'dc.contributor.author',
placeholder: 'Authors',
readOnly: false,
relationFields: ['local.contributor.affiliation'],
required: true,
scopeUUID: '43fe1f8c-09a6-4fcf-9c78-5d4fed8f2c8f',
submissionScope: undefined,
validators: {required: null}
} as DynamicGroupModelConfig;
export const FORM_GROUP_TEST_GROUP = new FormGroup({
dc_contributor_author: new FormControl(),
});
describe('DsDynamicGroupComponent test suite', () => {
const config = {
form: {
validatorMap: {
required: 'required',
regex: 'pattern'
}
}
} as any;
let testComp: TestComponent;
let groupComp: DsDynamicGroupComponent;
let testFixture: ComponentFixture<TestComponent>;
let groupFixture: ComponentFixture<DsDynamicGroupComponent>;
let modelValue: any;
let html;
let control1: FormControl;
let model1: DsDynamicInputModel;
let control2: FormControl;
let model2: DsDynamicInputModel;
const store: Store<AppState> = jasmine.createSpyObj('store', {
dispatch: {},
select: Observable.of(true)
});
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
BrowserAnimationsModule,
FormsModule,
ReactiveFormsModule,
NgbModule.forRoot(),
TranslateModule.forRoot()
],
declarations: [
FormComponent,
DsDynamicGroupComponent,
TestComponent,
], // declare the test component
providers: [
ChangeDetectorRef,
DsDynamicGroupComponent,
DynamicFormValidationService,
FormBuilderService,
FormComponent,
FormService,
{provide: GLOBAL_CONFIG, useValue: config},
{provide: Store, useValue: store},
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
}));
describe('', () => {
// synchronous beforeEach
beforeEach(() => {
html = `
<ds-dynamic-group [model]="model"
[formId]="formId"
[group]="group"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-group>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
it('should create DsDynamicGroupComponent', inject([DsDynamicGroupComponent], (app: DsDynamicGroupComponent) => {
expect(app).toBeDefined();
}));
});
describe('when init model value is empty', () => {
beforeEach(inject([FormBuilderService], (service: FormBuilderService) => {
groupFixture = TestBed.createComponent(DsDynamicGroupComponent);
groupComp = groupFixture.componentInstance; // FormComponent test instance
groupComp.formId = 'testForm';
groupComp.group = FORM_GROUP_TEST_GROUP;
groupComp.model = new DynamicGroupModel(FORM_GROUP_TEST_MODEL_CONFIG);
groupComp.showErrorMessages = false;
groupFixture.detectChanges();
control1 = service.getFormControlById('dc_contributor_author', (groupComp as any).formRef.formGroup, groupComp.formModel) as FormControl;
model1 = service.findById('dc_contributor_author', groupComp.formModel) as DsDynamicInputModel;
control2 = service.getFormControlById('local_contributor_affiliation', (groupComp as any).formRef.formGroup, groupComp.formModel) as FormControl;
model2 = service.findById('local_contributor_affiliation', groupComp.formModel) as DsDynamicInputModel;
// spyOn(store, 'dispatch');
}));
afterEach(() => {
groupFixture.destroy();
groupComp = null;
});
it('should init component properly', inject([FormBuilderService], (service: FormBuilderService) => {
const formConfig = {rows: groupComp.model.formConfiguration} as SubmissionFormsModel;
const formModel = service.modelFromConfiguration(formConfig, groupComp.model.scopeUUID, {}, groupComp.model.submissionScope, groupComp.model.readOnly);
const chips = new Chips([], 'value', 'dc.contributor.author');
expect(groupComp.formCollapsed).toEqual(Observable.of(false));
expect(groupComp.formModel.length).toEqual(formModel.length);
expect(groupComp.chips.getChipsItems()).toEqual(chips.getChipsItems());
}));
it('should save a new chips item', () => {
control1.setValue('test author');
(model1 as any).value = new FormFieldMetadataValueObject('test author');
control2.setValue('test affiliation');
(model2 as any).value = new FormFieldMetadataValueObject('test affiliation');
modelValue = [{
'dc.contributor.author': new FormFieldMetadataValueObject('test author'),
'local.contributor.affiliation': new FormFieldMetadataValueObject('test affiliation')
}];
groupFixture.detectChanges();
const buttons = groupFixture.debugElement.nativeElement.querySelectorAll('button');
const btnEl = buttons[0];
btnEl.click();
expect(groupComp.chips.getChipsItems()).toEqual(modelValue);
expect(groupComp.formCollapsed).toEqual(Observable.of(true));
});
it('should clear form inputs', () => {
control1.setValue('test author');
(model1 as any).value = new FormFieldMetadataValueObject('test author');
control2.setValue('test affiliation');
(model2 as any).value = new FormFieldMetadataValueObject('test affiliation');
groupFixture.detectChanges();
const buttons = groupFixture.debugElement.nativeElement.querySelectorAll('button');
const btnEl = buttons[2];
btnEl.click();
expect(control1.value).toBeNull();
expect(control2.value).toBeNull();
expect(groupComp.formCollapsed).toEqual(Observable.of(false));
});
});
describe('when init model value is not empty', () => {
beforeEach(() => {
groupFixture = TestBed.createComponent(DsDynamicGroupComponent);
groupComp = groupFixture.componentInstance; // FormComponent test instance
groupComp.formId = 'testForm';
groupComp.group = FORM_GROUP_TEST_GROUP;
groupComp.model = new DynamicGroupModel(FORM_GROUP_TEST_MODEL_CONFIG);
modelValue = [{
'dc.contributor.author': new FormFieldMetadataValueObject('test author'),
'local.contributor.affiliation': new FormFieldMetadataValueObject('test affiliation')
}];
groupComp.model.value = modelValue;
groupComp.showErrorMessages = false;
groupFixture.detectChanges();
});
afterEach(() => {
groupFixture.destroy();
groupComp = null;
});
it('should init component properly', inject([FormBuilderService], (service: FormBuilderService) => {
const formConfig = {rows: groupComp.model.formConfiguration} as SubmissionFormsModel;
const formModel = service.modelFromConfiguration(formConfig, groupComp.model.scopeUUID, {}, groupComp.model.submissionScope, groupComp.model.readOnly);
const chips = new Chips(modelValue, 'value', 'dc.contributor.author');
expect(groupComp.formCollapsed).toEqual(Observable.of(true));
expect(groupComp.formModel.length).toEqual(formModel.length);
expect(groupComp.chips.getChipsItems()).toEqual(chips.getChipsItems());
}));
it('should modify existing chips item', inject([FormBuilderService], (service: FormBuilderService) => {
groupComp.onChipSelected(0);
groupFixture.detectChanges();
control1 = service.getFormControlById('dc_contributor_author', (groupComp as any).formRef.formGroup, groupComp.formModel) as FormControl;
model1 = service.findById('dc_contributor_author', groupComp.formModel) as DsDynamicInputModel;
control1.setValue('test author modify');
(model1 as any).value = new FormFieldMetadataValueObject('test author modify');
modelValue = [{
'dc.contributor.author': new FormFieldMetadataValueObject('test author modify'),
'local.contributor.affiliation': new FormFieldMetadataValueObject('test affiliation')
}];
groupFixture.detectChanges();
const buttons = groupFixture.debugElement.nativeElement.querySelectorAll('button');
const btnEl = buttons[0];
btnEl.click();
groupFixture.detectChanges();
expect(groupComp.chips.getChipsItems()).toEqual(modelValue);
expect(groupComp.formCollapsed).toEqual(Observable.of(true));
}));
it('should delete existing chips item', () => {
groupComp.onChipSelected(0);
groupFixture.detectChanges();
const buttons = groupFixture.debugElement.nativeElement.querySelectorAll('button');
const btnEl = buttons[1];
btnEl.click();
expect(groupComp.chips.getChipsItems()).toEqual([]);
expect(groupComp.formCollapsed).toEqual(Observable.of(false));
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
group = FORM_GROUP_TEST_GROUP;
groupModelConfig = FORM_GROUP_TEST_MODEL_CONFIG;
model = new DynamicGroupModel(this.groupModelConfig);
showErrorMessages = false;
}

View File

@@ -0,0 +1,240 @@
import {
ChangeDetectorRef,
Component,
EventEmitter,
Inject,
Input,
OnDestroy,
OnInit,
Output,
ViewChild
} from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { DynamicFormControlModel, DynamicFormGroupModel, DynamicInputModel } from '@ng-dynamic-forms/core';
import { isEqual } from 'lodash';
import { DynamicGroupModel, PLACEHOLDER_PARENT_METADATA } from './dynamic-group.model';
import { FormBuilderService } from '../../../form-builder.service';
import { SubmissionFormsModel } from '../../../../../../core/shared/config/config-submission-forms.model';
import { FormService } from '../../../../form.service';
import { FormComponent } from '../../../../form.component';
import { Chips } from '../../../../../chips/models/chips.model';
import { hasValue, isEmpty, isNotEmpty } from '../../../../../empty.util';
import { shrinkInOut } from '../../../../../animations/shrink';
import { ChipsItem } from '../../../../../chips/models/chips-item.model';
import { GlobalConfig } from '../../../../../../../config/global-config.interface';
import { GLOBAL_CONFIG } from '../../../../../../../config';
import { FormGroup } from '@angular/forms';
import { Subscription } from 'rxjs/Subscription';
import { hasOnlyEmptyProperties } from '../../../../../object.util';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
@Component({
selector: 'ds-dynamic-group',
styleUrls: ['./dynamic-group.component.scss'],
templateUrl: './dynamic-group.component.html',
animations: [shrinkInOut]
})
export class DsDynamicGroupComponent implements OnDestroy, OnInit {
@Input() formId: string;
@Input() group: FormGroup;
@Input() model: DynamicGroupModel;
@Input() showErrorMessages = false;
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
@Output() change: EventEmitter<any> = new EventEmitter<any>();
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
public chips: Chips;
public formCollapsed = Observable.of(false);
public formModel: DynamicFormControlModel[];
public editMode = false;
private selectedChipItem: ChipsItem;
private subs: Subscription[] = [];
@ViewChild('formRef') private formRef: FormComponent;
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
private formBuilderService: FormBuilderService,
private formService: FormService,
private cdr: ChangeDetectorRef) {
}
ngOnInit() {
const config = {rows: this.model.formConfiguration} as SubmissionFormsModel;
if (!this.model.isEmpty()) {
this.formCollapsed = Observable.of(true);
}
this.model.valueUpdates.subscribe((value: any[]) => {
if ((isNotEmpty(value) && !(value.length === 1 && hasOnlyEmptyProperties(value[0])))) {
this.collapseForm();
} else {
this.expandForm();
}
// this.formCollapsed = (isNotEmpty(value) && !(value.length === 1 && hasOnlyEmptyProperties(value[0]))) ? Observable.of(true) : Observable.of(false);
});
this.formId = this.formService.getUniqueId(this.model.id);
this.formModel = this.formBuilderService.modelFromConfiguration(
config,
this.model.scopeUUID,
{},
this.model.submissionScope,
this.model.readOnly);
const initChipsValue = this.model.isEmpty() ? [] : this.model.value;
this.chips = new Chips(
initChipsValue,
'value',
this.model.mandatoryField);
this.subs.push(
this.chips.chipsItems
.subscribe((subItems: any[]) => {
const items = this.chips.getChipsItems();
// Does not emit change if model value is equal to the current value
if (!isEqual(items, this.model.value)) {
// if ((isNotEmpty(items) && !this.model.isEmpty()) || (isEmpty(items) && !this.model.isEmpty())) {
if (!(isEmpty(items) && this.model.isEmpty())) {
this.model.valueUpdates.next(items);
this.change.emit();
}
}
}),
)
}
isMandatoryFieldEmpty() {
// formModel[0].group[0].value == null
let res = true;
this.formModel.forEach((row) => {
const modelRow = row as DynamicFormGroupModel;
modelRow.group.forEach((model: DynamicInputModel) => {
if (model.name === this.model.mandatoryField) {
res = model.value == null;
return;
}
});
});
return res;
}
onBlur(event) {
this.blur.emit();
}
onChipSelected(event) {
this.expandForm();
this.selectedChipItem = this.chips.getChipByIndex(event);
this.formModel.forEach((row) => {
const modelRow = row as DynamicFormGroupModel;
modelRow.group.forEach((model: DynamicInputModel) => {
const value = (this.selectedChipItem.item[model.name] === PLACEHOLDER_PARENT_METADATA
|| this.selectedChipItem.item[model.name].value === PLACEHOLDER_PARENT_METADATA)
? null
: this.selectedChipItem.item[model.name];
// if (value instanceof FormFieldMetadataValueObject || value instanceof AuthorityValueModel) {
// model.valueUpdates.next(value.display);
// } else {
// model.valueUpdates.next(value);
// }
model.valueUpdates.next(value);
});
});
this.editMode = true;
}
onFocus(event) {
this.focus.emit(event);
}
collapseForm() {
this.formCollapsed = Observable.of(true);
this.clear();
}
expandForm() {
this.formCollapsed = Observable.of(false);
}
clear() {
if (this.editMode) {
this.selectedChipItem.editMode = false;
this.selectedChipItem = null;
this.editMode = false;
}
this.resetForm();
if (!this.model.isEmpty()) {
this.formCollapsed = Observable.of(true);
}
}
save() {
if (this.editMode) {
this.modifyChip();
} else {
this.addToChips();
}
}
delete() {
this.chips.remove(this.selectedChipItem);
this.clear();
}
private addToChips() {
if (!this.formRef.formGroup.valid) {
this.formService.validateAllFormFields(this.formRef.formGroup);
return;
}
// Item to add
if (!this.isMandatoryFieldEmpty()) {
const item = this.buildChipItem();
this.chips.add(item);
this.resetForm();
}
}
private modifyChip() {
if (!this.formRef.formGroup.valid) {
this.formService.validateAllFormFields(this.formRef.formGroup);
return;
}
if (!this.isMandatoryFieldEmpty()) {
const item = this.buildChipItem();
this.chips.update(this.selectedChipItem.id, item);
this.resetForm();
this.cdr.detectChanges();
}
}
private buildChipItem() {
const item = Object.create({});
this.formModel.forEach((row) => {
const modelRow = row as DynamicFormGroupModel;
modelRow.group.forEach((control: DynamicInputModel) => {
item[control.name] = control.value || PLACEHOLDER_PARENT_METADATA;
});
});
return item;
}
private resetForm() {
if (this.formRef) {
this.formService.resetForm(this.formRef.formGroup, this.formModel, this.formId);
}
}
ngOnDestroy(): void {
this.subs
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -0,0 +1,73 @@
import { DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
import { FormRowModel } from '../../../../../../core/shared/config/config-submission-forms.model';
import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model';
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
import { isEmpty, isNull } from '../../../../../empty.util';
export const DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP = 'RELATION';
export const PLACEHOLDER_PARENT_METADATA = '#PLACEHOLDER_PARENT_METADATA_VALUE#';
/**
* Dynamic Group Model configuration interface
*/
export interface DynamicGroupModelConfig extends DsDynamicInputModelConfig {
formConfiguration: FormRowModel[],
mandatoryField: string,
relationFields: string[],
scopeUUID: string,
submissionScope: string;
}
/**
* Dynamic Group Model class
*/
export class DynamicGroupModel extends DsDynamicInputModel {
@serializable() formConfiguration: FormRowModel[];
@serializable() mandatoryField: string;
@serializable() relationFields: string[];
@serializable() scopeUUID: string;
@serializable() submissionScope: string;
@serializable() _value: any[];
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP;
constructor(config: DynamicGroupModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.formConfiguration = config.formConfiguration;
this.mandatoryField = config.mandatoryField;
this.relationFields = config.relationFields;
this.scopeUUID = config.scopeUUID;
this.submissionScope = config.submissionScope;
const value = config.value || [];
this.valueUpdates.next(value);
}
get value() {
return this._value
}
set value(value) {
this._value = (isEmpty(value)) ? null : value;
}
isEmpty() {
const value = this.getGroupValue();
return (value.length === 1 && isNull(value[0][this.mandatoryField]));
}
getGroupValue(): any[] {
if (isEmpty(this._value)) {
// If items is empty, last element has been removed
// so emit an empty value that allows to dispatch
// a remove JSON PATCH operation
const emptyItem = Object.create({});
emptyItem[this.mandatoryField] = null;
this.relationFields
.forEach((field) => {
emptyItem[field] = null;
});
return [emptyItem];
}
return this._value
}
}

View File

@@ -0,0 +1,60 @@
import { Subject } from 'rxjs/Subject';
import {
DynamicCheckboxGroupModel, DynamicFormControlLayout,
DynamicFormGroupModelConfig,
serializable
} from '@ng-dynamic-forms/core';
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
import { hasValue } from '../../../../../empty.util';
export interface DynamicListCheckboxGroupModelConfig extends DynamicFormGroupModelConfig {
authorityOptions: AuthorityOptions;
groupLength?: number;
repeatable: boolean;
value?: any;
}
export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel {
@serializable() authorityOptions: AuthorityOptions;
@serializable() repeatable: boolean;
@serializable() groupLength: number;
@serializable() _value: AuthorityValueModel[];
isListGroup = true;
valueUpdates: Subject<any>;
constructor(config: DynamicListCheckboxGroupModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.authorityOptions = config.authorityOptions;
this.groupLength = config.groupLength || 5;
this._value = [];
this.repeatable = config.repeatable;
this.valueUpdates = new Subject<any>();
this.valueUpdates.subscribe((value: AuthorityValueModel | AuthorityValueModel[]) => this.value = value);
this.valueUpdates.next(config.value);
}
get hasAuthority(): boolean {
return this.authorityOptions && hasValue(this.authorityOptions.name);
}
get value() {
return this._value;
}
set value(value: AuthorityValueModel | AuthorityValueModel[]) {
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 AuthorityValueModel[]).concat([value]);
this._value = newValue
}
}
}
}

View File

@@ -0,0 +1,36 @@
import {
DynamicFormControlLayout,
DynamicRadioGroupModel,
DynamicRadioGroupModelConfig,
serializable
} from '@ng-dynamic-forms/core';
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
import { hasValue } from '../../../../../empty.util';
export interface DynamicListModelConfig extends DynamicRadioGroupModelConfig<any> {
authorityOptions: AuthorityOptions;
groupLength?: number;
repeatable: boolean;
value?: any;
}
export class DynamicListRadioGroupModel extends DynamicRadioGroupModel<any> {
@serializable() authorityOptions: AuthorityOptions;
@serializable() repeatable: boolean;
@serializable() groupLength: number;
isListGroup = true;
constructor(config: DynamicListModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.authorityOptions = config.authorityOptions;
this.groupLength = config.groupLength || 5;
this.repeatable = config.repeatable;
this.valueUpdates.next(config.value);
}
get hasAuthority(): boolean {
return this.authorityOptions && hasValue(this.authorityOptions.name);
}
}

View File

@@ -0,0 +1,66 @@
<div [formGroup]="group">
<div *ngIf="model.repeatable"
class="form-row"
[attr.tabindex]="model.tabIndex"
[dynamicId]="bindId && model.id"
[formGroupName]="model.id"
[ngClass]="model.layout.element?.control">
<div *ngFor="let columnItems of items" class="col-sm ml-3">
<div *ngFor="let item of columnItems" class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input"
[attr.tabindex]="item.index"
[checked]="item.value"
[id]="item.id"
[dynamicId]="item.id"
[formControlName]="item.id"
[name]="model.name"
[required]="model.required"
[value]="item.value"
(blur)="onBlur($event)"
(change)="onChange($event)"
(focus)="onFocus($event)"/>
<label class="custom-control-label"
[class.disabled]="model.disabled"
[ngClass]="model.layout.element?.control"
[for]="item.id">
<span [ngClass]="model.layout.element?.label" [innerHTML]="item.label"></span>
</label>
</div>
<br>
</div>
</div>
<div *ngIf="!model.repeatable"
class="form-row"
ngbRadioGroup
[attr.tabindex]="model.tabIndex"
[dynamicId]="bindId && model.id"
[ngClass]="model.layout.element?.control"
(change)="onChange($event)">
<div *ngFor="let columnItems of items" class="col-sm ml-3">
<div *ngFor="let item of columnItems" class="custom-control custom-radio">
<label class="custom-control-label"
[class.disabled]="model.disabled"
[ngClass]="model.layout.element?.control">
<input type="radio" class="custom-control-input"
[checked]="item.value"
[dynamicId]="item.id"
[name]="model.id"
[required]="model.required"
[value]="item.index"
(blur)="onBlur($event)"
(focus)="onFocus($event)"/>
<span [ngClass]="model.layout.element?.label" [innerHTML]="item.label"></span>
</label>
</div>
<br>
</div>
</div>
</div>

View File

@@ -0,0 +1,299 @@
// Load the implementations that should be tested
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 { 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 { DynamicFormControlLayout, 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 { DynamicListRadioGroupModel } from './dynamic-list-radio-group.model';
import { By } from '@angular/platform-browser';
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
import { createTestComponent } from '../../../../../testing/utils';
export const LAYOUT_TEST = {
element: {
group: ''
}
} as DynamicFormControlLayout;
export const LIST_TEST_GROUP = new FormGroup({
listCheckbox: new FormGroup({}),
listRadio: new FormGroup({})
});
export const LIST_CHECKBOX_TEST_MODEL_CONFIG = {
authorityOptions: {
closed: false,
metadata: 'listCheckbox',
name: 'type_programme',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as AuthorityOptions,
disabled: false,
id: 'listCheckbox',
label: 'Programme',
name: 'listCheckbox',
placeholder: 'Programme',
readOnly: false,
required: false,
repeatable: true
};
export const LIST_RADIO_TEST_MODEL_CONFIG = {
authorityOptions: {
closed: false,
metadata: 'listRadio',
name: 'type_programme',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as AuthorityOptions,
disabled: false,
id: 'listRadio',
label: 'Programme',
name: 'listRadio',
placeholder: 'Programme',
readOnly: false,
required: false,
repeatable: false
};
describe('DsDynamicListComponent test suite', () => {
let testComp: TestComponent;
let listComp: DsDynamicListComponent;
let testFixture: ComponentFixture<TestComponent>;
let listFixture: ComponentFixture<DsDynamicListComponent>;
let html;
let modelValue;
const authorityServiceStub = new AuthorityServiceStub();
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
DynamicFormsCoreModule,
DynamicFormsNGBootstrapUIModule,
FormsModule,
ReactiveFormsModule,
NgbModule.forRoot()
],
declarations: [
DsDynamicListComponent,
TestComponent,
], // declare the test component
providers: [
AuthorityService,
ChangeDetectorRef,
DsDynamicListComponent,
DynamicFormValidationService,
FormBuilderService,
{provide: AuthorityService, useValue: authorityServiceStub},
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
}));
describe('', () => {
// synchronous beforeEach
beforeEach(() => {
html = `
<ds-dynamic-list
[bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-list>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
it('should create DsDynamicListComponent', inject([DsDynamicListComponent], (app: DsDynamicListComponent) => {
expect(app).toBeDefined();
}));
});
describe('when model is a DynamicListCheckboxGroupModel', () => {
describe('and init model value is empty', () => {
beforeEach(() => {
listFixture = TestBed.createComponent(DsDynamicListComponent);
listComp = listFixture.componentInstance; // FormComponent test instance
listComp.group = LIST_TEST_GROUP;
listComp.model = new DynamicListCheckboxGroupModel(LIST_CHECKBOX_TEST_MODEL_CONFIG, LAYOUT_TEST);
listFixture.detectChanges();
});
afterEach(() => {
listFixture.destroy();
listComp = null;
});
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);
})
});
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 AuthorityValueModel(), {id: 1, display: 'one', value: 1})];
item.nativeElement.click();
expect(listComp.model.value).toEqual(modelValue)
});
it('should emit blur Event onBlur', () => {
spyOn(listComp.blur, 'emit');
listComp.onBlur(new Event('blur'));
expect(listComp.blur.emit).toHaveBeenCalled();
});
it('should emit focus Event onFocus', () => {
spyOn(listComp.focus, 'emit');
listComp.onFocus(new Event('focus'));
expect(listComp.focus.emit).toHaveBeenCalled();
});
});
describe('and init model value is not empty', () => {
beforeEach(() => {
listFixture = TestBed.createComponent(DsDynamicListComponent);
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 AuthorityValueModel(), {id: 1, display: 'one', value: 1})];
listComp.model.value = modelValue;
listFixture.detectChanges();
});
afterEach(() => {
listFixture.destroy();
listComp = null;
});
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();
})
});
it('should set model value properly when a checkbox option is deselected', () => {
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 = [];
item.nativeElement.click();
expect(listComp.model.value).toEqual(modelValue)
});
});
});
describe('when model is a DynamicListRadioGroupModel', () => {
describe('and init model value is empty', () => {
beforeEach(() => {
listFixture = TestBed.createComponent(DsDynamicListComponent);
listComp = listFixture.componentInstance; // FormComponent test instance
listComp.group = LIST_TEST_GROUP;
listComp.model = new DynamicListRadioGroupModel(LIST_RADIO_TEST_MODEL_CONFIG, LAYOUT_TEST);
listFixture.detectChanges();
});
afterEach(() => {
listFixture.destroy();
listComp = null;
});
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);
})
});
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 AuthorityValueModel(), {id: 1, display: 'one', value: 1});
item.nativeElement.click();
expect(listComp.model.value).toEqual(modelValue)
});
});
describe('and init model value is not empty', () => {
beforeEach(() => {
listFixture = TestBed.createComponent(DsDynamicListComponent);
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 AuthorityValueModel(), {id: 1, display: 'one', value: 1});
listComp.model.value = modelValue;
listFixture.detectChanges();
});
afterEach(() => {
listFixture.destroy();
listComp = null;
});
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();
})
});
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
group: FormGroup = LIST_TEST_GROUP;
model = new DynamicListCheckboxGroupModel(LIST_CHECKBOX_TEST_MODEL_CONFIG, LAYOUT_TEST);
showErrorMessages = false;
}

View File

@@ -0,0 +1,135 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
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 } from '@ng-dynamic-forms/core';
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
import { DynamicListRadioGroupModel } from './dynamic-list-radio-group.model';
import { IntegrationData } from '../../../../../../core/integration/integration-data';
export interface ListItem {
id: string,
label: string,
value: boolean,
index: number
}
@Component({
selector: 'ds-dynamic-list',
styleUrls: ['./dynamic-list.component.scss'],
templateUrl: './dynamic-list.component.html'
})
export class DsDynamicListComponent implements OnInit {
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicListCheckboxGroupModel | DynamicListRadioGroupModel;
@Input() showErrorMessages = false;
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
@Output() change: EventEmitter<any> = new EventEmitter<any>();
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
public items: ListItem[][] = [];
protected optionsList: AuthorityValueModel[];
protected searchOptions: IntegrationSearchOptions;
constructor(private authorityService: AuthorityService,
private cdr: ChangeDetectorRef,
private formBuilderService: FormBuilderService) {
}
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();
}
}
onBlur(event: Event) {
this.blur.emit(event);
}
onFocus(event: Event) {
this.focus.emit(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: AuthorityValueModel = this.optionsList[target.tabIndex];
if (target.checked) {
this.model.valueUpdates.next(authorityValue);
} else {
const newValue = [];
this.model.value
.filter((item) => item.value !== authorityValue.value)
.forEach((item) => newValue.push(item));
this.model.valueUpdates.next(newValue);
}
} else {
(this.model as DynamicListRadioGroupModel).valueUpdates.next(this.optionsList[target.value]);
}
this.change.emit(event);
}
protected setOptionsFromAuthority() {
if (this.model.authorityOptions.name && this.model.authorityOptions.name.length > 0) {
const listGroup = this.group.controls[this.model.id] as FormGroup;
this.authorityService.getEntriesByName(this.searchOptions).subscribe((authorities: IntegrationData) => {
let groupCounter = 0;
let itemsPerGroup = 0;
let tempList: ListItem[] = [];
this.optionsList = authorities.payload as AuthorityValueModel[];
// Make a list of available options (checkbox/radio) and split in groups of 'model.groupLength'
(authorities.payload as AuthorityValueModel[]).forEach((option, key) => {
const value = option.id || option.value;
const checked: boolean = isNotEmpty(findKey(
this.model.value,
{value: option.value}));
const item: ListItem = {
id: value,
label: option.display,
value: checked,
index: key
};
if (this.model.repeatable) {
this.formBuilderService.addFormGroupControl(listGroup, (this.model as DynamicListCheckboxGroupModel), new DynamicCheckboxModel(item));
} else {
(this.model as DynamicListRadioGroupModel).options.push({label: item.label, value: option});
}
tempList.push(item);
itemsPerGroup++;
this.items[groupCounter] = tempList;
if (itemsPerGroup === this.model.groupLength) {
groupCounter++;
itemsPerGroup = 0;
tempList = [];
}
});
this.cdr.detectChanges();
});
}
}
protected hasAuthorityOptions() {
return (hasValue(this.model.authorityOptions.scope)
&& hasValue(this.model.authorityOptions.name)
&& hasValue(this.model.authorityOptions.metadata));
}
}

View File

@@ -0,0 +1,26 @@
import { DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
import { DynamicLookupModel, DynamicLookupModelConfig } from './dynamic-lookup.model';
export const DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME = 'LOOKUP_NAME';
export interface DynamicLookupNameModelConfig extends DynamicLookupModelConfig {
separator?: string;
firstPlaceholder?: string;
secondPlaceholder?: string;
}
export class DynamicLookupNameModel extends DynamicLookupModel {
@serializable() separator: string;
@serializable() secondPlaceholder: string;
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME;
constructor(config: DynamicLookupNameModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.separator = config.separator || ',';
this.placeholder = config.firstPlaceholder || 'form.last-name';
this.secondPlaceholder = config.secondPlaceholder || 'form.first-name';
}
}

View File

@@ -0,0 +1,127 @@
<div ngbDropdown #sdRef="ngbDropdown"
(click)="$event.stopPropagation();"
(openChange)="openChange($event);">
<!--Simple lookup, only 1 field -->
<div class="form-row" *ngIf="!isLookupName()">
<div class="col-xs-12 col-sm-8 col-md-9 col-lg-10">
<div class="row">
<div class="col">
<input class="form-control"
[attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[name]="model.name"
[type]="model.inputType"
[(ngModel)]="firstInputValue"
[disabled]="isInputDisabled()"
[placeholder]="model.placeholder"
[readonly]="model.readOnly"
(change)="$event.preventDefault()"
(blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();"
(focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();"
(click)="$event.stopPropagation(); $event.stopPropagation(); sdRef.close();"
(input)="onInput($event)">
</div>
</div>
</div>
<div class="col-xs-12 col-sm-4 col-md-2 col-lg-1 text-center">
<button ngbDropdownAnchor
*ngIf="!isInputDisabled()" class="btn btn-secondary"
type="button"
[disabled]="model.readOnly || isSearchDisabled()"
(click)="sdRef.open(); search(); $event.stopPropagation();">{{'form.search' | translate}}
</button>
<button *ngIf="isInputDisabled()" class="btn btn-secondary"
type="button"
[disabled]="model.readOnly"
(click)="remove($event)">{{'form.remove' | translate}}
</button>
</div>
</div>
<!--Lookup-name, 2 fields-->
<div class="form-row" *ngIf="isLookupName()">
<div class="col-xs-12 col-md-8 col-lg-9">
<div class="row">
<div class="col-xs-12 col-md-6">
<input class="form-control"
[attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[name]="model.name"
[type]="model.inputType"
[(ngModel)]="firstInputValue"
[disabled]="isInputDisabled()"
[placeholder]="model.placeholder | translate"
[readonly]="model.readOnly"
(change)="$event.preventDefault()"
(blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();"
(focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();"
(click)="$event.stopPropagation(); $event.stopPropagation(); sdRef.close();"
(input)="onInput($event)">
</div>
<div *ngIf="isLookupName()" class="col-xs-12 col-md-6 pl-md-0" >
<input class="form-control"
[ngClass]="{}"
[attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[name]="model.name + '_2'"
[type]="model.inputType"
[(ngModel)]="secondInputValue"
[disabled]="firstInputValue.length === 0 || isInputDisabled()"
[placeholder]="model.secondPlaceholder | translate"
[readonly]="model.readOnly"
(change)="$event.preventDefault()"
(blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();"
(focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();"
(click)="$event.stopPropagation(); sdRef.close();"
(input)="onInput($event)">
</div>
</div>
</div>
<div class="col-xs-12 col-md-3 col-lg-2 text-center">
<button ngbDropdownAnchor
*ngIf="!isInputDisabled()" class="btn btn-secondary"
type="button"
[disabled]="isSearchDisabled()"
(click)="sdRef.open(); search(); $event.stopPropagation();">{{'form.search' | translate}}
</button>
<button *ngIf="isInputDisabled()" class="btn btn-secondary"
type="button"
(click)="remove($event)">{{'form.remove' | translate}}
</button>
</div>
</div>
<div ngbDropdownMenu
class="mt-0 dropdown-menu scrollable-dropdown-menu w-100"
aria-haspopup="true"
aria-expanded="false"
aria-labelledby="scrollableDropdownMenuButton">
<div class="scrollable-menu"
aria-labelledby="scrollableDropdownMenuButton"
infiniteScroll
[infiniteScrollDistance]="2"
[infiniteScrollThrottle]="50"
(scrolled)="onScroll()"
[scrollWindow]="false">
<button class="dropdown-item disabled"
*ngIf="optionsList && optionsList.length == 0"
(click)="$event.stopPropagation(); clearFields(); sdRef.close();">{{'form.no-results' | translate}}
</button>
<button class="dropdown-item collection-item"
*ngFor="let listEntry of optionsList"
(click)="$event.stopPropagation(); onSelect(listEntry); sdRef.close();"
title="{{ listEntry.display }}">
{{listEntry.value}}
</button>
<div class="scrollable-dropdown-loading text-center" *ngIf="loading"><p>{{'form.loading' | translate}}</p></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,65 @@
@import "../../../../../../../styles/variables";
.dropdown-toggle::after {
display:none
}
/* enable absolute positioning */
.spinner-addon {
position: relative;
}
/* style fa-spin */
.spinner-addon .fa-spin {
color: map-get($theme-colors, primary);
position: absolute;
margin-top: 3px;
padding: 0;
pointer-events: none;
}
/* align fa-spin */
.left-addon .fa-spin {
left: 0px;
}
.right-addon .fa-spin {
right: 0px;
}
/* add padding */
.left-addon input {
padding-left: $spacer * 2;
}
.right-addon input {
padding-right: $spacer * 2;
}
:host /deep/ .dropdown-menu {
width: 100% !important;
max-height: $dropdown-menu-max-height;
overflow-y: auto !important;
overflow-x: hidden;
}
:host /deep/ .dropdown-item.active,
:host /deep/ .dropdown-item:active,
:host /deep/ .dropdown-item:focus,
:host /deep/ .dropdown-item:hover {
color: $dropdown-link-hover-color !important;
background-color: $dropdown-link-hover-bg !important;
}
//
//.dropdown-menu {
// margin-top: -($spacer * 0.625);
//}
div {
overflow: visible;
//padding-right: 0 !important;
}
//
//button {
// margin-right: $spacer * 0.625;
//}

View File

@@ -0,0 +1,343 @@
// 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 { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
import { 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 { DsDynamicLookupComponent } from './dynamic-lookup.component';
import { DynamicLookupModel } from './dynamic-lookup.model';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { TranslateModule } from '@ngx-translate/core';
import { FormBuilderService } from '../../../form-builder.service';
import { FormService } from '../../../../form.service';
import { FormComponent } from '../../../../form.component';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { By } from '@angular/platform-browser';
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
import { DynamicLookupNameModel } from './dynamic-lookup-name.model';
import { createTestComponent } from '../../../../../testing/utils';
export const LOOKUP_TEST_MODEL_CONFIG = {
authorityOptions: {
closed: false,
metadata: 'lookup',
name: 'RPAuthority',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as AuthorityOptions,
disabled: false,
errorMessages: {required: 'Required field.'},
id: 'lookup',
label: 'Author',
maxOptions: 10,
name: 'lookup',
placeholder: 'Author',
readOnly: false,
required: true,
repeatable: true,
separator: ',',
validators: {required: null},
value: undefined
};
export const LOOKUP_NAME_TEST_MODEL_CONFIG = {
authorityOptions: {
closed: false,
metadata: 'lookup-name',
name: 'RPAuthority',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as AuthorityOptions,
disabled: false,
errorMessages: {required: 'Required field.'},
id: 'lookupName',
label: 'Author',
maxOptions: 10,
name: 'lookupName',
placeholder: 'Author',
readOnly: false,
required: true,
repeatable: true,
separator: ',',
validators: {required: null},
value: undefined
};
export const LOOKUP_TEST_GROUP = new FormGroup({
lookup: new FormControl(),
lookupName: new FormControl()
});
describe('Dynamic Lookup component', () => {
let testComp: TestComponent;
let lookupComp: DsDynamicLookupComponent;
let testFixture: ComponentFixture<TestComponent>;
let lookupFixture: ComponentFixture<DsDynamicLookupComponent>;
let html;
const authorityServiceStub = new AuthorityServiceStub();
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
DynamicFormsCoreModule,
DynamicFormsNGBootstrapUIModule,
FormsModule,
InfiniteScrollModule,
ReactiveFormsModule,
NgbModule.forRoot(),
TranslateModule.forRoot()
],
declarations: [
DsDynamicLookupComponent,
TestComponent,
], // declare the test component
providers: [
ChangeDetectorRef,
DsDynamicLookupComponent,
DynamicFormValidationService,
FormBuilderService,
FormComponent,
FormService,
{provide: AuthorityService, useValue: authorityServiceStub},
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
}));
describe('', () => {
// synchronous beforeEach
beforeEach(() => {
html = `
<ds-dynamic-lookup
[bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-lookup>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
it('should create DsDynamicLookupComponent', inject([DsDynamicLookupComponent], (app: DsDynamicLookupComponent) => {
expect(app).toBeDefined();
}));
});
describe('when model is DynamicLookupModel', () => {
describe('', () => {
beforeEach(() => {
lookupFixture = TestBed.createComponent(DsDynamicLookupComponent);
lookupComp = lookupFixture.componentInstance; // FormComponent test instance
lookupComp.group = LOOKUP_TEST_GROUP;
lookupComp.model = new DynamicLookupModel(LOOKUP_TEST_MODEL_CONFIG);
lookupFixture.detectChanges();
});
it('should render only an input element', () => {
const de = lookupFixture.debugElement.queryAll(By.css('input.form-control'));
expect(de.length).toBe(1);
});
});
describe('and init model value is empty', () => {
beforeEach(() => {
lookupFixture = TestBed.createComponent(DsDynamicLookupComponent);
lookupComp = lookupFixture.componentInstance; // FormComponent test instance
lookupComp.group = LOOKUP_TEST_GROUP;
lookupComp.model = new DynamicLookupModel(LOOKUP_TEST_MODEL_CONFIG);
lookupFixture.detectChanges();
});
it('should init component properly', () => {
expect(lookupComp.firstInputValue).toBe('');
});
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);
lookupComp.firstInputValue = 'test';
lookupFixture.detectChanges();
btnEl.click();
tick();
lookupFixture.detectChanges();
results$.subscribe((results) => {
expect(lookupComp.optionsList).toEqual(results.payload);
})
}));
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 AuthorityValueModel(), {id: 1, display: 'one', value: 1});
spyOn(lookupComp.change, 'emit');
lookupComp.firstInputValue = 'test';
lookupFixture.detectChanges();
btnEl.click();
tick();
lookupFixture.detectChanges();
de = lookupFixture.debugElement.queryAll(By.css('button.dropdown-item'));
const entryEl = de[0].nativeElement;
entryEl.click();
expect(lookupComp.firstInputValue).toEqual('one');
expect(lookupComp.model.value).toEqual(selectedValue);
expect(lookupComp.change.emit).toHaveBeenCalled();
}));
it('should set model.value on input type when AuthorityOptions.closed is false', fakeAsync(() => {
lookupComp.firstInputValue = 'test';
lookupFixture.detectChanges();
lookupComp.onInput(new Event('input'));
expect(lookupComp.model.value).toEqual(new FormFieldMetadataValueObject('test'))
}));
it('should not set model.value on input type when AuthorityOptions.closed is true', () => {
lookupComp.model.authorityOptions.closed = true;
lookupComp.firstInputValue = 'test';
lookupFixture.detectChanges();
lookupComp.onInput(new Event('input'));
expect(lookupComp.model.value).not.toBeDefined();
});
});
describe('and init model value is not empty', () => {
beforeEach(() => {
lookupFixture = TestBed.createComponent(DsDynamicLookupComponent);
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');
lookupFixture.detectChanges();
// spyOn(store, 'dispatch');
});
it('should init component properly', () => {
expect(lookupComp.firstInputValue).toBe('test')
});
});
});
describe('when model is DynamicLookupNameModel', () => {
describe('', () => {
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);
lookupFixture.detectChanges();
// spyOn(store, 'dispatch');
});
it('should render two input element', () => {
const de = lookupFixture.debugElement.queryAll(By.css('input.form-control'));
expect(de.length).toBe(2);
});
});
describe('and init model value is empty', () => {
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);
lookupFixture.detectChanges();
});
it('should select a results entry properly', fakeAsync(() => {
const payload = [
Object.assign(new AuthorityValueModel(), {id: 1, display: 'Name, Lastname', value: 1}),
Object.assign(new AuthorityValueModel(), {id: 2, display: 'NameTwo, LastnameTwo', value: 2}),
];
let de = lookupFixture.debugElement.queryAll(By.css('button'));
const btnEl = de[0].nativeElement;
const selectedValue = Object.assign(new AuthorityValueModel(), {id: 1, display: 'Name, Lastname', value: 1});
spyOn(lookupComp.change, 'emit');
authorityServiceStub.setNewPayload(payload);
lookupComp.firstInputValue = 'test';
lookupFixture.detectChanges();
btnEl.click();
tick();
lookupFixture.detectChanges();
de = lookupFixture.debugElement.queryAll(By.css('button.dropdown-item'));
const entryEl = de[0].nativeElement;
entryEl.click();
expect(lookupComp.firstInputValue).toEqual('Name');
expect(lookupComp.secondInputValue).toEqual('Lastname');
expect(lookupComp.model.value).toEqual(selectedValue);
expect(lookupComp.change.emit).toHaveBeenCalled();
}));
});
describe('and init model value is not empty', () => {
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');
lookupFixture.detectChanges();
});
it('should init component properly', () => {
expect(lookupComp.firstInputValue).toBe('Name');
expect(lookupComp.secondInputValue).toBe('Lastname');
});
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
group: FormGroup = LOOKUP_TEST_GROUP;
inputLookupModelConfig = LOOKUP_TEST_MODEL_CONFIG;
model = new DynamicLookupModel(this.inputLookupModelConfig);
showErrorMessages = false;
}

View File

@@ -0,0 +1,217 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
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 { PageInfo } from '../../../../../../core/shared/page-info.model';
import { Subscription } from 'rxjs/Subscription';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
import { DynamicLookupNameModel } from './dynamic-lookup-name.model';
@Component({
selector: 'ds-dynamic-lookup',
styleUrls: ['./dynamic-lookup.component.scss'],
templateUrl: './dynamic-lookup.component.html'
})
export class DsDynamicLookupComponent implements OnDestroy, OnInit {
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicLookupModel | DynamicLookupNameModel;
@Input() showErrorMessages = false;
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
@Output() change: EventEmitter<any> = new EventEmitter<any>();
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
public firstInputValue = '';
public secondInputValue = '';
public loading = false;
public pageInfo: PageInfo;
public optionsList: any;
protected searchOptions: IntegrationSearchOptions;
protected sub: Subscription;
constructor(private authorityService: AuthorityService,
private cdr: ChangeDetectorRef) {
}
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);
this.model.valueUpdates
.subscribe((value) => {
if (isEmpty(value)) {
this.resetFields();
} else {
this.setInputsValue(this.model.value);
}
});
}
public formatItemForInput(item: any, field: number): string {
if (isUndefined(item) || isNull(item)) {
return '';
}
return (typeof item === 'string') ? item : this.inputFormatter(item, field);
}
// inputFormatter = (x: { display: string }) => x.display;
inputFormatter = (x: { display: string }, y: number) => {
// this.splitValues();
return y === 1 ? this.firstInputValue : this.secondInputValue;
};
onInput(event) {
if (!this.model.authorityOptions.closed) {
if (isNotEmpty(this.getCurrentValue())) {
const currentValue = new FormFieldMetadataValueObject(this.getCurrentValue());
this.onSelect(currentValue);
} else {
this.remove();
}
}
}
onScroll() {
if (!this.loading && this.pageInfo.currentPage <= this.pageInfo.totalPages) {
this.searchOptions.currentPage++;
this.search();
}
}
protected setInputsValue(value) {
if (hasValue(value)) {
let displayValue = value;
if (value instanceof FormFieldMetadataValueObject || value instanceof AuthorityValueModel) {
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 getCurrentValue(): string {
let result = '';
if (!this.isLookupName()) {
result = this.firstInputValue;
} else {
if (isNotEmpty(this.firstInputValue)) {
result = this.firstInputValue;
}
if (isNotEmpty(this.secondInputValue)) {
result = isEmpty(result)
? this.secondInputValue
: this.firstInputValue + (this.model as DynamicLookupNameModel).separator + ' ' + this.secondInputValue;
}
}
return result;
}
search() {
this.optionsList = null;
this.pageInfo = null;
// Query
this.searchOptions.query = this.getCurrentValue();
this.loading = true;
this.authorityService.getEntriesByName(this.searchOptions)
.distinctUntilChanged()
.subscribe((object: IntegrationData) => {
this.optionsList = object.payload;
this.pageInfo = object.pageInfo;
this.loading = false;
this.cdr.detectChanges();
});
}
clearFields() {
// Clear inputs whether there is no results and authority is closed
if (this.model.authorityOptions.closed) {
this.resetFields();
}
}
protected resetFields() {
this.firstInputValue = '';
if (this.isLookupName()) {
this.secondInputValue = '';
}
}
onSelect(event) {
this.group.markAsDirty();
this.model.valueUpdates.next(event);
this.setInputsValue(event);
this.change.emit(event);
this.optionsList = null;
this.pageInfo = null;
}
isInputDisabled() {
return this.model.authorityOptions.closed && hasValue(this.model.value);
}
isLookupName() {
return (this.model instanceof DynamicLookupNameModel);
}
isSearchDisabled() {
// if (this.firstInputValue === ''
// && (this.isLookupName ? this.secondInputValue === '' : true)) {
// return true;
// }
// return false;
return isEmpty(this.firstInputValue);
}
remove() {
this.group.markAsPristine();
this.model.valueUpdates.next(null);
this.change.emit(null);
}
openChange(isOpened: boolean) {
if (!isOpened) {
if (this.model.authorityOptions.closed) {
this.setInputsValue('');
}
}
}
onBlurEvent(event: Event) {
this.blur.emit(event);
}
onFocusEvent(event) {
this.focus.emit(event);
}
ngOnDestroy() {
if (hasValue(this.sub)) {
this.sub.unsubscribe();
}
}
}

View File

@@ -0,0 +1,26 @@
import { AUTOCOMPLETE_OFF, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model';
export const DYNAMIC_FORM_CONTROL_TYPE_LOOKUP = 'LOOKUP';
export interface DynamicLookupModelConfig extends DsDynamicInputModelConfig {
maxOptions?: number;
value?: any;
}
export class DynamicLookupModel extends DsDynamicInputModel {
@serializable() maxOptions: number;
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_LOOKUP;
@serializable() value: any;
constructor(config: DynamicLookupModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.autoComplete = AUTOCOMPLETE_OFF;
this.maxOptions = config.maxOptions || 10;
this.valueUpdates.next(config.value);
}
}

View File

@@ -0,0 +1,43 @@
<div #sdRef="ngbDropdown" ngbDropdown class="input-group w-100">
<input class="form-control"
[attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[name]="model.name"
[readonly]="model.readOnly"
[type]="model.inputType"
[value]="formatItemForInput(model.value)"
(blur)="onBlur($event)"
(click)="$event.stopPropagation(); openDropdown(sdRef);"
(focus)="onFocus($event)"
(keypress)="$event.preventDefault()">
<button aria-describedby="collectionControlsMenuLabel"
class="ds-form-input-btn btn btn-outline-primary"
id="scrollableDropdownMenuButton_{{model.id}}"
ngbDropdownToggle
[disabled]="model.readOnly"></button>
<div ngbDropdownMenu
class="dropdown-menu scrollable-dropdown-menu w-100"
aria-haspopup="true"
aria-expanded="false"
aria-labelledby="scrollableDropdownMenuButton">
<div class="scrollable-menu"
aria-labelledby="scrollableDropdownMenuButton"
infiniteScroll
[infiniteScrollDistance]="2"
[infiniteScrollThrottle]="50"
(scrolled)="onScroll()"
[scrollWindow]="false">
<button class="dropdown-item disabled" *ngIf="optionsList && optionsList.length == 0">{{'form.no-results' | translate}}</button>
<button class="dropdown-item collection-item" *ngFor="let listEntry of optionsList" (click)="onSelect(listEntry)" title="{{ listEntry.display }}">
{{inputFormatter(listEntry)}}
</button>
<div class="scrollable-dropdown-loading text-center" *ngIf="loading"><p>{{'form.loading' | translate}}</p></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,26 @@
@import '../../../../form.component';
.scrollable-menu {
height: auto;
max-height: $dropdown-menu-max-height;
overflow-x: hidden;
}
.collection-item {
border-bottom: $dropdown-border-width solid $dropdown-border-color;
}
.scrollable-dropdown-loading {
background-color: map-get($theme-colors, primary);
color: white;
height: $spacer * 2 !important;
line-height: $spacer * 2;
position: sticky;
bottom: 0;
}
.scrollable-dropdown-menu {
left: 0 !important;
margin-bottom: $spacer;
z-index: 1000;
}

View File

@@ -0,0 +1,235 @@
// 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 { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
import { DynamicFormsCoreModule } 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 { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { TranslateModule } from '@ngx-translate/core';
import { DsDynamicScrollableDropdownComponent } from './dynamic-scrollable-dropdown.component';
import { DynamicScrollableDropdownModel } from './dynamic-scrollable-dropdown.model';
import { DsDynamicTypeaheadComponent } from '../typeahead/dynamic-typeahead.component';
import { DynamicTypeaheadModel } from '../typeahead/dynamic-typeahead.model';
import { TYPEAHEAD_TEST_GROUP, TYPEAHEAD_TEST_MODEL_CONFIG } from '../typeahead/dynamic-typeahead.component.spec';
import { By } from '@angular/platform-browser';
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
import { hasClass, createTestComponent } from '../../../../../testing/utils';
export const SD_TEST_GROUP = new FormGroup({
dropdown: new FormControl(),
});
export const SD_TEST_MODEL_CONFIG = {
authorityOptions: {
closed: false,
metadata: 'dropdown',
name: 'common_iso_languages',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as AuthorityOptions,
disabled: false,
errorMessages: {required: 'Required field.'},
id: 'dropdown',
label: 'Language',
maxOptions: 10,
name: 'dropdown',
placeholder: 'Language',
readOnly: false,
required: false,
repeatable: false,
value: undefined
};
describe('Dynamic Dynamic Scrollable Dropdown component', () => {
let testComp: TestComponent;
let scrollableDropdownComp: DsDynamicScrollableDropdownComponent;
let testFixture: ComponentFixture<TestComponent>;
let scrollableDropdownFixture: ComponentFixture<DsDynamicScrollableDropdownComponent>;
let html;
let modelValue;
const authorityServiceStub = new AuthorityServiceStub();
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
DynamicFormsCoreModule,
DynamicFormsNGBootstrapUIModule,
FormsModule,
InfiniteScrollModule,
ReactiveFormsModule,
NgbModule.forRoot(),
TranslateModule.forRoot()
],
declarations: [
DsDynamicScrollableDropdownComponent,
TestComponent,
], // declare the test component
providers: [
ChangeDetectorRef,
DsDynamicScrollableDropdownComponent,
{provide: AuthorityService, useValue: authorityServiceStub},
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
}));
describe('', () => {
// synchronous beforeEach
beforeEach(() => {
html = `
<ds-dynamic-scrollable-dropdown [bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-scrollable-dropdown>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
it('should create DsDynamicScrollableDropdownComponent', inject([DsDynamicScrollableDropdownComponent], (app: DsDynamicScrollableDropdownComponent) => {
expect(app).toBeDefined();
}));
});
describe('', () => {
describe('when init model value is empty', () => {
beforeEach(() => {
scrollableDropdownFixture = TestBed.createComponent(DsDynamicScrollableDropdownComponent);
scrollableDropdownComp = scrollableDropdownFixture.componentInstance; // FormComponent test instance
scrollableDropdownComp.group = SD_TEST_GROUP;
scrollableDropdownComp.model = new DynamicScrollableDropdownModel(SD_TEST_MODEL_CONFIG);
scrollableDropdownFixture.detectChanges();
});
afterEach(() => {
scrollableDropdownFixture.destroy();
scrollableDropdownComp = null;
});
it('should init component properly', () => {
const results$ = authorityServiceStub.getEntriesByName({} as any);
expect(scrollableDropdownComp.optionsList).toBeDefined();
results$.subscribe((results) => {
expect(scrollableDropdownComp.optionsList).toEqual(results.payload);
})
});
it('should display dropdown menu entries', () => {
const de = scrollableDropdownFixture.debugElement.query(By.css('button.ds-form-input-btn'));
const btnEl = de.nativeElement;
const deMenu = scrollableDropdownFixture.debugElement.query(By.css('div.scrollable-dropdown-menu'));
const menuEl = deMenu.nativeElement;
btnEl.click();
scrollableDropdownFixture.detectChanges();
expect(hasClass(menuEl, 'show')).toBeTruthy();
});
it('should fetch the next set of results when the user scroll to the end of the list', fakeAsync(() => {
scrollableDropdownComp.pageInfo.currentPage = 1;
scrollableDropdownComp.pageInfo.totalPages = 2;
scrollableDropdownFixture.detectChanges();
scrollableDropdownComp.onScroll();
tick();
expect(scrollableDropdownComp.optionsList.length).toBe(4);
}));
it('should select a results entry properly', fakeAsync(() => {
const selectedValue = Object.assign(new AuthorityValueModel(), {id: 1, display: 'one', value: 1});
let de: any = scrollableDropdownFixture.debugElement.query(By.css('button.ds-form-input-btn'));
let btnEl = de.nativeElement;
de = scrollableDropdownFixture.debugElement.query(By.css('div.scrollable-dropdown-menu'));
const menuEl = de.nativeElement;
btnEl.click();
scrollableDropdownFixture.detectChanges();
de = scrollableDropdownFixture.debugElement.queryAll(By.css('button.dropdown-item'));
btnEl = de[0].nativeElement;
btnEl.click();
scrollableDropdownFixture.detectChanges();
expect((scrollableDropdownComp.model as any).value).toEqual(selectedValue);
}));
it('should emit blur Event onBlur', () => {
spyOn(scrollableDropdownComp.blur, 'emit');
scrollableDropdownComp.onBlur(new Event('blur'));
expect(scrollableDropdownComp.blur.emit).toHaveBeenCalled();
});
it('should emit focus Event onFocus', () => {
spyOn(scrollableDropdownComp.focus, 'emit');
scrollableDropdownComp.onFocus(new Event('focus'));
expect(scrollableDropdownComp.focus.emit).toHaveBeenCalled();
});
});
describe('when init model value is not empty', () => {
beforeEach(() => {
scrollableDropdownFixture = TestBed.createComponent(DsDynamicScrollableDropdownComponent);
scrollableDropdownComp = scrollableDropdownFixture.componentInstance; // FormComponent test instance
scrollableDropdownComp.group = SD_TEST_GROUP;
modelValue = Object.assign(new AuthorityValueModel(), {id: 1, display: 'one', value: 1});
scrollableDropdownComp.model = new DynamicScrollableDropdownModel(SD_TEST_MODEL_CONFIG);
scrollableDropdownComp.model.value = modelValue;
scrollableDropdownFixture.detectChanges();
});
afterEach(() => {
scrollableDropdownFixture.destroy();
scrollableDropdownComp = null;
});
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);
})
});
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
group: FormGroup = SD_TEST_GROUP;
model = new DynamicScrollableDropdownModel(SD_TEST_MODEL_CONFIG);
showErrorMessages = false;
}

View File

@@ -0,0 +1,92 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { FormGroup } from '@angular/forms';
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 { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap';
@Component({
selector: 'ds-dynamic-scrollable-dropdown',
styleUrls: ['./dynamic-scrollable-dropdown.component.scss'],
templateUrl: './dynamic-scrollable-dropdown.component.html'
})
export class DsDynamicScrollableDropdownComponent implements OnInit {
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicScrollableDropdownModel;
@Input() showErrorMessages = false;
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
@Output() change: EventEmitter<any> = new EventEmitter<any>();
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
public loading = false;
public pageInfo: PageInfo;
public optionsList: any;
protected searchOptions: IntegrationSearchOptions;
constructor(private authorityService: AuthorityService, private cdr: ChangeDetectorRef) {}
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)
.subscribe((object: IntegrationData) => {
this.optionsList = object.payload;
this.pageInfo = object.pageInfo;
this.cdr.detectChanges();
})
}
public formatItemForInput(item: any): string {
if (isUndefined(item) || isNull(item)) { return '' }
return (typeof item === 'string') ? item : this.inputFormatter(item);
}
inputFormatter = (x: AuthorityValueModel) => x.display || x.value;
openDropdown(sdRef: NgbDropdown) {
if (!this.model.readOnly) {
sdRef.open();
}
}
onScroll() {
if (!this.loading && this.pageInfo.currentPage <= this.pageInfo.totalPages) {
this.loading = true;
this.searchOptions.currentPage++;
this.authorityService.getEntriesByName(this.searchOptions)
.do(() => this.loading = false)
.subscribe((object: IntegrationData) => {
this.optionsList = this.optionsList.concat(object.payload);
this.pageInfo = object.pageInfo;
this.cdr.detectChanges();
})
}
}
onBlur(event: Event) {
this.blur.emit(event);
}
onFocus(event) {
this.focus.emit(event);
}
onSelect(event) {
this.group.markAsDirty();
this.model.valueUpdates.next(event);
this.change.emit(event);
}
}

View File

@@ -0,0 +1,27 @@
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';
export const DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN = 'SCROLLABLE_DROPDOWN';
export interface DynamicScrollableDropdownModelConfig extends DsDynamicInputModelConfig {
authorityOptions: AuthorityOptions;
maxOptions?: number;
value?: any;
}
export class DynamicScrollableDropdownModel extends DsDynamicInputModel {
@serializable() maxOptions: number;
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN;
constructor(config: DynamicScrollableDropdownModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.autoComplete = AUTOCOMPLETE_OFF;
this.authorityOptions = config.authorityOptions;
this.maxOptions = config.maxOptions || 10;
}
}

View File

@@ -0,0 +1,46 @@
<ng-template #rt let-r="result" let-t="term">
{{ r.display }}
</ng-template>
<ds-chips [chips]="chips" [wrapperClass]="'border-bottom border-light'">
<input *ngIf="!searchOptions"
class="border-0 form-control-plaintext tag-input flex-grow-1 mt-1 mb-1 chips-sort-ignore"
type="text"
[class.pl-3]="chips.hasItems()"
[placeholder]="model.placeholder"
[readonly]="model.readOnly"
[(ngModel)]="currentValue"
(blur)="onBlur($event)"
(keypress)="preventEventsPropagation($event)"
(keydown)="preventEventsPropagation($event)"
(keyup)="onKeyUp($event)" />
<input *ngIf="searchOptions"
class="border-0 form-control-plaintext tag-input flex-grow-1 mt-1 mb-1 chips-sort-ignore"
type="text"
[(ngModel)]="currentValue"
[attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages"
[class.pl-3]="chips.hasItems()"
[dynamicId]="bindId && model.id"
[inputFormatter]="formatter"
[name]="model.name"
[ngbTypeahead]="search"
[placeholder]="model.placeholder"
[readonly]="model.readOnly"
[resultTemplate]="rt"
[type]="model.inputType"
(blur)="onBlur($event)"
(focus)="onFocus($event)"
(change)="$event.stopPropagation()"
(input)="onInput($event)"
(selectItem)="onSelectItem($event)"
(keypress)="preventEventsPropagation($event)"
(keydown)="preventEventsPropagation($event)"
(keyup)="onKeyUp($event)"/>
<i *ngIf="searching" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw text-primary position-absolute mt-1 p-0" aria-hidden="true"></i>
</ds-chips>

View File

@@ -0,0 +1,33 @@
@import "../../../../../../../styles/variables";
/* style fa-spin */
.fa-spin {
pointer-events: none;
right: 0;
}
.chips-left {
left: 0;
padding-right: 100%;
}
:host /deep/ .dropdown-menu {
width: 100% !important;
max-height: $dropdown-menu-max-height;
overflow-y: scroll;
overflow-x: hidden;
left: 0 !important;
margin-top: $spacer !important;
}
:host /deep/ .dropdown-item.active,
:host /deep/ .dropdown-item:active,
:host /deep/ .dropdown-item:focus,
:host /deep/ .dropdown-item:hover {
color: $dropdown-link-hover-color !important;
background-color: $dropdown-link-hover-bg !important;
}
.tag-input {
outline: none;
width: auto !important;
}

View File

@@ -0,0 +1,293 @@
// 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, flush, inject, TestBed, } from '@angular/core/testing';
import { DynamicFormsCoreModule } from '@ng-dynamic-forms/core';
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
import { NgbModule, NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of'
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
import { AuthorityService } from '../../../../../../core/integration/authority.service';
import { AuthorityServiceStub } from '../../../../../testing/authority-service-stub';
import { DsDynamicTagComponent } from './dynamic-tag.component';
import { DynamicTagModel } from './dynamic-tag.model';
import { GlobalConfig } from '../../../../../../../config/global-config.interface';
import { GLOBAL_CONFIG } from '../../../../../../../config';
import { Chips } from '../../../../../chips/models/chips.model';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
import { createTestComponent } from '../../../../../testing/utils';
function createKeyUpEvent(key: number) {
/* tslint:disable:no-empty */
const event = {
keyCode: key, preventDefault: () => {
}, stopPropagation: () => {
}
};
/* tslint:enable:no-empty */
spyOn(event, 'preventDefault');
spyOn(event, 'stopPropagation');
return event;
}
export const TAG_TEST_GROUP = new FormGroup({
tag: new FormControl(),
});
export const TAG_TEST_MODEL_CONFIG = {
authorityOptions: {
closed: false,
metadata: 'tag',
name: 'common_iso_languages',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
} as AuthorityOptions,
disabled: false,
id: 'tag',
label: 'Keywords',
minChars: 3,
name: 'tag',
placeholder: 'Keywords',
readOnly: false,
required: false,
repeatable: false
};
describe('DsDynamicTagComponent test suite', () => {
let testComp: TestComponent;
let tagComp: DsDynamicTagComponent;
let testFixture: ComponentFixture<TestComponent>;
let tagFixture: ComponentFixture<DsDynamicTagComponent>;
let html;
let chips: Chips;
let modelValue: any;
// async beforeEach
beforeEach(async(() => {
const authorityServiceStub = new AuthorityServiceStub();
TestBed.configureTestingModule({
imports: [
DynamicFormsCoreModule,
DynamicFormsNGBootstrapUIModule,
FormsModule,
NgbModule.forRoot(),
ReactiveFormsModule,
],
declarations: [
DsDynamicTagComponent,
TestComponent,
], // declare the test component
providers: [
ChangeDetectorRef,
DsDynamicTagComponent,
{provide: AuthorityService, useValue: authorityServiceStub},
{provide: GLOBAL_CONFIG, useValue: {} as GlobalConfig},
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
}));
describe('', () => {
// synchronous beforeEach
beforeEach(() => {
html = `
<ds-dynamic-tag [bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-tag>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
it('should create DsDynamicTagComponent', inject([DsDynamicTagComponent], (app: DsDynamicTagComponent) => {
expect(app).toBeDefined();
}));
});
describe('when authorityOptions are setted', () => {
describe('and init model value is empty', () => {
beforeEach(() => {
tagFixture = TestBed.createComponent(DsDynamicTagComponent);
tagComp = tagFixture.componentInstance; // FormComponent test instance
tagComp.group = TAG_TEST_GROUP;
tagComp.model = new DynamicTagModel(TAG_TEST_MODEL_CONFIG);
tagFixture.detectChanges();
});
afterEach(() => {
tagFixture.destroy();
tagComp = null;
});
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();
tagComp.search(Observable.of('test')).subscribe(() => {
expect((tagComp as any).authorityService.getEntriesByName).toHaveBeenCalled();
});
}));
it('should select a results entry properly', fakeAsync(() => {
modelValue = [
Object.assign(new AuthorityValueModel(), {id: 1, display: 'Name, Lastname', value: 1})
];
const event: NgbTypeaheadSelectItemEvent = {
item: Object.assign(new AuthorityValueModel(), {id: 1, display: 'Name, Lastname', value: 1}),
preventDefault: () => {
return;
}
};
spyOn(tagComp.change, 'emit');
tagComp.onSelectItem(event);
tagFixture.detectChanges();
flush();
expect(tagComp.chips.getChipsItems()).toEqual(modelValue);
expect(tagComp.model.value).toEqual(modelValue);
expect(tagComp.currentValue).toBeNull();
expect(tagComp.change.emit).toHaveBeenCalled();
}));
it('should emit blur Event onBlur', () => {
spyOn(tagComp.blur, 'emit');
tagComp.onBlur(new Event('blur'));
expect(tagComp.blur.emit).toHaveBeenCalled();
});
it('should emit focus Event onFocus', () => {
spyOn(tagComp.focus, 'emit');
tagComp.onFocus(new Event('focus'));
expect(tagComp.focus.emit).toHaveBeenCalled();
});
it('should emit change Event onBlur when currentValue is not empty', fakeAsync(() => {
tagComp.currentValue = 'test value';
tagFixture.detectChanges();
spyOn(tagComp.blur, 'emit');
spyOn(tagComp.change, 'emit');
tagComp.onBlur(new Event('blur'));
tagFixture.detectChanges();
flush();
expect(tagComp.change.emit).toHaveBeenCalled();
expect(tagComp.blur.emit).toHaveBeenCalled();
}));
});
describe('and init model value is not empty', () => {
beforeEach(() => {
tagFixture = TestBed.createComponent(DsDynamicTagComponent);
tagComp = tagFixture.componentInstance; // FormComponent test instance
tagComp.group = TAG_TEST_GROUP;
tagComp.model = new DynamicTagModel(TAG_TEST_MODEL_CONFIG);
modelValue = [
new FormFieldMetadataValueObject('a', null, 'test001'),
new FormFieldMetadataValueObject('b', null, 'test002'),
new FormFieldMetadataValueObject('c', null, 'test003'),
];
tagComp.model.value = modelValue;
tagFixture.detectChanges();
});
afterEach(() => {
tagFixture.destroy();
tagComp = null;
});
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 setted', () => {
describe('and init model value is empty', () => {
beforeEach(() => {
tagFixture = TestBed.createComponent(DsDynamicTagComponent);
tagComp = tagFixture.componentInstance; // FormComponent test instance
tagComp.group = TAG_TEST_GROUP;
const config = TAG_TEST_MODEL_CONFIG;
config.authorityOptions = null;
tagComp.model = new DynamicTagModel(config);
tagFixture.detectChanges();
});
afterEach(() => {
tagFixture.destroy();
tagComp = null;
});
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(() => {
let event = createKeyUpEvent(13);
tagComp.currentValue = 'test value';
tagFixture.detectChanges();
tagComp.onKeyUp(event);
flush();
expect(tagComp.model.value).toEqual(['test value']);
expect(tagComp.currentValue).toBeNull();
event = createKeyUpEvent(188);
tagComp.currentValue = 'test value';
tagFixture.detectChanges();
tagComp.onKeyUp(event);
flush();
expect(tagComp.model.value).toEqual(['test value']);
expect(tagComp.currentValue).toBeNull();
}));
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
group: FormGroup = TAG_TEST_GROUP;
model = new DynamicTagModel(TAG_TEST_MODEL_CONFIG);
showErrorMessages = false;
}

View File

@@ -0,0 +1,182 @@
import { ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import { NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap';
import { AuthorityService } from '../../../../../../core/integration/authority.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 { isEqual } from 'lodash';
import { GlobalConfig } from '../../../../../../../config/global-config.interface';
import { GLOBAL_CONFIG } from '../../../../../../../config';
@Component({
selector: 'ds-dynamic-tag',
styleUrls: ['./dynamic-tag.component.scss'],
templateUrl: './dynamic-tag.component.html'
})
export class DsDynamicTagComponent implements OnInit {
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicTagModel;
@Input() showErrorMessages = false;
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
@Output() change: EventEmitter<any> = new EventEmitter<any>();
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
chips: Chips;
hasAuthority: boolean;
searching = false;
searchOptions: IntegrationSearchOptions;
searchFailed = false;
hideSearchingWhenUnsubscribed = new Observable(() => () => this.changeSearchingStatus(false));
currentValue: any;
formatter = (x: { display: string }) => x.display;
search = (text$: Observable<string>) =>
text$
.debounceTime(300)
.distinctUntilChanged()
.do(() => this.changeSearchingStatus(true))
.switchMap((term) => {
if (term === '' || term.length < this.model.minChars) {
return Observable.of({list: []});
} else {
this.searchOptions.query = term;
return this.authorityService.getEntriesByName(this.searchOptions)
.map((authorities) => {
// @TODO Pagination for authority is not working, to refactor when it will be fixed
return {
list: authorities.payload,
pageInfo: authorities.pageInfo
};
})
.do(() => this.searchFailed = false)
.catch(() => {
this.searchFailed = true;
return Observable.of({list: []});
});
}
})
.map((results) => results.list)
.do(() => this.changeSearchingStatus(false))
.merge(this.hideSearchingWhenUnsubscribed);
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
private authorityService: AuthorityService,
private cdr: ChangeDetectorRef) {
}
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.chips = new Chips(this.model.value, 'display');
this.chips.chipsItems
.subscribe((subItems: any[]) => {
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);
}
})
}
changeSearchingStatus(status: boolean) {
this.searching = status;
this.cdr.detectChanges();
}
onInput(event) {
if (event.data) {
this.group.markAsDirty();
}
this.cdr.detectChanges();
}
onBlur(event: Event) {
if (isNotEmpty(this.currentValue)) {
this.addTagsToChips();
}
this.blur.emit(event);
}
onFocus(event) {
this.focus.emit(event);
}
onSelectItem(event: NgbTypeaheadSelectItemEvent) {
this.chips.add(event.item);
// this.group.controls[this.model.id].setValue(this.model.value);
this.updateModel(event);
setTimeout(() => {
// Reset the input text after x ms, mandatory or the formatter overwrite it
this.currentValue = null;
this.cdr.detectChanges();
}, 50);
}
updateModel(event) {
this.model.valueUpdates.next(this.chips.getChipsItems());
this.change.emit(event);
}
onKeyUp(event) {
if (event.keyCode === 13 || event.keyCode === 188) {
event.preventDefault();
// Key: Enter or ',' or ';'
this.addTagsToChips();
event.stopPropagation();
}
}
preventEventsPropagation(event) {
event.stopPropagation();
if (event.keyCode === 13) {
event.preventDefault();
}
}
private addTagsToChips() {
if (!this.hasAuthority || !this.model.authorityOptions.closed) {
let res: string[] = [];
res = this.currentValue.split(',');
const res1 = [];
res.forEach((item) => {
item.split(';').forEach((i) => {
res1.push(i);
});
});
res1.forEach((c) => {
c = c.trim();
if (c.length > 0) {
this.chips.add(c);
}
});
// this.currentValue = '';
setTimeout(() => {
// Reset the input text after x ms, mandatory or the formatter overwrite it
this.currentValue = null;
this.cdr.detectChanges();
}, 50);
this.updateModel(event);
}
}
}

View File

@@ -0,0 +1,28 @@
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';
export const DYNAMIC_FORM_CONTROL_TYPE_TAG = 'TAG';
export interface DynamicTagModelConfig extends DsDynamicInputModelConfig {
minChars?: number;
value?: any;
}
export class DynamicTagModel extends DsDynamicInputModel {
@serializable() minChars: number;
@serializable() value: any[];
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_TAG;
constructor(config: DynamicTagModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.autoComplete = AUTOCOMPLETE_OFF;
this.minChars = config.minChars || 3;
const value = config.value || [];
this.valueUpdates.next(value)
}
}

View File

@@ -0,0 +1,25 @@
<ng-template #rt let-r="result" let-t="term">
{{ r.display}}
</ng-template>
<div class="position-relative right-addon">
<i *ngIf="searching" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw text-primary position-absolute mt-1 p-0" aria-hidden="true"></i>
<input class="form-control"
[attr.autoComplete]="model.autoComplete"
[class.is-invalid]="showErrorMessages"
[dynamicId]="bindId && model.id"
[inputFormatter]="formatter"
[name]="model.name"
[ngbTypeahead]="search"
[placeholder]="model.placeholder"
[readonly]="model.readOnly"
[resultTemplate]="rt"
[type]="model.inputType"
[(ngModel)]="currentValue"
(blur)="onBlur($event)"
(focus)="onFocus($event)"
(change)="onChange($event)"
(input)="onInput($event)"
(selectItem)="onSelectItem($event)">
<div class="invalid-feedback" *ngIf="searchFailed">Sorry, suggestions could not be loaded.</div>
</div>

View File

@@ -0,0 +1,25 @@
@import "../../../../../../../styles/variables";
/* style fa-spin */
.fa-spin {
pointer-events: none;
}
/* align fa-spin */
.left-addon .fa-spin { left: 0;}
.right-addon .fa-spin { right: 0;}
:host /deep/ .dropdown-menu {
width: 100% !important;
max-height: $dropdown-menu-max-height;
overflow-y: auto !important;
overflow-x: hidden;
}
:host /deep/ .dropdown-item.active,
:host /deep/ .dropdown-item:active,
:host /deep/ .dropdown-item:focus,
:host /deep/ .dropdown-item:hover {
color: $dropdown-link-hover-color !important;
background-color: $dropdown-link-hover-bg !important;
}

View File

@@ -0,0 +1,224 @@
// 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, } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
import { DynamicFormsCoreModule } 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 { GlobalConfig } from '../../../../../../../config/global-config.interface';
import { GLOBAL_CONFIG } from '../../../../../../../config';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
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';
export const TYPEAHEAD_TEST_GROUP = new FormGroup({
typeahead: new FormControl(),
});
export const 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();
TestBed.configureTestingModule({
imports: [
DynamicFormsCoreModule,
DynamicFormsNGBootstrapUIModule,
FormsModule,
NgbModule.forRoot(),
ReactiveFormsModule,
],
declarations: [
DsDynamicTypeaheadComponent,
TestComponent,
], // declare the test component
providers: [
ChangeDetectorRef,
DsDynamicTypeaheadComponent,
{provide: AuthorityService, useValue: authorityServiceStub},
{provide: GLOBAL_CONFIG, useValue: {} as GlobalConfig},
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
}));
describe('', () => {
// synchronous beforeEach
beforeEach(() => {
html = `
<ds-dynamic-typeahead [bindId]="bindId"
[group]="group"
[model]="model"
[showErrorMessages]="showErrorMessages"
(blur)="onBlur($event)"
(change)="onValueChange($event)"
(focus)="onFocus($event)"></ds-dynamic-typeahead>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
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(Observable.of('test')).subscribe(() => {
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.model as any).value).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', () => {
spyOn(typeaheadComp.blur, 'emit');
typeaheadComp.onBlur(new Event('blur'));
expect(typeaheadComp.blur.emit).toHaveBeenCalled();
});
it('should emit change Event onBlur when AuthorityOptions.closed is false', () => {
typeaheadComp.inputValue = 'test value';
typeaheadFixture.detectChanges();
spyOn(typeaheadComp.blur, 'emit');
spyOn(typeaheadComp.change, 'emit');
typeaheadComp.onBlur(new Event('blur'));
// expect(typeaheadComp.change.emit).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);
showErrorMessages = false;
}

View File

@@ -0,0 +1,123 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Observable } from 'rxjs/Observable';
import { 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 } from '../../../../../empty.util';
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
@Component({
selector: 'ds-dynamic-typeahead',
styleUrls: ['./dynamic-typeahead.component.scss'],
templateUrl: './dynamic-typeahead.component.html'
})
export class DsDynamicTypeaheadComponent implements OnInit {
@Input() bindId = true;
@Input() group: FormGroup;
@Input() model: DynamicTypeaheadModel;
@Input() showErrorMessages = false;
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
@Output() change: EventEmitter<any> = new EventEmitter<any>();
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
searching = false;
searchOptions: IntegrationSearchOptions;
searchFailed = false;
hideSearchingWhenUnsubscribed = new Observable(() => () => this.changeSearchingStatus(false));
currentValue: any;
inputValue: any;
formatter = (x: { display: string }) => {
return (typeof x === 'object') ? x.display : x
};
search = (text$: Observable<string>) =>
text$
.debounceTime(300)
.distinctUntilChanged()
.do(() => this.changeSearchingStatus(true))
.switchMap((term) => {
if (term === '' || term.length < this.model.minChars) {
return Observable.of({list: []});
} else {
this.searchOptions.query = term;
return this.authorityService.getEntriesByName(this.searchOptions)
.map((authorities) => {
// @TODO Pagination for authority is not working, to refactor when it will be fixed
return {
list: authorities.payload,
pageInfo: authorities.pageInfo
};
})
.do(() => this.searchFailed = false)
.catch(() => {
this.searchFailed = true;
return Observable.of({list: []});
});
}
})
.map((results) => results.list)
.do(() => this.changeSearchingStatus(false))
.merge(this.hideSearchingWhenUnsubscribed);
constructor(private authorityService: AuthorityService, private cdr: ChangeDetectorRef) {
}
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
.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)) {
const valueObj = new FormFieldMetadataValueObject(event.target.value);
this.inputValue = valueObj;
this.model.valueUpdates.next(this.inputValue);
}
}
onBlur(event: Event) {
if (!this.model.authorityOptions.closed && isNotEmpty(this.inputValue)) {
this.change.emit(this.inputValue);
this.inputValue = null;
}
this.blur.emit(event);
}
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);
}
}

View File

@@ -0,0 +1,24 @@
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 interface DsDynamicTypeaheadModelConfig extends DsDynamicInputModelConfig {
minChars?: number;
value?: any;
}
export class DynamicTypeaheadModel extends DsDynamicInputModel {
@serializable() minChars: number;
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD;
constructor(config: DsDynamicTypeaheadModelConfig, layout?: DynamicFormControlLayout) {
super(config, layout);
this.autoComplete = AUTOCOMPLETE_OFF;
this.minChars = config.minChars || 3;
}
}

View File

@@ -0,0 +1,830 @@
import { inject, TestBed } from '@angular/core/testing';
import {
FormArray,
FormControl,
FormGroup,
NG_ASYNC_VALIDATORS,
NG_VALIDATORS,
ReactiveFormsModule
} from '@angular/forms';
import {
DynamicCheckboxGroupModel,
DynamicCheckboxModel,
DynamicColorPickerModel,
DynamicDatePickerModel,
DynamicEditorModel,
DynamicFileUploadModel, DynamicFormArrayGroupModel,
DynamicFormArrayModel,
DynamicFormControlModel,
DynamicFormControlValue,
DynamicFormGroupModel,
DynamicFormService,
DynamicFormValidationService,
DynamicFormValueControlModel,
DynamicInputModel,
DynamicRadioGroupModel,
DynamicRatingModel,
DynamicSelectModel,
DynamicSliderModel,
DynamicSwitchModel,
DynamicTextAreaModel,
DynamicTimePickerModel
} from '@ng-dynamic-forms/core';
import { DynamicTagModel } from './ds-dynamic-form-ui/models/tag/dynamic-tag.model';
import { DynamicListCheckboxGroupModel } from './ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model';
import { DynamicQualdropModel } from './ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model';
import { DynamicScrollableDropdownModel } from './ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
import { DynamicGroupModel } from './ds-dynamic-form-ui/models/dynamic-group/dynamic-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 { DynamicListRadioGroupModel } from './ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model';
import { AuthorityOptions } from '../../../core/integration/models/authority-options.model';
import { FormFieldModel } from './models/form-field.model';
import { FormRowModel, SubmissionFormsModel } from '../../../core/shared/config/config-submission-forms.model';
import { FormBuilderService } from './form-builder.service';
import { DynamicRowGroupModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-group-model';
import { DsDynamicInputModel } from './ds-dynamic-form-ui/models/ds-dynamic-input.model';
import { FormFieldMetadataValueObject } from './models/form-field-metadata-value.model';
import { DynamicConcatModel } from './ds-dynamic-form-ui/models/ds-dynamic-concat.model';
import { DynamicLookupNameModel } from './ds-dynamic-form-ui/models/lookup/dynamic-lookup-name.model';
import { DynamicRowArrayModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-array-model';
describe('FormBuilderService test suite', () => {
let testModel: DynamicFormControlModel[];
let testFormConfiguration: SubmissionFormsModel;
let service: FormBuilderService;
function testValidator() {
return {testValidator: {valid: true}};
}
function testAsyncValidator() {
return new Promise<boolean>((resolve) => setTimeout(() => resolve(true), 0));
}
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ReactiveFormsModule],
providers: [
FormBuilderService,
DynamicFormService,
DynamicFormValidationService,
{provide: NG_VALIDATORS, useValue: testValidator, multi: true},
{provide: NG_ASYNC_VALIDATORS, useValue: testAsyncValidator, multi: true}
]
});
const authorityOptions: AuthorityOptions = {
closed: false,
metadata: 'list',
name: 'type_programme',
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
};
testModel = [
new DynamicSelectModel<string>(
{
id: 'testSelect',
options: [
{
label: 'Option 1',
value: 'option-1'
},
{
label: 'Option 2',
value: 'option-2'
}
],
value: 'option-3'
}
),
new DynamicInputModel(
{
id: 'testInput',
mask: ['(', /[1-9]/, /\d/, /\d/, ')', ' ', /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/],
}
),
new DynamicCheckboxGroupModel(
{
id: 'testCheckboxGroup',
group: [
new DynamicCheckboxModel(
{
id: 'testCheckboxGroup1',
value: true
}
),
new DynamicCheckboxModel(
{
id: 'testCheckboxGroup2',
value: true
}
)
]
}
),
new DynamicRadioGroupModel<string>(
{
id: 'testRadioGroup',
options: [
{
label: 'Option 1',
value: 'option-1'
},
{
label: 'Option 2',
value: 'option-2'
}
],
value: 'option-3'
}
),
new DynamicTextAreaModel({id: 'testTextArea'}),
new DynamicCheckboxModel({id: 'testCheckbox'}),
new DynamicFormArrayModel(
{
id: 'testFormArray',
initialCount: 5,
groupFactory: () => {
return [
new DynamicInputModel({id: 'testFormArrayGroupInput'}),
new DynamicFormArrayModel({
id: 'testNestedFormArray', groupFactory: () => [
new DynamicInputModel({id: 'testNestedFormArrayGroupInput'})
]
})
];
}
}
),
new DynamicFormGroupModel(
{
id: 'testFormGroup',
group: [
new DynamicInputModel({id: 'nestedTestInput'}),
new DynamicTextAreaModel({id: 'nestedTestTextArea'})
]
}
),
new DynamicSliderModel({id: 'testSlider'}),
new DynamicSwitchModel({id: 'testSwitch'}),
new DynamicDatePickerModel({id: 'testDatepicker', value: new Date()}),
new DynamicFileUploadModel({id: 'testFileUpload'}),
new DynamicEditorModel({id: 'testEditor'}),
new DynamicTimePickerModel({id: 'testTimePicker'}),
new DynamicRatingModel({id: 'testRating'}),
new DynamicColorPickerModel({id: 'testColorPicker'}),
new DynamicTypeaheadModel({id: 'testTypeahead'}),
new DynamicScrollableDropdownModel({id: 'testScrollableDropdown', authorityOptions: authorityOptions}),
new DynamicTagModel({id: 'testTag'}),
new DynamicListCheckboxGroupModel({id: 'testCheckboxList', authorityOptions: authorityOptions, repeatable: true}),
new DynamicListRadioGroupModel({id: 'testRadioList', authorityOptions: authorityOptions, repeatable: false}),
new DynamicGroupModel({
id: 'testRelationGroup',
formConfiguration: [{
fields: [{
hints: 'Enter the name of the author.',
input: {type: 'onebox'},
label: 'Authors',
languageCodes: [],
mandatory: 'true',
mandatoryMessage: 'Required field!',
repeatable: false,
selectableMetadata: [{
authority: 'RPAuthority',
closed: false,
metadata: 'dc.contributor.author'
}],
} as FormFieldModel]
} as FormRowModel, {
fields: [{
hints: 'Enter the affiliation of the author.',
input: {type: 'onebox'},
label: 'Affiliation',
languageCodes: [],
mandatory: 'false',
repeatable: false,
selectableMetadata: [{
authority: 'OUAuthority',
closed: false,
metadata: 'local.contributor.affiliation'
}]
} as FormFieldModel]
} as FormRowModel],
mandatoryField: '',
name: 'testRelationGroup',
relationFields: [],
scopeUUID: '',
submissionScope: ''
}),
new DynamicDsDatePickerModel({id: 'testDate'}),
new DynamicLookupModel({id: 'testLookup'}),
new DynamicLookupNameModel({id: 'testLookupName'}),
new DynamicQualdropModel({id: 'testCombobox', readOnly: false}),
new DynamicRowArrayModel(
{
id: 'testFormRowArray',
initialCount: 5,
notRepeteable: false,
groupFactory: () => {
return [
new DynamicInputModel({id: 'testFormRowArrayGroupInput'})
];
},
}
),
];
testFormConfiguration = {
name: 'testFormConfiguration',
rows: [
{
fields: [
{
input: {type: 'lookup'},
label: 'Journal',
mandatory: 'false',
repeatable: false,
hints: 'Enter the name of the journal where the item has been\n\t\t\t\t\tpublished, if any.',
selectableMetadata: [
{
metadata: 'journal',
authority: 'JOURNALAuthority',
closed: false
}
],
languageCodes: []
} as FormFieldModel,
{
input: {type: 'onebox'},
label: 'Issue',
mandatory: 'false',
repeatable: false,
hints: ' Enter issue number.',
selectableMetadata: [
{
metadata: 'issue'
}
],
languageCodes: []
} as FormFieldModel,
{
input: {type: 'name'},
label: 'Name',
mandatory: 'false',
repeatable: false,
hints: 'Enter full name.',
selectableMetadata: [
{
metadata: 'name'
}
],
languageCodes: []
} as FormFieldModel
]
} as FormRowModel,
{
fields: [
{
hints: 'If the item has any identification numbers or codes associated with↵ it, please enter the types and the actual numbers or codes.',
input: {type: 'onebox'},
label: 'Identifiers',
languageCodes: [],
mandatory: 'false',
repeatable: false,
selectableMetadata: [
{metadata: 'dc.identifier.issn', label: 'ISSN'},
{metadata: 'dc.identifier.other', label: 'Other'},
{metadata: 'dc.identifier.ismn', label: 'ISMN'},
{metadata: 'dc.identifier.govdoc', label: 'Gov\'t Doc #'},
{metadata: 'dc.identifier.uri', label: 'URI'},
{metadata: 'dc.identifier.isbn', label: 'ISBN'},
{metadata: 'dc.identifier.doi', label: 'DOI'},
{metadata: 'dc.identifier.pmid', label: 'PubMed ID'},
{metadata: 'dc.identifier.arxiv', label: 'arXiv'}
]
}, {
input: {type: 'onebox'},
label: 'Publisher',
mandatory: 'false',
repeatable: false,
hints: 'Enter the name of the publisher of the previously issued instance of this item.',
selectableMetadata: [
{
metadata: 'publisher'
}
],
languageCodes: []
}
]
} as FormRowModel,
{
fields: [
{
input: {type: 'onebox'},
label: 'Conference',
mandatory: 'false',
repeatable: false,
hints: 'Enter the name of the events, if any.',
selectableMetadata: [
{
metadata: 'conference',
authority: 'EVENTAuthority',
closed: false
}
],
languageCodes: []
}
]
} as FormRowModel
],
self: 'testFormConfiguration.url',
type: 'submissionform',
_links: {
self: 'testFormConfiguration.url'
}
}
});
beforeEach(inject([FormBuilderService], (formService: FormBuilderService) => service = formService));
it('should find a dynamic form control model by id', () => {
expect(service.findById('testCheckbox', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testCheckboxGroup', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testDatepicker', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testFormArray', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testInput', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testRadioGroup', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testSelect', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testSlider', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testSwitch', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testTextArea', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testFileUpload', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testEditor', testModel) instanceof DynamicEditorModel).toBe(true);
expect(service.findById('testTimePicker', testModel) instanceof DynamicTimePickerModel).toBe(true);
expect(service.findById('testRating', testModel) instanceof DynamicRatingModel).toBe(true);
expect(service.findById('testColorPicker', testModel) instanceof DynamicColorPickerModel).toBe(true);
});
it('should find a nested dynamic form control model by id', () => {
expect(service.findById('testCheckboxGroup1', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testCheckboxGroup2', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('nestedTestInput', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testFormRowArrayGroupInput', testModel) instanceof DynamicFormControlModel).toBe(true);
expect(service.findById('testFormRowArrayGroupInput', testModel, 2) instanceof DynamicFormControlModel).toBe(true);
});
it('should create an array of form models', () => {
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
expect(formModel[0] instanceof DynamicRowGroupModel).toBe(true);
expect((formModel[0] as DynamicRowGroupModel).group.length).toBe(3);
expect((formModel[0] as DynamicRowGroupModel).get(0) instanceof DynamicLookupModel).toBe(true);
expect((formModel[0] as DynamicRowGroupModel).get(1) instanceof DsDynamicInputModel).toBe(true);
expect((formModel[0] as DynamicRowGroupModel).get(2) instanceof DynamicConcatModel).toBe(true);
expect(formModel[1] instanceof DynamicRowGroupModel).toBe(true);
expect((formModel[1] as DynamicRowGroupModel).group.length).toBe(2);
expect((formModel[1] as DynamicRowGroupModel).get(0) instanceof DynamicQualdropModel).toBe(true);
expect(((formModel[1] as DynamicRowGroupModel).get(0) as DynamicQualdropModel).get(0) instanceof DynamicSelectModel).toBe(true);
expect(((formModel[1] as DynamicRowGroupModel).get(0) as DynamicQualdropModel).get(1) instanceof DsDynamicInputModel).toBe(true);
expect((formModel[1] as DynamicRowGroupModel).get(1) instanceof DsDynamicInputModel).toBe(true);
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);
});
it('should return form\'s fields value from form model', () => {
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
let value = {} as any;
expect(service.getValueFromModel(formModel)).toEqual(value);
((formModel[0] as DynamicRowGroupModel).get(1) as DsDynamicInputModel).valueUpdates.next('test');
value = {
issue: [new FormFieldMetadataValueObject('test')]
};
expect(service.getValueFromModel(formModel)).toEqual(value);
((formModel[2] as DynamicRowGroupModel).get(0) as DynamicTypeaheadModel).valueUpdates.next('test one');
value = {
issue: [new FormFieldMetadataValueObject('test')],
conference: [new FormFieldMetadataValueObject('test one')]
};
expect(service.getValueFromModel(formModel)).toEqual(value);
});
it('should clear all form\'s fields value', () => {
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
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');
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)
});
it('should return true when model has a custom group model as parent', () => {
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
let model = service.findById('dc_identifier_QUALDROP_VALUE', formModel);
let modelParent = service.findById('dc_identifier_QUALDROP_GROUP', formModel);
model.parent = modelParent;
expect(service.isModelInCustomGroup(model)).toBe(true);
model = service.findById('name_CONCAT_FIRST_INPUT', formModel);
modelParent = service.findById('name_CONCAT_GROUP', formModel);
model.parent = modelParent;
expect(service.isModelInCustomGroup(model)).toBe(true);
});
it('should return true when model value is an array', () => {
let model = service.findById('testCheckboxList', testModel) as DynamicFormArrayModel;
expect(service.hasArrayGroupValue(model)).toBe(true);
model = service.findById('testRadioList', testModel) as DynamicFormArrayModel;
expect(service.hasArrayGroupValue(model)).toBe(true);
model = service.findById('testTag', testModel) as DynamicFormArrayModel;
expect(service.hasArrayGroupValue(model)).toBe(true);
});
it('should return true when model value is a map', () => {
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
const model = service.findById('dc_identifier_QUALDROP_VALUE', formModel);
const modelParent = service.findById('dc_identifier_QUALDROP_GROUP', formModel);
model.parent = modelParent;
expect(service.hasMappedGroupValue(model)).toBe(true);
});
it('should return true when model is a Qualdrop Group', () => {
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
let model = service.findById('dc_identifier_QUALDROP_GROUP', formModel);
expect(service.isQualdropGroup(model)).toBe(true);
model = service.findById('name_CONCAT_GROUP', formModel);
expect(service.isQualdropGroup(model)).toBe(false);
});
it('should return true when model is a Custom or List Group', () => {
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
let model = service.findById('dc_identifier_QUALDROP_GROUP', formModel);
expect(service.isCustomOrListGroup(model)).toBe(true);
model = service.findById('name_CONCAT_GROUP', formModel);
expect(service.isCustomOrListGroup(model)).toBe(true);
model = service.findById('testCheckboxList', testModel);
expect(service.isCustomOrListGroup(model)).toBe(true);
model = service.findById('testRadioList', testModel);
expect(service.isCustomOrListGroup(model)).toBe(true);
});
it('should return true when model is a Custom Group', () => {
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
let model = service.findById('dc_identifier_QUALDROP_GROUP', formModel);
expect(service.isCustomGroup(model)).toBe(true);
model = service.findById('name_CONCAT_GROUP', formModel);
expect(service.isCustomGroup(model)).toBe(true);
model = service.findById('testCheckboxList', testModel);
expect(service.isCustomGroup(model)).toBe(false);
model = service.findById('testRadioList', testModel);
expect(service.isCustomGroup(model)).toBe(false);
});
it('should return true when model is a List Group', () => {
let model = service.findById('testCheckboxList', testModel);
expect(service.isListGroup(model)).toBe(true);
model = service.findById('testRadioList', testModel);
expect(service.isListGroup(model)).toBe(true);
});
it('should return true when model is a Relation Group', () => {
const model = service.findById('testRelationGroup', testModel);
expect(service.isRelationGroup(model)).toBe(true);
});
it('should return true when model is a Array Row Group', () => {
let model = service.findById('testFormRowArray', testModel, null);
expect(service.isRowArrayGroup(model)).toBe(true);
model = service.findById('testFormArray', testModel);
expect(service.isRowArrayGroup(model)).toBe(false);
});
it('should return true when model is a Array Group', () => {
let model = service.findById('testFormRowArray', testModel) as DynamicFormArrayModel;
expect(service.isArrayGroup(model)).toBe(true);
model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
expect(service.isArrayGroup(model)).toBe(true);
});
it('should return properly form control by field id', () => {
const group = service.createFormGroup(testModel);
const control = group.controls.testLookup;
expect(service.getFormControlById('testLookup', group, testModel)).toEqual(control);
});
it('should return field id from model', () => {
const model = service.findById('testRadioList', testModel);
expect(service.getId(model)).toEqual('testRadioList');
});
it('should create a form group', () => {
const formGroup = service.createFormGroup(testModel);
expect(formGroup instanceof FormGroup).toBe(true);
expect(formGroup.get('testCheckbox') instanceof FormControl).toBe(true);
expect(formGroup.get('testCheckboxGroup') instanceof FormGroup).toBe(true);
expect(formGroup.get('testDatepicker') instanceof FormControl).toBe(true);
expect(formGroup.get('testFormArray') instanceof FormArray).toBe(true);
expect(formGroup.get('testInput') instanceof FormControl).toBe(true);
expect(formGroup.get('testRadioGroup') instanceof FormControl).toBe(true);
expect(formGroup.get('testSelect') instanceof FormControl).toBe(true);
expect(formGroup.get('testTextArea') instanceof FormControl).toBe(true);
expect(formGroup.get('testFileUpload') instanceof FormControl).toBe(true);
expect(formGroup.get('testEditor') instanceof FormControl).toBe(true);
expect(formGroup.get('testTimePicker') instanceof FormControl).toBe(true);
expect(formGroup.get('testRating') instanceof FormControl).toBe(true);
expect(formGroup.get('testColorPicker') instanceof FormControl).toBe(true);
});
it('should throw when unknown DynamicFormControlModel id is specified in JSON', () => {
expect(() => service.fromJSON([{id: 'test'}]))
.toThrow(new Error(`unknown form control model type defined on JSON object with id "test"`));
});
it('should resolve array group path', () => {
service.createFormGroup(testModel);
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
const nestedModel = (model.get(0).get(1) as DynamicFormArrayModel).get(0);
expect(service.getPath(model)).toEqual(['testFormArray']);
expect(service.getPath(nestedModel)).toEqual(['testFormArray', '0', 'testNestedFormArray', '0']);
});
it('should add a form control to an existing form group', () => {
const formGroup = service.createFormGroup(testModel);
const nestedFormGroup = formGroup.controls.testFormGroup as FormGroup;
const nestedFormGroupModel = testModel[7] as DynamicFormGroupModel;
const newModel1 = new DynamicInputModel({id: 'newInput1'});
const newModel2 = new DynamicInputModel({id: 'newInput2'});
service.addFormGroupControl(formGroup, testModel, newModel1);
service.addFormGroupControl(nestedFormGroup, nestedFormGroupModel, newModel2);
expect(formGroup.controls[newModel1.id]).toBeTruthy();
expect(testModel[testModel.length - 1] === newModel1).toBe(true);
expect((formGroup.controls.testFormGroup as FormGroup).controls[newModel2.id]).toBeTruthy();
expect(nestedFormGroupModel.get(nestedFormGroupModel.group.length - 1) === newModel2).toBe(true);
});
it('should insert a form control to an existing form group', () => {
const formGroup = service.createFormGroup(testModel);
const nestedFormGroup = formGroup.controls.testFormGroup as FormGroup;
const nestedFormGroupModel = testModel[7] as DynamicFormGroupModel;
const newModel1 = new DynamicInputModel({id: 'newInput1'});
const newModel2 = new DynamicInputModel({id: 'newInput2'});
service.insertFormGroupControl(4, formGroup, testModel, newModel1);
service.insertFormGroupControl(0, nestedFormGroup, nestedFormGroupModel, newModel2);
expect(formGroup.controls[newModel1.id]).toBeTruthy();
expect(testModel[4] === newModel1).toBe(true);
expect(service.getPath(testModel[4])).toEqual(['newInput1']);
expect((formGroup.controls.testFormGroup as FormGroup).controls[newModel2.id]).toBeTruthy();
expect(nestedFormGroupModel.get(0) === newModel2).toBe(true);
expect(service.getPath(nestedFormGroupModel.get(0))).toEqual(['testFormGroup', 'newInput2']);
});
it('should move an existing form control to a different group position', () => {
const formGroup = service.createFormGroup(testModel);
const nestedFormGroupModel = testModel[7] as DynamicFormGroupModel;
const model1 = testModel[0];
const model2 = nestedFormGroupModel.get(0);
service.moveFormGroupControl(0, 2, testModel);
expect(formGroup.controls[model1.id]).toBeTruthy();
expect(testModel[2] === model1).toBe(true);
service.moveFormGroupControl(0, 1, nestedFormGroupModel);
expect((formGroup.controls.testFormGroup as FormGroup).controls[model2.id]).toBeTruthy();
expect(nestedFormGroupModel.get(1) === model2).toBe(true);
});
it('should remove a form control from an existing form group', () => {
const formGroup = service.createFormGroup(testModel);
const nestedFormGroup = formGroup.controls.testFormGroup as FormGroup;
const nestedFormGroupModel = testModel[7] as DynamicFormGroupModel;
const length = testModel.length;
const size = nestedFormGroupModel.size();
const index = 1;
const id1 = testModel[index].id;
const id2 = nestedFormGroupModel.get(index).id;
service.removeFormGroupControl(index, formGroup, testModel);
expect(Object.keys(formGroup.controls).length).toBe(length - 1);
expect(formGroup.controls[id1]).toBeUndefined();
expect(testModel.length).toBe(length - 1);
expect(service.findById(id1, testModel)).toBeNull();
service.removeFormGroupControl(index, nestedFormGroup, nestedFormGroupModel);
expect(Object.keys(nestedFormGroup.controls).length).toBe(size - 1);
expect(nestedFormGroup.controls[id2]).toBeUndefined();
expect(nestedFormGroupModel.size()).toBe(size - 1);
expect(service.findById(id2, testModel)).toBeNull();
});
it('should create a form array', () => {
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
let formArray;
expect(service.createFormArray).toBeTruthy();
formArray = service.createFormArray(model);
expect(formArray instanceof FormArray).toBe(true);
expect(formArray.length).toBe(model.initialCount);
});
it('should add a form array group', () => {
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
const formArray = service.createFormArray(model);
service.addFormArrayGroup(formArray, model);
expect(formArray.length).toBe(model.initialCount + 1);
});
it('should insert a form array group', () => {
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
const formArray = service.createFormArray(model);
service.insertFormArrayGroup(0, formArray, model);
expect(formArray.length).toBe(model.initialCount + 1);
});
it('should move up a form array group', () => {
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
const formArray = service.createFormArray(model);
const index = 3;
const step = 1;
(formArray.at(index) as FormGroup).controls.testFormArrayGroupInput.setValue('next test value 1');
(formArray.at(index + step) as FormGroup).controls.testFormArrayGroupInput.setValue('next test value 2');
(model.get(index).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).valueUpdates.next('next test value 1');
(model.get(index + step).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).valueUpdates.next('next test value 2');
service.moveFormArrayGroup(index, step, formArray, model);
expect(formArray.length).toBe(model.initialCount);
expect((formArray.at(index) as FormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 2');
expect((formArray.at(index + step) as FormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 1');
expect((model.get(index).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).value).toEqual('next test value 2');
expect((model.get(index + step).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).value).toEqual('next test value 1');
});
it('should move down a form array group', () => {
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
const formArray = service.createFormArray(model);
const index = 3;
const step = -1;
(formArray.at(index) as FormGroup).controls.testFormArrayGroupInput.setValue('next test value 1');
(formArray.at(index + step) as FormGroup).controls.testFormArrayGroupInput.setValue('next test value 2');
(model.get(index).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).valueUpdates.next('next test value 1');
(model.get(index + step).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).valueUpdates.next('next test value 2');
service.moveFormArrayGroup(index, step, formArray, model);
expect(formArray.length).toBe(model.initialCount);
expect((formArray.at(index) as FormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 2');
expect((formArray.at(index + step) as FormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 1');
expect((model.get(index).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).value).toEqual('next test value 2');
expect((model.get(index + step).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).value).toEqual('next test value 1');
});
it('should throw when form array group is to be moved out of bounds', () => {
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
const formArray = service.createFormArray(model);
expect(() => service.moveFormArrayGroup(2, -5, formArray, model))
.toThrow(new Error(`form array group cannot be moved due to index or new index being out of bounds`));
});
it('should remove a form array group', () => {
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
const formArray = service.createFormArray(model);
service.removeFormArrayGroup(0, formArray, model);
expect(formArray.length).toBe(model.initialCount - 1);
});
it('should clear a form array', () => {
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
const formArray = service.createFormArray(model);
service.clearFormArray(formArray, model);
expect(formArray.length === 0).toBe(true);
});
});

View File

@@ -0,0 +1,284 @@
import { Injectable } from '@angular/core';
import { AbstractControl, FormGroup } from '@angular/forms';
import {
DYNAMIC_FORM_CONTROL_TYPE_ARRAY,
DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX_GROUP,
DYNAMIC_FORM_CONTROL_TYPE_GROUP,
DYNAMIC_FORM_CONTROL_TYPE_RADIO_GROUP,
DynamicFormArrayModel,
DynamicFormControlModel,
DynamicFormGroupModel,
DynamicFormService,
DynamicPathable,
JSONUtils,
} from '@ng-dynamic-forms/core';
import { isObject, isString, mergeWith } from 'lodash';
import { hasValue, isEmpty, isNotEmpty, isNotNull, isNotUndefined, isNull } from '../../empty.util';
import { DynamicQualdropModel } from './ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model';
import { SubmissionFormsModel } from '../../../core/shared/config/config-submission-forms.model';
import {
DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP,
DynamicGroupModel
} from './ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model';
import { DYNAMIC_FORM_CONTROL_TYPE_TAG } from './ds-dynamic-form-ui/models/tag/dynamic-tag.model';
import { RowParser } from './parsers/row-parser';
import { DynamicRowArrayModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-array-model';
import { DsDynamicInputModel } from './ds-dynamic-form-ui/models/ds-dynamic-input.model';
import { FormFieldMetadataValueObject } from './models/form-field-metadata-value.model';
@Injectable()
export class FormBuilderService extends DynamicFormService {
findById(id: string, groupModel: DynamicFormControlModel[], arrayIndex = null): DynamicFormControlModel | null {
let result = null;
const findByIdFn = (findId: string, findGroupModel: DynamicFormControlModel[], findArrayIndex): void => {
for (const controlModel of findGroupModel) {
if (controlModel.id === findId) {
if (this.isArrayGroup(controlModel) && isNotNull(findArrayIndex)) {
result = (controlModel as DynamicFormArrayModel).get(findArrayIndex);
} else {
result = controlModel;
}
break;
}
if (this.isGroup(controlModel)) {
findByIdFn(findId, (controlModel as DynamicFormGroupModel).group, findArrayIndex);
}
if (this.isArrayGroup(controlModel)
&& (isNull(findArrayIndex) || (controlModel as DynamicFormArrayModel).size > (findArrayIndex))) {
const index = (isNull(findArrayIndex)) ? 0 : findArrayIndex;
findByIdFn(findId, (controlModel as DynamicFormArrayModel).get(index).group, index);
}
}
};
findByIdFn(id, groupModel, arrayIndex);
return result;
}
clearAllModelsValue(groupModel: DynamicFormControlModel[]): void {
const iterateControlModels = (findGroupModel: DynamicFormControlModel[]): void => {
for (const controlModel of findGroupModel) {
if (this.isGroup(controlModel)) {
iterateControlModels((controlModel as DynamicFormGroupModel).group);
continue;
}
if (this.isArrayGroup(controlModel)) {
iterateControlModels((controlModel as DynamicFormArrayModel).groupFactory());
continue;
}
if (controlModel.hasOwnProperty('valueUpdates')) {
(controlModel as any).valueUpdates.next(undefined);
}
}
};
iterateControlModels(groupModel);
}
getValueFromModel(groupModel: DynamicFormControlModel[]): void {
let result = Object.create({});
const customizer = (objValue, srcValue) => {
if (Array.isArray(objValue)) {
return objValue.concat(srcValue);
}
};
const normalizeValue = (controlModel, controlValue, controlModelIndex) => {
const controlLanguage = (controlModel as DsDynamicInputModel).hasLanguages ? (controlModel as DsDynamicInputModel).language : null;
if (isString(controlValue)) {
return new FormFieldMetadataValueObject(controlValue, controlLanguage, null, null, controlModelIndex);
} else if (isObject(controlValue)) {
const authority = controlValue.authority || controlValue.id || null;
const place = controlModelIndex || controlValue.place;
return new FormFieldMetadataValueObject(controlValue.value, controlLanguage, authority, controlValue.display, place);
}
};
const iterateControlModels = (findGroupModel: DynamicFormControlModel[], controlModelIndex: number = 0): void => {
let iterateResult = Object.create({});
// Iterate over all group's controls
for (const controlModel of findGroupModel) {
if (this.isRowGroup(controlModel) && !this.isCustomOrListGroup(controlModel)) {
iterateResult = mergeWith(iterateResult, iterateControlModels((controlModel as DynamicFormGroupModel).group), customizer);
continue;
}
if (this.isGroup(controlModel) && !this.isCustomOrListGroup(controlModel)) {
iterateResult[controlModel.name] = iterateControlModels((controlModel as DynamicFormGroupModel).group);
continue;
}
if (this.isRowArrayGroup(controlModel)) {
for (const arrayItemModel of (controlModel as DynamicRowArrayModel).groups) {
iterateResult = mergeWith(iterateResult, iterateControlModels(arrayItemModel.group, arrayItemModel.index), customizer);
}
continue;
}
if (this.isArrayGroup(controlModel)) {
iterateResult[controlModel.name] = [];
for (const arrayItemModel of (controlModel as DynamicFormArrayModel).groups) {
iterateResult[controlModel.name].push(iterateControlModels(arrayItemModel.group, arrayItemModel.index));
}
continue;
}
let controlId;
// Get the field's name
if (this.isQualdropGroup(controlModel)) {
// If is instance of DynamicQualdropModel take the qualdrop id as field's name
controlId = (controlModel as DynamicQualdropModel).qualdropId;
} else {
controlId = controlModel.name;
}
if (this.isRelationGroup(controlModel)) {
const values = (controlModel as DynamicGroupModel).getGroupValue();
values.forEach((groupValue, groupIndex) => {
const newGroupValue = Object.create({});
Object.keys(groupValue)
.forEach((key) => {
const normValue = normalizeValue(controlModel, groupValue[key], groupIndex);
if (isNotEmpty(normValue) && normValue.hasValue()) {
if (iterateResult.hasOwnProperty(key)) {
iterateResult[key].push(normValue);
} else {
iterateResult[key] = [normValue];
}
}
});
})
} else if (isNotUndefined((controlModel as any).value) && isNotEmpty((controlModel as any).value)) {
const controlArrayValue = [];
// Normalize control value as an array of FormFieldMetadataValueObject
const values = Array.isArray((controlModel as any).value) ? (controlModel as any).value : [(controlModel as any).value];
values.forEach((controlValue) => {
controlArrayValue.push(normalizeValue(controlModel, controlValue, controlModelIndex))
});
if (controlId && iterateResult.hasOwnProperty(controlId) && isNotNull(iterateResult[controlId])) {
iterateResult[controlId] = iterateResult[controlId].concat(controlArrayValue);
} else {
iterateResult[controlId] = isNotEmpty(controlArrayValue) ? controlArrayValue : null;
}
}
}
return iterateResult;
};
result = iterateControlModels(groupModel);
return result;
}
modelFromConfiguration(json: string | SubmissionFormsModel, scopeUUID: string, initFormValues: any = {}, submissionScope?: string, readOnly = false): DynamicFormControlModel[] | never {
let rows: DynamicFormControlModel[] = [];
const rawData = typeof json === 'string' ? JSON.parse(json, JSONUtils.parseReviver) : json;
if (rawData.rows && !isEmpty(rawData.rows)) {
rawData.rows.forEach((currentRow) => {
const rowParsed = new RowParser(currentRow, scopeUUID, initFormValues, submissionScope, readOnly).parse();
if (isNotNull(rowParsed)) {
if (Array.isArray(rowParsed)) {
rows = rows.concat(rowParsed);
} else {
rows.push(rowParsed);
}
}
});
}
return rows;
}
isModelInCustomGroup(model: DynamicFormControlModel): boolean {
return this.isCustomGroup((model as any).parent);
}
hasArrayGroupValue(model: DynamicFormControlModel): boolean {
return model && (this.isListGroup(model) || model.type === DYNAMIC_FORM_CONTROL_TYPE_TAG);
}
hasMappedGroupValue(model: DynamicFormControlModel): boolean {
return (this.isQualdropGroup((model as any).parent)
|| this.isRelationGroup((model as any).parent));
}
isGroup(model: DynamicFormControlModel): boolean {
return model && (model.type === DYNAMIC_FORM_CONTROL_TYPE_GROUP || model.type === DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX_GROUP);
}
isQualdropGroup(model: DynamicFormControlModel): boolean {
return (model && model.type === DYNAMIC_FORM_CONTROL_TYPE_GROUP && hasValue((model as any).qualdropId));
}
isCustomGroup(model: DynamicFormControlModel): boolean {
return model && ((model as any).type === DYNAMIC_FORM_CONTROL_TYPE_GROUP && (model as any).isCustomGroup === true);
}
isRowGroup(model: DynamicFormControlModel): boolean {
return model && ((model as any).type === DYNAMIC_FORM_CONTROL_TYPE_GROUP && (model as any).isRowGroup === true);
}
isCustomOrListGroup(model: DynamicFormControlModel): boolean {
return model &&
(this.isCustomGroup(model)
|| this.isListGroup(model));
}
isListGroup(model: DynamicFormControlModel): boolean {
return model &&
((model.type === DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX_GROUP && (model as any).isListGroup === true)
|| (model.type === DYNAMIC_FORM_CONTROL_TYPE_RADIO_GROUP && (model as any).isListGroup === true));
}
isRelationGroup(model: DynamicFormControlModel): boolean {
return model && model.type === DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP;
}
isRowArrayGroup(model: DynamicFormControlModel): boolean {
return model.type === DYNAMIC_FORM_CONTROL_TYPE_ARRAY && (model as any).isRowArray === true;
}
isArrayGroup(model: DynamicFormControlModel): boolean {
return model.type === DYNAMIC_FORM_CONTROL_TYPE_ARRAY;
}
getFormControlById(id: string, formGroup: FormGroup, groupModel: DynamicFormControlModel[], index = 0): AbstractControl {
const fieldModel = this.findById(id, groupModel, index);
return isNotEmpty(fieldModel) ? formGroup.get(this.getPath(fieldModel)) : null;
}
getId(model: DynamicPathable): string {
if (this.isArrayGroup(model as DynamicFormControlModel)) {
return model.index.toString();
} else {
return ((model as DynamicFormControlModel).id !== (model as DynamicFormControlModel).name) ?
(model as DynamicFormControlModel).name :
(model as DynamicFormControlModel).id;
}
}
}

View File

@@ -0,0 +1,14 @@
export class FormFieldLanguageValueObject {
value: string;
language: string;
constructor(value: string, language: string) {
this.value = value;
this.language = language;
}
}
export interface LanguageCode {
display: string;
code: string;
}

View File

@@ -0,0 +1,46 @@
import { isNotEmpty, isNotNull } from '../../../empty.util';
export class FormFieldMetadataValueObject {
metadata?: string;
value: any;
display: string;
language: any;
authority: string;
confidence: number;
place: number;
closed: boolean;
label: string;
constructor(value: any = null,
language: any = null,
authority: string = null,
display: string = null,
place: number = 0,
confidence: number = -1,
metadata: string = null) {
this.value = isNotNull(value) ? ((typeof value === 'string') ? value.trim() : value) : null;
this.language = language;
this.authority = authority;
this.display = display || value;
this.confidence = confidence;
if (authority != null) {
this.confidence = 600;
} else if (isNotEmpty(confidence)) {
this.confidence = confidence;
}
this.place = place;
if (isNotEmpty(metadata)) {
this.metadata = metadata;
}
}
hasAuthority(): boolean {
return isNotEmpty(this.authority);
}
hasValue(): boolean {
return isNotEmpty(this.value);
}
}

View File

@@ -0,0 +1,37 @@
import { isEqual } from 'lodash';
export class FormFieldPreviousValueObject {
private _path;
private _value;
constructor(path: any[] = null, value: any = null) {
this._path = path;
this._value = value;
}
get path() {
return this._path;
}
set path(path: any[]) {
this._path = path;
}
get value() {
return this._value;
}
set value(value: any) {
this._value = value;
}
public delete() {
this._value = null;
this._path = null;
}
public isPathEqual(path) {
return this._path && isEqual(this._path, path);
}
}

View File

@@ -0,0 +1,3 @@
export interface FormFieldChangedObject {
string: any
}

View File

@@ -0,0 +1,43 @@
import { autoserialize } from 'cerialize';
import { FormRowModel } from '../../../../core/shared/config/config-submission-forms.model';
import { LanguageCode } from './form-field-language-value.model';
import { FormFieldMetadataValueObject } from './form-field-metadata-value.model';
export class FormFieldModel {
@autoserialize
hints: string;
@autoserialize
label: string;
@autoserialize
languageCodes: LanguageCode[];
@autoserialize
mandatoryMessage: string;
@autoserialize
mandatory: string;
@autoserialize
repeatable: boolean;
@autoserialize
input: {
type: string;
regex?: string;
};
@autoserialize
selectableMetadata: FormFieldMetadataValueObject[];
@autoserialize
rows: FormRowModel[];
@autoserialize
scope: string;
@autoserialize
value: any;
}

View File

@@ -0,0 +1,105 @@
import { FieldParser } from './field-parser';
import { FormFieldModel } from '../models/form-field.model';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
import { DynamicFormControlLayout, DynamicInputModel, DynamicInputModelConfig } from '@ng-dynamic-forms/core';
import {
CONCAT_FIRST_INPUT_SUFFIX,
CONCAT_GROUP_SUFFIX,
CONCAT_SECOND_INPUT_SUFFIX,
DynamicConcatModel,
DynamicConcatModelConfig
} from '../ds-dynamic-form-ui/models/ds-dynamic-concat.model';
import { isNotEmpty } from '../../../empty.util';
import { ParserOptions } from './parser-options';
export class ConcatFieldParser extends FieldParser {
constructor(protected configData: FormFieldModel,
protected initFormValues,
protected parserOptions: ParserOptions,
protected separator: string,
protected firstPlaceholder: string = null,
protected secondPlaceholder: string = null) {
super(configData, initFormValues, parserOptions);
this.separator = separator;
this.firstPlaceholder = firstPlaceholder;
this.secondPlaceholder = secondPlaceholder;
}
public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any {
let clsGroup: DynamicFormControlLayout;
let clsInput: DynamicFormControlLayout;
let newId: string;
if (this.configData.selectableMetadata[0].metadata.includes('.')) {
newId = this.configData.selectableMetadata[0].metadata
.split('.')
.slice(0, this.configData.selectableMetadata[0].metadata.split('.').length - 1)
.join('.');
} else {
newId = this.configData.selectableMetadata[0].metadata
}
clsInput = {
grid: {
host: 'col-sm-6'
}
};
const groupId = newId.replace(/\./g, '_') + CONCAT_GROUP_SUFFIX;
const concatGroup: DynamicConcatModelConfig = this.initModel(groupId, false, false);
concatGroup.group = [];
concatGroup.separator = this.separator;
const input1ModelConfig: DynamicInputModelConfig = this.initModel(newId + CONCAT_FIRST_INPUT_SUFFIX, label, false, false);
const input2ModelConfig: DynamicInputModelConfig = this.initModel(newId + CONCAT_SECOND_INPUT_SUFFIX, label, true, false);
if (this.configData.mandatory) {
input1ModelConfig.required = true;
}
if (isNotEmpty(this.firstPlaceholder)) {
input1ModelConfig.placeholder = this.firstPlaceholder;
}
if (isNotEmpty(this.secondPlaceholder)) {
input2ModelConfig.placeholder = this.secondPlaceholder;
}
// Init values
if (isNotEmpty(fieldValue)) {
const values = fieldValue.value.split(this.separator);
if (values.length > 1) {
input1ModelConfig.value = values[0].trim();
input2ModelConfig.value = values[1].trim();
}
}
// Split placeholder if is like 'placeholder1/placeholder2'
const placeholder = this.configData.label.split('/');
if (placeholder.length === 2) {
input1ModelConfig.placeholder = placeholder[0];
input2ModelConfig.placeholder = placeholder[1];
}
const model1 = new DynamicInputModel(input1ModelConfig, clsInput);
const model2 = new DynamicInputModel(input2ModelConfig, clsInput);
concatGroup.group.push(model1);
concatGroup.group.push(model2);
clsGroup = {
element: {
control: 'form-row',
}
};
const concatModel = new DynamicConcatModel(concatGroup, clsGroup);
concatModel.name = this.getFieldId();
return concatModel;
}
}

View File

@@ -0,0 +1,65 @@
import { FormFieldModel } from '../models/form-field.model';
import { DynamicConcatModel } from '../ds-dynamic-form-ui/models/ds-dynamic-concat.model';
import { SeriesFieldParser } from './series-field-parser';
import { DateFieldParser } from './date-field-parser';
import { DynamicDsDatePickerModel } from '../ds-dynamic-form-ui/models/date-picker/date-picker.model';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
import { ParserOptions } from './parser-options';
describe('DateFieldParser test suite', () => {
let field: FormFieldModel;
let initFormValues: any = {};
const parserOptions: ParserOptions = {
readOnly: false,
submissionScope: null,
authorityUuid: null
};
beforeEach(() => {
field = {
input: {
type: 'date'
},
label: 'Date of Issue.',
mandatory: 'true',
repeatable: false,
hints: 'Please give the date of previous publication or public distribution. You can leave out the day and/or month if they aren\'t applicable.',
mandatoryMessage: 'You must enter at least the year.',
selectableMetadata: [
{
metadata: 'date',
}
],
languageCodes: []
} as FormFieldModel;
});
it('should init parser properly', () => {
const parser = new DateFieldParser(field, initFormValues, parserOptions);
expect(parser instanceof DateFieldParser).toBe(true);
});
it('should return a DynamicDsDatePickerModel object when repeatable option is false', () => {
const parser = new DateFieldParser(field, initFormValues, parserOptions);
const fieldModel = parser.parse();
expect(fieldModel instanceof DynamicDsDatePickerModel).toBe(true);
});
it('should set init value properly', () => {
initFormValues = {
date: [new FormFieldMetadataValueObject('1983-11-18')],
};
const expectedValue = '1983-11-18';
const parser = new DateFieldParser(field, initFormValues, parserOptions);
const fieldModel = parser.parse();
expect(fieldModel.value).toEqual(expectedValue);
});
});

View File

@@ -0,0 +1,36 @@
import { FieldParser } from './field-parser';
import { DynamicDatePickerModelConfig } from '@ng-dynamic-forms/core';
import { DynamicDsDatePickerModel } from '../ds-dynamic-form-ui/models/date-picker/date-picker.model';
import { isNotEmpty } from '../../../empty.util';
import { DS_DATE_PICKER_SEPARATOR } from '../ds-dynamic-form-ui/models/date-picker/date-picker.component';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
export class DateFieldParser extends FieldParser {
public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any {
let malformedDate = false;
const inputDateModelConfig: DynamicDatePickerModelConfig = this.initModel(null, label);
inputDateModelConfig.toggleIcon = 'fa fa-calendar';
this.setValues(inputDateModelConfig as any, fieldValue);
// Init Data and validity check
if (isNotEmpty(inputDateModelConfig.value)) {
const value = inputDateModelConfig.value.toString();
if (value.length >= 4) {
const valuesArray = value.split(DS_DATE_PICKER_SEPARATOR);
if (valuesArray.length < 4) {
for (let i = 0; i < valuesArray.length; i++) {
const len = i === 0 ? 4 : 2;
if (valuesArray[i].length !== len) {
malformedDate = true;
}
}
}
}
}
const dateModel = new DynamicDsDatePickerModel(inputDateModelConfig);
dateModel.malformedDate = malformedDate;
return dateModel;
}
}

View File

@@ -0,0 +1,59 @@
import { FormFieldModel } from '../models/form-field.model';
import { DropdownFieldParser } from './dropdown-field-parser';
import { DynamicScrollableDropdownModel } from '../ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
import { ParserOptions } from './parser-options';
describe('DropdownFieldParser test suite', () => {
let field: FormFieldModel;
const initFormValues = {};
const parserOptions: ParserOptions = {
readOnly: false,
submissionScope: 'testScopeUUID',
authorityUuid: null
};
beforeEach(() => {
field = {
input: {
type: 'dropdown'
},
label: 'Type',
mandatory: 'false',
repeatable: false,
hints: 'Select the tyupe.',
selectableMetadata: [
{
metadata: 'type',
authority: 'common_types_dataset',
closed: false
}
],
languageCodes: []
} as FormFieldModel;
});
it('should init parser properly', () => {
const parser = new DropdownFieldParser(field, initFormValues, parserOptions);
expect(parser instanceof DropdownFieldParser).toBe(true);
});
it('should return a DynamicScrollableDropdownModel object when repeatable option is false', () => {
const parser = new DropdownFieldParser(field, initFormValues, parserOptions);
const fieldModel = parser.parse();
expect(fieldModel instanceof DynamicScrollableDropdownModel).toBe(true);
});
it('should throw when authority is not passed', () => {
field.selectableMetadata[0].authority = null;
const parser = new DropdownFieldParser(field, initFormValues, parserOptions);
expect(() => parser.parse())
.toThrow();
});
});

View File

@@ -0,0 +1,35 @@
import { FieldParser } from './field-parser';
import { DynamicFormControlLayout, } from '@ng-dynamic-forms/core';
import {
DynamicScrollableDropdownModel,
DynamicScrollableDropdownModelConfig
} from '../ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
import { isNotEmpty } from '../../../empty.util';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
export class DropdownFieldParser extends FieldParser {
public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any {
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(fieldValue)) {
dropdownModelConfig.value = fieldValue;
}
layout = {
element: {
control: 'col'
},
grid: {
host: 'col'
}
};
const dropdownModel = new DynamicScrollableDropdownModel(dropdownModelConfig, layout);
return dropdownModel;
} else {
throw Error(`Authority name is not available. Please checks form configuration file.`);
}
}
}

View File

@@ -0,0 +1,307 @@
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 { setLayout } from './parser.utils';
import { AuthorityOptions } from '../../../../core/integration/models/authority-options.model';
import { ParserOptions } from './parser-options';
export abstract class FieldParser {
protected fieldId: string;
constructor(protected configData: FormFieldModel, protected initFormValues, protected parserOptions: ParserOptions) {
}
public abstract modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any;
public parse() {
if (((this.getInitValueCount() > 1 && !this.configData.repeatable) || (this.configData.repeatable))
&& (this.configData.input.type !== 'list')
&& (this.configData.input.type !== 'tag')
&& (this.configData.input.type !== 'group')
) {
let arrayCounter = 0;
let fieldArrayCounter = 0;
const config = {
id: uniqueId() + '_array',
label: this.configData.label,
initialCount: this.getInitArrayIndex(),
notRepeteable: !this.configData.repeatable,
groupFactory: () => {
let model;
if ((arrayCounter === 0)) {
model = this.modelFactory();
arrayCounter++;
} else {
const fieldArrayOfValueLenght = this.getInitValueCount(arrayCounter - 1);
let fieldValue = null;
if (fieldArrayOfValueLenght > 0) {
fieldValue = this.getInitFieldValue(arrayCounter - 1, fieldArrayCounter++);
if (fieldArrayCounter === fieldArrayOfValueLenght) {
fieldArrayCounter = 0;
arrayCounter++;
}
}
model = this.modelFactory(fieldValue, false);
}
setLayout(model, 'element', 'host', 'col');
if (model.hasLanguages) {
setLayout(model, 'grid', 'control', 'col');
}
return [model];
}
} as DynamicRowArrayModelConfig;
const layout: DynamicFormControlLayout = {
grid: {
group: 'form-row'
}
};
return new DynamicRowArrayModel(config, layout);
} else {
const model = this.modelFactory(this.getInitFieldValue());
if (model.hasLanguages) {
setLayout(model, 'grid', 'control', 'col');
}
return model;
}
}
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])) {
return this.initFormValues[fieldIds[0]].length;
} else if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length > 1) {
const values = [];
fieldIds.forEach((id) => {
if (this.initFormValues.hasOwnProperty(id)) {
values.push(this.initFormValues[id].length);
}
});
return values[index];
} else {
return 0;
}
}
protected getInitGroupValues(): FormFieldMetadataValueObject[] {
const fieldIds = this.getAllFieldIds();
if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length === 1 && this.initFormValues.hasOwnProperty(fieldIds[0])) {
return this.initFormValues[fieldIds[0]];
}
}
protected getInitFieldValues(fieldId): FormFieldMetadataValueObject[] {
if (isNotEmpty(this.initFormValues) && isNotNull(fieldId) && this.initFormValues.hasOwnProperty(fieldId)) {
return this.initFormValues[fieldId];
}
}
protected getInitFieldValue(outerIndex = 0, innerIndex = 0, fieldId?): FormFieldMetadataValueObject {
const fieldIds = fieldId || this.getAllFieldIds();
if (isNotEmpty(this.initFormValues)
&& isNotNull(fieldIds)
&& fieldIds.length === 1
&& this.initFormValues.hasOwnProperty(fieldIds[outerIndex])
&& this.initFormValues[fieldIds[outerIndex]].length > innerIndex) {
return this.initFormValues[fieldIds[outerIndex]][innerIndex];
} else if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length > 1) {
const values: FormFieldMetadataValueObject[] = [];
fieldIds.forEach((id) => {
if (this.initFormValues.hasOwnProperty(id)) {
const valueObj: FormFieldMetadataValueObject = Object.assign(new FormFieldMetadataValueObject(), this.initFormValues[id][innerIndex]);
valueObj.metadata = id;
// valueObj.value = this.initFormValues[id][innerIndex];
values.push(valueObj);
}
});
return values[outerIndex];
} else {
return null;
}
}
protected getInitArrayIndex() {
const fieldIds: any = this.getAllFieldIds();
if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length === 1 && this.initFormValues.hasOwnProperty(fieldIds)) {
return this.initFormValues[fieldIds].length;
} else if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length > 1) {
let counter = 0;
fieldIds.forEach((id) => {
if (this.initFormValues.hasOwnProperty(id)) {
counter = counter + this.initFormValues[id].length;
}
});
return (counter === 0) ? 1 : counter;
} else {
return 1;
}
}
protected getFieldId(): string {
const ids = this.getAllFieldIds();
return isNotNull(ids) ? ids[0] : null;
}
protected getAllFieldIds(): string[] {
if (Array.isArray(this.configData.selectableMetadata)) {
if (this.configData.selectableMetadata.length === 1) {
return [this.configData.selectableMetadata[0].metadata];
} else {
const ids = [];
this.configData.selectableMetadata.forEach((entry) => ids.push(entry.metadata));
return ids;
}
} else {
return null;
}
}
protected initModel(id?: string, label = true, labelEmpty = false, setErrors = true) {
const controlModel = Object.create(null);
// Sets input ID
this.fieldId = id ? id : this.getFieldId();
// Sets input name (with the original field's id value)
controlModel.name = this.fieldId;
// input ID doesn't allow dots, so replace them
controlModel.id = (this.fieldId).replace(/\./g, '_');
// Set read only option
controlModel.readOnly = this.parserOptions.readOnly;
controlModel.disabled = this.parserOptions.readOnly;
// Set label
this.setLabel(controlModel, label, labelEmpty);
controlModel.placeholder = this.configData.label;
if (this.configData.mandatory && setErrors) {
this.markAsRequired(controlModel);
}
if (this.hasRegex()) {
this.addPatternValidator(controlModel);
}
// Available Languages
if (this.configData.languageCodes && this.configData.languageCodes.length > 0) {
(controlModel as DsDynamicInputModel).languageCodes = this.configData.languageCodes;
}
return controlModel;
}
protected hasRegex() {
return hasValue(this.configData.input.regex);
}
protected addPatternValidator(controlModel) {
const regex = new RegExp(this.configData.input.regex);
controlModel.validators = Object.assign({}, controlModel.validators, {pattern: regex});
controlModel.errorMessages = Object.assign(
{},
controlModel.errorMessages,
{pattern: 'error.validation.pattern'});
}
protected markAsRequired(controlModel) {
controlModel.required = true;
controlModel.validators = Object.assign({}, controlModel.validators, {required: null});
controlModel.errorMessages = Object.assign(
{},
controlModel.errorMessages,
{required: this.configData.mandatoryMessage});
}
protected setLabel(controlModel, label = true, labelEmpty = false) {
if (label) {
controlModel.label = (labelEmpty) ? '&nbsp;' : this.configData.label;
}
}
protected setOptions(controlModel) {
// Checks if field has multiple values and sets options available
if (isNotUndefined(this.configData.selectableMetadata) && this.configData.selectableMetadata.length > 1) {
controlModel.options = [];
this.configData.selectableMetadata.forEach((option, key) => {
if (key === 0) {
controlModel.value = option.metadata;
}
controlModel.options.push({label: option.label, value: option.metadata});
});
}
}
public setAuthorityOptions(controlModel, authorityUuid) {
if (isNotEmpty(this.configData.selectableMetadata[0].authority)) {
controlModel.authorityOptions = new AuthorityOptions(
this.configData.selectableMetadata[0].authority,
this.configData.selectableMetadata[0].metadata,
authorityUuid,
this.configData.selectableMetadata[0].closed
)
}
}
public setValues(modelConfig: DsDynamicInputModelConfig, fieldValue: any, forceValueAsObj: boolean = false, groupModel?: boolean) {
if (isNotEmpty(fieldValue)) {
if (groupModel) {
// Array, values is an array
modelConfig.value = this.getInitGroupValues();
if (Array.isArray(modelConfig.value) && modelConfig.value.length > 0 && modelConfig.value[0].language) {
// Array Item has language, ex. AuthorityModel
modelConfig.language = modelConfig.value[0].language;
}
return;
}
if (typeof fieldValue === 'object') {
modelConfig.language = fieldValue.language;
if (forceValueAsObj) {
modelConfig.value = fieldValue;
} else {
modelConfig.value = fieldValue.value;
}
// if (hasValue(fieldValue.language)) {
// // Instance of FormFieldLanguageValueObject
// modelConfig.value = fieldValue.value;
// } else if (hasValue(fieldValue.metadata)) {
// // Is a combobox field's value
// modelConfig.value = fieldValue.value;
// } else {
// // Instance of FormFieldMetadataValueObject
// modelConfig.value = fieldValue;
// }
} 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

@@ -0,0 +1,111 @@
import { FormFieldModel } from '../models/form-field.model';
import { GroupFieldParser } from './group-field-parser';
import { DynamicGroupModel } from '../ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
import { ParserOptions } from './parser-options';
describe('GroupFieldParser test suite', () => {
let field: FormFieldModel;
let initFormValues = {};
const parserOptions: ParserOptions = {
readOnly: false,
submissionScope: 'testScopeUUID',
authorityUuid: 'WORKSPACE'
};
beforeEach(() => {
field = {
input: {
type: 'group'
},
rows: [
{
fields: [
{
input: {
type: 'onebox'
},
label: 'Author',
mandatory: 'false',
repeatable: false,
hints: 'Enter the name of the author.',
selectableMetadata: [
{
metadata: 'author'
}
],
languageCodes: []
},
{
input: {
type: 'onebox'
},
label: 'Affiliation',
mandatory: false,
repeatable: true,
hints: 'Enter the affiliation of the author.',
selectableMetadata: [
{
metadata: 'affiliation'
}
],
languageCodes: []
}
]
}
],
label: 'Authors',
mandatory: 'true',
repeatable: false,
mandatoryMessage: 'Entering at least the first author is mandatory.',
hints: 'Enter the names of the authors of this item.',
selectableMetadata: [
{
metadata: 'author'
}
],
languageCodes: []
} as FormFieldModel;
});
it('should init parser properly', () => {
const parser = new GroupFieldParser(field, initFormValues, parserOptions);
expect(parser instanceof GroupFieldParser).toBe(true);
});
it('should return a DynamicGroupModel object', () => {
const parser = new GroupFieldParser(field, initFormValues, parserOptions);
const fieldModel = parser.parse();
expect(fieldModel instanceof DynamicGroupModel).toBe(true);
});
it('should throw when rows configuration is empty', () => {
field.rows = null;
const parser = new GroupFieldParser(field, initFormValues, parserOptions);
expect(() => parser.parse())
.toThrow();
});
it('should set group init value properly', () => {
initFormValues = {
author: [new FormFieldMetadataValueObject('test author')],
affiliation: [new FormFieldMetadataValueObject('test affiliation')]
};
const parser = new GroupFieldParser(field, initFormValues, parserOptions);
const fieldModel = parser.parse();
const expectedValue = [{
author: new FormFieldMetadataValueObject('test author'),
affiliation: new FormFieldMetadataValueObject('test affiliation')
}];
expect(fieldModel.value).toEqual(expectedValue);
});
});

View File

@@ -0,0 +1,62 @@
import { FieldParser } from './field-parser';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
import { FormFieldModel } from '../models/form-field.model';
import {
DynamicGroupModel,
DynamicGroupModelConfig,
PLACEHOLDER_PARENT_METADATA
} from '../ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model';
import { isNotEmpty } from '../../../empty.util';
import { FormRowModel } from '../../../../core/shared/config/config-submission-forms.model';
export class GroupFieldParser extends FieldParser {
public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean) {
const modelConfiguration: DynamicGroupModelConfig = this.initModel(null, label);
modelConfiguration.scopeUUID = this.parserOptions.authorityUuid;
modelConfiguration.submissionScope = this.parserOptions.submissionScope;
if (this.configData && this.configData.rows && this.configData.rows.length > 0) {
modelConfiguration.formConfiguration = this.configData.rows;
modelConfiguration.relationFields = [];
this.configData.rows.forEach((row: FormRowModel) => {
row.fields.forEach((field: FormFieldModel) => {
if (field.selectableMetadata[0].metadata === this.configData.selectableMetadata[0].metadata) {
if (!field.mandatory) {
// throw new Error(`Configuration not valid: Main field ${this.configData.selectableMetadata[0].metadata} may be mandatory`);
}
modelConfiguration.mandatoryField = this.configData.selectableMetadata[0].metadata;
} else {
modelConfiguration.relationFields.push(field.selectableMetadata[0].metadata);
}
})
});
} else {
throw new Error(`Configuration not valid: ${modelConfiguration.name}`);
}
if (isNotEmpty(this.getInitGroupValues())) {
modelConfiguration.value = [];
const mandatoryFieldEntries: FormFieldMetadataValueObject[] = this.getInitFieldValues(modelConfiguration.mandatoryField);
mandatoryFieldEntries.forEach((entry, index) => {
const item = Object.create(null);
const listFields = [modelConfiguration.mandatoryField].concat(modelConfiguration.relationFields);
listFields.forEach((fieldId) => {
const value = this.getInitFieldValue(0, index, [fieldId]);
item[fieldId] = isNotEmpty(value) ? value : PLACEHOLDER_PARENT_METADATA;
});
modelConfiguration.value.push(item);
})
}
const cls = {
element: {
container: 'mb-3'
}
};
const model = new DynamicGroupModel(modelConfiguration, cls);
model.name = this.getFieldId();
return model;
}
}

View File

@@ -0,0 +1,75 @@
import { FormFieldModel } from '../models/form-field.model';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
import { ListFieldParser } from './list-field-parser';
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';
import { ParserOptions } from './parser-options';
describe('ListFieldParser test suite', () => {
let field: FormFieldModel;
let initFormValues = {};
const parserOptions: ParserOptions = {
readOnly: false,
submissionScope: 'testScopeUUID',
authorityUuid: null
};
beforeEach(() => {
field = {
input: {
type: 'list'
},
label: 'Type',
mandatory: 'false',
repeatable: true,
hints: 'Select the type.',
selectableMetadata: [
{
metadata: 'type',
authority: 'type_programme',
closed: false
}
],
languageCodes: []
} as FormFieldModel;
});
it('should init parser properly', () => {
const parser = new ListFieldParser(field, initFormValues, parserOptions);
expect(parser instanceof ListFieldParser).toBe(true);
});
it('should return a DynamicListCheckboxGroupModel object when repeatable option is true', () => {
const parser = new ListFieldParser(field, initFormValues, parserOptions);
const fieldModel = parser.parse();
expect(fieldModel instanceof DynamicListCheckboxGroupModel).toBe(true);
});
it('should return a DynamicListRadioGroupModel object when repeatable option is false', () => {
field.repeatable = false;
const parser = new ListFieldParser(field, initFormValues, parserOptions);
const fieldModel = parser.parse();
expect(fieldModel instanceof DynamicListRadioGroupModel).toBe(true);
});
it('should set init value properly', () => {
initFormValues = {
type: [new FormFieldMetadataValueObject('test type')],
};
const expectedValue = [new FormFieldMetadataValueObject('test type')];
const parser = new ListFieldParser(field, initFormValues, parserOptions);
const fieldModel = parser.parse();
expect(fieldModel.value).toEqual(expectedValue);
});
});

View File

@@ -0,0 +1,44 @@
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 (isNotEmpty(this.getInitGroupValues())) {
listModelConfig.value = [];
this.getInitGroupValues().forEach((value: any) => {
if (value instanceof FormFieldMetadataValueObject) {
listModelConfig.value.push(value);
} else {
const valueObj = new FormFieldMetadataValueObject(value);
listModelConfig.value.push(valueObj);
}
});
}
this.setAuthorityOptions(listModelConfig, this.parserOptions.authorityUuid);
}
let listModel;
if (listModelConfig.repeatable) {
listModelConfig.group = [];
listModel = new DynamicListCheckboxGroupModel(listModelConfig);
} else {
listModelConfig.options = [];
listModel = new DynamicListRadioGroupModel(listModelConfig);
}
return listModel;
}
}

View File

@@ -0,0 +1,65 @@
import { FormFieldModel } from '../models/form-field.model';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
import { LookupFieldParser } from './lookup-field-parser';
import { DynamicLookupModel } from '../ds-dynamic-form-ui/models/lookup/dynamic-lookup.model';
import { ParserOptions } from './parser-options';
describe('LookupFieldParser test suite', () => {
let field: FormFieldModel;
let initFormValues = {};
const parserOptions: ParserOptions = {
readOnly: false,
submissionScope: 'testScopeUUID',
authorityUuid: null
};
beforeEach(() => {
field = {
input: {
type: 'lookup'
},
label: 'Journal',
mandatory: 'false',
repeatable: false,
hints: 'Enter the name of the journal where the item has been published, if any.',
selectableMetadata: [
{
metadata: 'journal',
authority: 'JOURNALAuthority',
closed: false
}
],
languageCodes: []
} as FormFieldModel;
});
it('should init parser properly', () => {
const parser = new LookupFieldParser(field, initFormValues, parserOptions);
expect(parser instanceof LookupFieldParser).toBe(true);
});
it('should return a DynamicLookupModel object when repeatable option is false', () => {
const parser = new LookupFieldParser(field, initFormValues, parserOptions);
const fieldModel = parser.parse();
expect(fieldModel instanceof DynamicLookupModel).toBe(true);
});
it('should set init value properly', () => {
initFormValues = {
journal: [new FormFieldMetadataValueObject('test journal')],
};
const expectedValue = new FormFieldMetadataValueObject('test journal');
const parser = new LookupFieldParser(field, initFormValues, parserOptions);
const fieldModel = parser.parse();
expect(fieldModel.value).toEqual(expectedValue);
});
});

View File

@@ -0,0 +1,19 @@
import { FieldParser } from './field-parser';
import { DynamicLookupModel, DynamicLookupModelConfig } from '../ds-dynamic-form-ui/models/lookup/dynamic-lookup.model';
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
export class LookupFieldParser extends FieldParser {
public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any {
if (this.configData.selectableMetadata[0].authority) {
const lookupModelConfig: DynamicLookupModelConfig = this.initModel(null, label);
this.setAuthorityOptions(lookupModelConfig, this.parserOptions.authorityUuid);
this.setValues(lookupModelConfig, fieldValue, true);
return new DynamicLookupModel(lookupModelConfig);
}
}
}

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