mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-17 23:13:04 +00:00
Merge branch 'master' into search-features
Conflicts: package.json src/app/shared/shared.module.ts yarn.lock
This commit is contained in:
@@ -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,
|
||||
|
16
src/app/core/cache/response-cache.models.ts
vendored
16
src/app/core/cache/response-cache.models.ts
vendored
@@ -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 */
|
||||
|
@@ -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
|
||||
|
@@ -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 {
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
}
|
||||
|
19
src/app/core/integration/authority.service.ts
Normal file
19
src/app/core/integration/authority.service.ts
Normal 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();
|
||||
}
|
||||
}
|
12
src/app/core/integration/integration-data.ts
Normal file
12
src/app/core/integration/integration-data.ts
Normal 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[]
|
||||
) { }
|
||||
}
|
17
src/app/core/integration/integration-object-factory.ts
Normal file
17
src/app/core/integration/integration-object-factory.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
@@ -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}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
83
src/app/core/integration/integration.service.spec.ts
Normal file
83
src/app/core/integration/integration.service.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
85
src/app/core/integration/integration.service.ts
Normal file
85
src/app/core/integration/integration.service.ts
Normal 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();
|
||||
}
|
||||
|
||||
}
|
4
src/app/core/integration/intergration-type.ts
Normal file
4
src/app/core/integration/intergration-type.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
export enum IntegrationType {
|
||||
Authority = 'authority'
|
||||
}
|
16
src/app/core/integration/models/authority-options.model.ts
Normal file
16
src/app/core/integration/models/authority-options.model.ts
Normal 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;
|
||||
}
|
||||
}
|
20
src/app/core/integration/models/authority-value.model.ts
Normal file
20
src/app/core/integration/models/authority-value.model.ts
Normal 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;
|
||||
}
|
14
src/app/core/integration/models/integration-options.model.ts
Normal file
14
src/app/core/integration/models/integration-options.model.ts
Normal 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) {
|
||||
|
||||
}
|
||||
}
|
12
src/app/core/integration/models/integration.model.ts
Normal file
12
src/app/core/integration/models/integration.model.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { autoserialize } from 'cerialize';
|
||||
|
||||
export abstract class IntegrationModel {
|
||||
|
||||
@autoserialize
|
||||
public type: string;
|
||||
|
||||
@autoserialize
|
||||
public _links: {
|
||||
[name: string]: string
|
||||
}
|
||||
}
|
23
src/app/core/shared/config/config-authority.model.ts
Normal file
23
src/app/core/shared/config/config-authority.model.ts
Normal 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;
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
@@ -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[];
|
||||
}
|
||||
|
@@ -10,5 +10,6 @@ export enum ConfigType {
|
||||
SubmissionForm = 'submissionform',
|
||||
SubmissionForms = 'submissionforms',
|
||||
SubmissionSections = 'submissionsections',
|
||||
SubmissionSection = 'submissionsection'
|
||||
SubmissionSection = 'submissionsection',
|
||||
Authority = 'authority'
|
||||
}
|
||||
|
13
src/app/shared/animations/shrink.ts
Normal file
13
src/app/shared/animations/shrink.ts
Normal 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}))
|
||||
])
|
||||
]);
|
34
src/app/shared/chips/chips.component.html
Normal file
34
src/app/shared/chips/chips.component.html
Normal 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>
|
9
src/app/shared/chips/chips.component.scss
Normal file
9
src/app/shared/chips/chips.component.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
@import "../../../styles/variables";
|
||||
|
||||
.chip-selected {
|
||||
background-color: map-get($theme-colors, info) !important;
|
||||
}
|
||||
|
||||
.chip-label {
|
||||
max-width: 10rem;
|
||||
}
|
227
src/app/shared/chips/chips.component.spec.ts
Normal file
227
src/app/shared/chips/chips.component.spec.ts
Normal 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;
|
||||
}
|
95
src/app/shared/chips/chips.component.ts
Normal file
95
src/app/shared/chips/chips.component.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
78
src/app/shared/chips/models/chips-item.model.spec.ts
Normal file
78
src/app/shared/chips/models/chips-item.model.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
72
src/app/shared/chips/models/chips-item.model.ts
Normal file
72
src/app/shared/chips/models/chips-item.model.ts
Normal 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;
|
||||
}
|
||||
}
|
126
src/app/shared/chips/models/chips.model.spec.ts
Normal file
126
src/app/shared/chips/models/chips.model.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
159
src/app/shared/chips/models/chips.model.ts
Normal file
159
src/app/shared/chips/models/chips.model.ts
Normal 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);
|
||||
}
|
||||
}
|
19
src/app/shared/date.util.ts
Normal file
19
src/app/shared/date.util.ts
Normal 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`;
|
||||
|
||||
}
|
@@ -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>
|
@@ -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);
|
||||
});
|
||||
});
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -0,0 +1,5 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -0,0 +1,3 @@
|
||||
.col-lg-1 {
|
||||
width: auto;
|
||||
}
|
@@ -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;
|
||||
|
||||
}
|
@@ -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();
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
import { DynamicFormGroupModel } from '@ng-dynamic-forms/core';
|
||||
|
||||
export class DynamicRowGroupModel extends DynamicFormGroupModel {
|
||||
isRowGroup = true;
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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>
|
@@ -0,0 +1,3 @@
|
||||
.close {
|
||||
top: -2.5rem;
|
||||
}
|
@@ -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;
|
||||
|
||||
}
|
@@ -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());
|
||||
}
|
||||
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -0,0 +1 @@
|
||||
|
@@ -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;
|
||||
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
@@ -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';
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -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;
|
||||
//}
|
@@ -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;
|
||||
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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>
|
||||
|
||||
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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>
|
||||
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
|
||||
}
|
@@ -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>
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
830
src/app/shared/form/builder/form-builder.service.spec.ts
Normal file
830
src/app/shared/form/builder/form-builder.service.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
284
src/app/shared/form/builder/form-builder.service.ts
Normal file
284
src/app/shared/form/builder/form-builder.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
export interface FormFieldChangedObject {
|
||||
string: any
|
||||
}
|
43
src/app/shared/form/builder/models/form-field.model.ts
Normal file
43
src/app/shared/form/builder/models/form-field.model.ts
Normal 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;
|
||||
}
|
105
src/app/shared/form/builder/parsers/concat-field-parser.ts
Normal file
105
src/app/shared/form/builder/parsers/concat-field-parser.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
});
|
||||
});
|
36
src/app/shared/form/builder/parsers/date-field-parser.ts
Normal file
36
src/app/shared/form/builder/parsers/date-field-parser.ts
Normal 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;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
});
|
||||
|
||||
});
|
35
src/app/shared/form/builder/parsers/dropdown-field-parser.ts
Normal file
35
src/app/shared/form/builder/parsers/dropdown-field-parser.ts
Normal 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.`);
|
||||
}
|
||||
}
|
||||
}
|
307
src/app/shared/form/builder/parsers/field-parser.ts
Normal file
307
src/app/shared/form/builder/parsers/field-parser.ts
Normal 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) ? ' ' : 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;
|
||||
}
|
||||
|
||||
}
|
111
src/app/shared/form/builder/parsers/group-field-parser.spec.ts
Normal file
111
src/app/shared/form/builder/parsers/group-field-parser.spec.ts
Normal 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);
|
||||
});
|
||||
|
||||
});
|
62
src/app/shared/form/builder/parsers/group-field-parser.ts
Normal file
62
src/app/shared/form/builder/parsers/group-field-parser.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
});
|
||||
|
||||
});
|
44
src/app/shared/form/builder/parsers/list-field-parser.ts
Normal file
44
src/app/shared/form/builder/parsers/list-field-parser.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@@ -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);
|
||||
});
|
||||
|
||||
});
|
19
src/app/shared/form/builder/parsers/lookup-field-parser.ts
Normal file
19
src/app/shared/form/builder/parsers/lookup-field-parser.ts
Normal 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
Reference in New Issue
Block a user