mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-14 13:33:03 +00:00
Merged dynamic form module
This commit is contained in:
@@ -80,14 +80,19 @@
|
|||||||
"@angular/router": "^5.2.5",
|
"@angular/router": "^5.2.5",
|
||||||
"@angularclass/bootloader": "1.0.1",
|
"@angularclass/bootloader": "1.0.1",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^1.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^1.0.0",
|
||||||
|
"@ng-dynamic-forms/core": "5.4.7",
|
||||||
|
"@ng-dynamic-forms/ui-ng-bootstrap": "5.4.7",
|
||||||
"@ngrx/effects": "^5.1.0",
|
"@ngrx/effects": "^5.1.0",
|
||||||
"@ngrx/router-store": "^5.0.1",
|
"@ngrx/router-store": "^5.0.1",
|
||||||
"@ngrx/store": "^5.1.0",
|
"@ngrx/store": "^5.1.0",
|
||||||
"@nguniversal/express-engine": "5.0.0-beta.5",
|
"@nguniversal/express-engine": "5.0.0-beta.5",
|
||||||
"@ngx-translate/core": "9.1.1",
|
"@ngx-translate/core": "9.1.1",
|
||||||
"@ngx-translate/http-loader": "2.0.1",
|
"@ngx-translate/http-loader": "2.0.1",
|
||||||
|
"@nicky-lenaers/ngx-scroll-to": "^0.6.0",
|
||||||
"angular-idle-preload": "2.0.4",
|
"angular-idle-preload": "2.0.4",
|
||||||
|
"angular-sortablejs": "^2.5.0",
|
||||||
"angulartics2": "^5.2.0",
|
"angulartics2": "^5.2.0",
|
||||||
|
"angular2-text-mask": "8.0.4",
|
||||||
"body-parser": "1.18.2",
|
"body-parser": "1.18.2",
|
||||||
"bootstrap": "^4.0.0",
|
"bootstrap": "^4.0.0",
|
||||||
"cerialize": "0.1.18",
|
"cerialize": "0.1.18",
|
||||||
@@ -103,10 +108,14 @@
|
|||||||
"jsonschema": "1.2.2",
|
"jsonschema": "1.2.2",
|
||||||
"methods": "1.1.2",
|
"methods": "1.1.2",
|
||||||
"morgan": "1.9.0",
|
"morgan": "1.9.0",
|
||||||
|
"ng2-file-upload": "1.2.1",
|
||||||
|
"ngx-infinite-scroll": "0.8.2",
|
||||||
"ngx-pagination": "3.0.3",
|
"ngx-pagination": "3.0.3",
|
||||||
"pem": "1.12.3",
|
"pem": "1.12.3",
|
||||||
"reflect-metadata": "0.1.12",
|
"reflect-metadata": "0.1.12",
|
||||||
"rxjs": "5.5.6",
|
"rxjs": "5.5.6",
|
||||||
|
"sortablejs": "1.7.0",
|
||||||
|
"text-mask-core": "5.0.1",
|
||||||
"ts-md5": "^1.2.4",
|
"ts-md5": "^1.2.4",
|
||||||
"uuid": "^3.2.1",
|
"uuid": "^3.2.1",
|
||||||
"webfontloader": "1.6.28",
|
"webfontloader": "1.6.28",
|
||||||
|
@@ -154,5 +154,20 @@
|
|||||||
"item": "Error fetching item",
|
"item": "Error fetching item",
|
||||||
"objects": "Error fetching objects",
|
"objects": "Error fetching objects",
|
||||||
"search-results": "Error fetching search results"
|
"search-results": "Error fetching search results"
|
||||||
|
},
|
||||||
|
"form": {
|
||||||
|
"submit": "Submit",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"search": "Search",
|
||||||
|
"remove": "Remove",
|
||||||
|
"first-name": "First name",
|
||||||
|
"last-name": "Last name",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"no-results": "No results found",
|
||||||
|
"no-value": "No value entered",
|
||||||
|
"group-collapse": "Collapse",
|
||||||
|
"group-expand": "Expand",
|
||||||
|
"group-collapse-help": "Click here to collapse",
|
||||||
|
"group-expand-help": "Click here to expand and add more element"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,32 +1,35 @@
|
|||||||
import { ActionReducerMap } from '@ngrx/store';
|
import { ActionReducerMap } from '@ngrx/store';
|
||||||
import * as fromRouter from '@ngrx/router-store';
|
import * as fromRouter from '@ngrx/router-store';
|
||||||
|
|
||||||
import { headerReducer, HeaderState } from './header/header.reducer';
|
import { headerReducer, HeaderState } from './header/header.reducer';
|
||||||
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
|
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
|
||||||
import {
|
import { formReducer, FormState } from './shared/form/form.reducers';
|
||||||
SearchSidebarState,
|
import {
|
||||||
sidebarReducer
|
SearchSidebarState,
|
||||||
} from './+search-page/search-sidebar/search-sidebar.reducer';
|
sidebarReducer
|
||||||
import {
|
} from './+search-page/search-sidebar/search-sidebar.reducer';
|
||||||
filterReducer,
|
import {
|
||||||
SearchFiltersState
|
filterReducer,
|
||||||
} from './+search-page/search-filters/search-filter/search-filter.reducer';
|
SearchFiltersState
|
||||||
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
|
} from './+search-page/search-filters/search-filter/search-filter.reducer';
|
||||||
|
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer';
|
||||||
export interface AppState {
|
|
||||||
router: fromRouter.RouterReducerState;
|
export interface AppState {
|
||||||
hostWindow: HostWindowState;
|
router: fromRouter.RouterReducerState;
|
||||||
header: HeaderState;
|
hostWindow: HostWindowState;
|
||||||
searchSidebar: SearchSidebarState;
|
header: HeaderState;
|
||||||
searchFilter: SearchFiltersState;
|
forms: FormState;
|
||||||
truncatable: TruncatablesState;
|
searchSidebar: SearchSidebarState;
|
||||||
}
|
searchFilter: SearchFiltersState;
|
||||||
|
truncatable: TruncatablesState;
|
||||||
export const appReducers: ActionReducerMap<AppState> = {
|
}
|
||||||
router: fromRouter.routerReducer,
|
|
||||||
hostWindow: hostWindowReducer,
|
export const appReducers: ActionReducerMap<AppState> = {
|
||||||
header: headerReducer,
|
router: fromRouter.routerReducer,
|
||||||
searchSidebar: sidebarReducer,
|
hostWindow: hostWindowReducer,
|
||||||
searchFilter: filterReducer,
|
header: headerReducer,
|
||||||
truncatable: truncatableReducer
|
forms: formReducer,
|
||||||
};
|
searchSidebar: sidebarReducer,
|
||||||
|
searchFilter: filterReducer,
|
||||||
|
truncatable: truncatableReducer
|
||||||
|
};
|
||||||
|
11
src/app/core/cache/response-cache.models.ts
vendored
11
src/app/core/cache/response-cache.models.ts
vendored
@@ -5,6 +5,7 @@ import { BrowseDefinition } from '../shared/browse-definition.model';
|
|||||||
import { ConfigObject } from '../shared/config/config.model';
|
import { ConfigObject } from '../shared/config/config.model';
|
||||||
import { FacetValue } from '../../+search-page/search-service/facet-value.model';
|
import { FacetValue } from '../../+search-page/search-service/facet-value.model';
|
||||||
import { SearchFilterConfig } from '../../+search-page/search-service/search-filter-config.model';
|
import { SearchFilterConfig } from '../../+search-page/search-service/search-filter-config.model';
|
||||||
|
import { IntegrationModel } from '../integration/models/integration.model';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
export class RestResponse {
|
export class RestResponse {
|
||||||
@@ -106,4 +107,14 @@ export class ConfigSuccessResponse extends RestResponse {
|
|||||||
super(true, statusCode);
|
super(true, statusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class IntegrationSuccessResponse extends RestResponse {
|
||||||
|
constructor(
|
||||||
|
public dataDefinition: IntegrationModel[],
|
||||||
|
public statusCode: string,
|
||||||
|
public pageInfo?: PageInfo
|
||||||
|
) {
|
||||||
|
super(true, statusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
/* tslint:enable:max-classes-per-file */
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
@@ -44,6 +44,7 @@ import { HALEndpointService } from './shared/hal-endpoint.service';
|
|||||||
import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service';
|
import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service';
|
||||||
import { FacetValueMapResponseParsingService } from './data/facet-value-map-response-parsing.service';
|
import { FacetValueMapResponseParsingService } from './data/facet-value-map-response-parsing.service';
|
||||||
import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service';
|
import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service';
|
||||||
|
import { UploaderService } from '../shared/uploader/uploader.service';
|
||||||
|
|
||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -88,6 +89,7 @@ const PROVIDERS = [
|
|||||||
SubmissionDefinitionsConfigService,
|
SubmissionDefinitionsConfigService,
|
||||||
SubmissionFormsConfigService,
|
SubmissionFormsConfigService,
|
||||||
SubmissionSectionsConfigService,
|
SubmissionSectionsConfigService,
|
||||||
|
UploaderService,
|
||||||
UUIDService,
|
UUIDService,
|
||||||
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
|
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
|
||||||
];
|
];
|
||||||
|
@@ -7,6 +7,7 @@ import { ResponseParsingService } from './parsing.service';
|
|||||||
import { EndpointMapResponseParsingService } from './endpoint-map-response-parsing.service';
|
import { EndpointMapResponseParsingService } from './endpoint-map-response-parsing.service';
|
||||||
import { BrowseResponseParsingService } from './browse-response-parsing.service';
|
import { BrowseResponseParsingService } from './browse-response-parsing.service';
|
||||||
import { ConfigResponseParsingService } from './config-response-parsing.service';
|
import { ConfigResponseParsingService } from './config-response-parsing.service';
|
||||||
|
import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
@@ -174,6 +175,15 @@ export class ConfigRequest 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 {
|
export class RequestError extends Error {
|
||||||
statusText: string;
|
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 a Eperson 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,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}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
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 { SubmissionDefinitionsModel } from './config-submission-definitions.model';
|
||||||
import { ConfigType } from './config-type';
|
import { ConfigType } from './config-type';
|
||||||
import { ConfigObject } from './config.model';
|
import { ConfigObject } from './config.model';
|
||||||
|
import { ConfigAuthorityModel } from './config-authority.model';
|
||||||
|
|
||||||
export class ConfigObjectFactory {
|
export class ConfigObjectFactory {
|
||||||
public static getConstructor(type): GenericConstructor<ConfigObject> {
|
public static getConstructor(type): GenericConstructor<ConfigObject> {
|
||||||
@@ -22,6 +23,9 @@ export class ConfigObjectFactory {
|
|||||||
case ConfigType.SubmissionSections: {
|
case ConfigType.SubmissionSections: {
|
||||||
return SubmissionSectionModel
|
return SubmissionSectionModel
|
||||||
}
|
}
|
||||||
|
case ConfigType.Authority: {
|
||||||
|
return ConfigAuthorityModel
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,14 @@
|
|||||||
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
|
import { autoserialize, inheritSerialization } from 'cerialize';
|
||||||
import { ConfigObject } from './config.model';
|
import { ConfigObject } from './config.model';
|
||||||
|
import { FormFieldModel } from '../../../shared/form/builder/models/form-field.model';
|
||||||
|
|
||||||
|
export interface FormRowModel {
|
||||||
|
fields: FormFieldModel[];
|
||||||
|
}
|
||||||
|
|
||||||
@inheritSerialization(ConfigObject)
|
@inheritSerialization(ConfigObject)
|
||||||
export class SubmissionFormsModel extends ConfigObject {
|
export class SubmissionFormsModel extends ConfigObject {
|
||||||
|
|
||||||
@autoserialize
|
@autoserialize
|
||||||
fields: any[];
|
rows: FormRowModel[];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -10,5 +10,6 @@ export enum ConfigType {
|
|||||||
SubmissionForm = 'submissionform',
|
SubmissionForm = 'submissionform',
|
||||||
SubmissionForms = 'submissionforms',
|
SubmissionForms = 'submissionforms',
|
||||||
SubmissionSections = 'submissionsections',
|
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}))
|
||||||
|
])
|
||||||
|
]);
|
33
src/app/shared/chips/chips.component.html
Normal file
33
src/app/shared/chips/chips.component.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<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 [ngbTooltip]="tipContent"
|
||||||
|
triggers="manual"
|
||||||
|
#t="ngbTooltip"
|
||||||
|
class="nav-item mr-2 mb-1"
|
||||||
|
(click)="t.close()"
|
||||||
|
(dragstart)="onDragStart(t, i)"
|
||||||
|
(dragend)="onDragEnd(i)"
|
||||||
|
(mouseover)="showTooltip(t, i, c.display)"
|
||||||
|
(mouseout)="t.close()">
|
||||||
|
<a class="flex-sm-fill text-sm-center nav-link active"
|
||||||
|
href="#"
|
||||||
|
[ngClass]="{'chip-selected disabled': (editable && c.editMode) || dragged == i}"
|
||||||
|
(click)="t.close();chipsSelected($event, i);">
|
||||||
|
<span>
|
||||||
|
<ng-container *ngIf="c.hasIcons()">
|
||||||
|
<i *ngFor="let icon of c.icons; let i = index; let l = last"
|
||||||
|
class="fa {{icon.style}}"
|
||||||
|
[class.mr-2]="l"
|
||||||
|
aria-hidden="true"></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;
|
||||||
|
}
|
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 { 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: boolean;
|
||||||
|
|
||||||
|
@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'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (!this.editable) {
|
||||||
|
this.editable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(tooltip: NgbTooltip, index) {
|
||||||
|
tooltip.close();
|
||||||
|
this.uploaderService.overrideDragOverPage();
|
||||||
|
this.dragged = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragEnd(event) {
|
||||||
|
this.uploaderService.allowDragOverPage();
|
||||||
|
this.dragged = -1;
|
||||||
|
this.chips.updateOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
showTooltip(tooltip: NgbTooltip, index, content) {
|
||||||
|
tooltip.close();
|
||||||
|
if (!this.chips.getChipByIndex(index).editMode && this.dragged === -1) {
|
||||||
|
this.tipText = content;
|
||||||
|
this.cdr.detectChanges();
|
||||||
|
tooltip.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
73
src/app/shared/chips/models/chips-item.model.ts
Normal file
73
src/app/shared/chips/models/chips-item.model.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { uniqueId } from 'lodash';
|
||||||
|
import { isNotEmpty } from '../../empty.util';
|
||||||
|
|
||||||
|
export interface ChipsItemIcon {
|
||||||
|
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,
|
||||||
|
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 ( typeof this.item === 'object') {
|
||||||
|
// Check If displayField is in an internal object
|
||||||
|
const obj = this.objToDisplay ? this.item[this.objToDisplay] : this.item;
|
||||||
|
const displayFieldBkp = 'value';
|
||||||
|
|
||||||
|
if (obj instanceof Object && obj && obj[this.fieldToDisplay]) {
|
||||||
|
value = obj[this.fieldToDisplay];
|
||||||
|
} else if (obj instanceof Object && obj && obj[displayFieldBkp]) {
|
||||||
|
value = obj[displayFieldBkp];
|
||||||
|
} else {
|
||||||
|
value = obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.display = value;
|
||||||
|
}
|
||||||
|
}
|
130
src/app/shared/chips/models/chips.model.ts
Normal file
130
src/app/shared/chips/models/chips.model.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { findIndex, isEqual } from 'lodash';
|
||||||
|
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||||
|
import { ChipsItem, ChipsItemIcon } from './chips-item.model';
|
||||||
|
import { hasValue } from '../../empty.util';
|
||||||
|
import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model';
|
||||||
|
import { AuthorityValueModel } from '../../../core/integration/models/authority-value.model';
|
||||||
|
|
||||||
|
export interface ChipsIconsConfig {
|
||||||
|
[metadata: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Chips {
|
||||||
|
chipsItems: BehaviorSubject<ChipsItem[]>;
|
||||||
|
displayField: string;
|
||||||
|
displayObj: string;
|
||||||
|
iconsConfig: ChipsIconsConfig;
|
||||||
|
|
||||||
|
private _items: ChipsItem[];
|
||||||
|
|
||||||
|
constructor(items: any[] = [],
|
||||||
|
displayField: string = 'display',
|
||||||
|
displayObj?: string,
|
||||||
|
iconsConfig?: ChipsIconsConfig) {
|
||||||
|
|
||||||
|
this.displayField = displayField;
|
||||||
|
this.displayObj = displayObj;
|
||||||
|
this.iconsConfig = iconsConfig || Object.create({});
|
||||||
|
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 = [];
|
||||||
|
Object.keys(item)
|
||||||
|
.forEach((metadata) => {
|
||||||
|
const value = item[metadata];
|
||||||
|
if (hasValue(value)
|
||||||
|
&& (value instanceof FormFieldMetadataValueObject || value instanceof AuthorityValueModel)
|
||||||
|
&& ((value as FormFieldMetadataValueObject).authority || (value as AuthorityValueModel).id)
|
||||||
|
&& this.iconsConfig.hasOwnProperty(metadata)) {
|
||||||
|
|
||||||
|
const icon: ChipsItemIcon = {
|
||||||
|
style: this.iconsConfig[metadata]
|
||||||
|
};
|
||||||
|
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,442 @@
|
|||||||
|
<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"
|
||||||
|
[dynamicId]="bindId && checkboxModel.id"
|
||||||
|
[formControlName]="checkboxModel.id"
|
||||||
|
[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"
|
||||||
|
(blur)="onBlur($event)"
|
||||||
|
(change)="onValueChange($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-addon" [innerHTML]="model.prefix"></div>
|
||||||
|
|
||||||
|
<ng-container *ngTemplateOutlet="inputTemplate;
|
||||||
|
context:{bindId: bindId, model: model, showErrorMessages: showErrorMessages}">
|
||||||
|
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<div *ngIf="model.suffix" class="input-group-addon" [innerHTML]="model.suffix"></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" 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>
|
||||||
|
|
||||||
|
|
||||||
|
<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"
|
||||||
|
(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>
|
||||||
|
|
||||||
|
<small *ngIf="showHint" 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 }}</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,176 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
ContentChildren,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
Output,
|
||||||
|
QueryList,
|
||||||
|
SimpleChanges
|
||||||
|
} from '@angular/core';
|
||||||
|
import { FormGroup } from '@angular/forms';
|
||||||
|
import {
|
||||||
|
DynamicDatePickerModel,
|
||||||
|
DynamicFormArrayGroupModel,
|
||||||
|
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 } from './models/ds-dynamic-group/dynamic-group.model';
|
||||||
|
import { DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER } from './models/ds-date-picker/ds-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';
|
||||||
|
|
||||||
|
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'
|
||||||
|
TypeTag = 14, // 'TYPETAG'
|
||||||
|
List = 15, // 'TYPELIST'
|
||||||
|
Relation = 16, // Dynamic Group
|
||||||
|
DsDatePicker = 17, // Ds Date Picker
|
||||||
|
Lookup = 18, // LOOKUP
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 (model instanceof DynamicGroupModel) ? NGBootstrapFormControlType.DynamicGroup : NGBootstrapFormControlType.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.TypeTag;
|
||||||
|
|
||||||
|
case DYNAMIC_FORM_CONTROL_TYPE_RELATION:
|
||||||
|
return NGBootstrapFormControlType.Relation;
|
||||||
|
|
||||||
|
case DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER:
|
||||||
|
return NGBootstrapFormControlType.DsDatePicker;
|
||||||
|
|
||||||
|
case DYNAMIC_FORM_CONTROL_TYPE_LOOKUP:
|
||||||
|
return NGBootstrapFormControlType.Lookup;
|
||||||
|
|
||||||
|
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,47 @@
|
|||||||
|
<div class="d-flex">
|
||||||
|
<div class="mr-3">
|
||||||
|
<ds-number-picker
|
||||||
|
[disabled]="model.disabled"
|
||||||
|
[min]="minYear"
|
||||||
|
[max]="maxYear"
|
||||||
|
[name]="'year'"
|
||||||
|
[size]="4"
|
||||||
|
[(ngModel)]="initialYear"
|
||||||
|
[value]="year"
|
||||||
|
[invalid]="invalid"
|
||||||
|
[placeholder]='yearPlaceholder'
|
||||||
|
(change)="onChange($event)"
|
||||||
|
></ds-number-picker>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mr-3">
|
||||||
|
<ds-number-picker
|
||||||
|
[min]="minMonth"
|
||||||
|
[max]="maxMonth"
|
||||||
|
[name]="'month'"
|
||||||
|
[size]="6"
|
||||||
|
[(ngModel)]="initialMonth"
|
||||||
|
[value]="month"
|
||||||
|
[placeholder]="monthPlaceholder"
|
||||||
|
[disabled]="!year || model.disabled"
|
||||||
|
(change)="onChange($event)"
|
||||||
|
></ds-number-picker>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mr-3">
|
||||||
|
<ds-number-picker
|
||||||
|
[min]="minDay"
|
||||||
|
[max]="maxDay"
|
||||||
|
[name]="'day'"
|
||||||
|
[size]="2"
|
||||||
|
[(ngModel)]="initialDay"
|
||||||
|
[value]="day"
|
||||||
|
[placeholder]="dayPlaceholder"
|
||||||
|
[disabled]="!month || model.disabled"
|
||||||
|
(change)="onChange($event)"
|
||||||
|
></ds-number-picker>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clearfix"></div>
|
@@ -0,0 +1,3 @@
|
|||||||
|
.col-lg-1 {
|
||||||
|
width: auto;
|
||||||
|
}
|
@@ -0,0 +1,170 @@
|
|||||||
|
import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core';
|
||||||
|
import { FormGroup } from '@angular/forms';
|
||||||
|
import { DynamicDsDatePickerModel } from './ds-date-picker.model';
|
||||||
|
|
||||||
|
export const DS_DATE_PICKER_SEPARATOR = '-';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-date-picker',
|
||||||
|
styleUrls: ['./ds-date-picker.component.scss'],
|
||||||
|
templateUrl: './ds-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()
|
||||||
|
change = new EventEmitter<any>();
|
||||||
|
|
||||||
|
initialYear: number;
|
||||||
|
initialMonth: number;
|
||||||
|
initialDay: number;
|
||||||
|
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
day: number;
|
||||||
|
|
||||||
|
minYear: 0;
|
||||||
|
maxYear: number;
|
||||||
|
minMonth = 1;
|
||||||
|
maxMonth = 12;
|
||||||
|
minDay = 1;
|
||||||
|
maxDay = 31;
|
||||||
|
|
||||||
|
yearPlaceholder = 'year';
|
||||||
|
monthPlaceholder = 'month';
|
||||||
|
dayPlaceholder = 'day';
|
||||||
|
|
||||||
|
disabledMonth = true;
|
||||||
|
disabledDay = true;
|
||||||
|
invalid = false;
|
||||||
|
|
||||||
|
ngOnInit() {// TODO Manage fields when not setted
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Invalid state for year
|
||||||
|
this.group.get(this.model.id).statusChanges.subscribe((state) => {
|
||||||
|
if (state === 'INVALID' || this.model.malformedDate) {
|
||||||
|
this.invalid = true;
|
||||||
|
} else {
|
||||||
|
this.invalid = false;
|
||||||
|
this.model.malformedDate = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'month': {
|
||||||
|
if (event.value !== null) {
|
||||||
|
this.month = event.value;
|
||||||
|
} else {
|
||||||
|
this.month = undefined;
|
||||||
|
this.day = undefined;
|
||||||
|
}
|
||||||
|
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 (!this.model.value && event.field === 'year') {
|
||||||
|
this.disabledMonth = false;
|
||||||
|
} else if (this.disabledDay && event.field === 'month') {
|
||||||
|
this.disabledDay = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update value
|
||||||
|
let value = null;
|
||||||
|
if (this.year) {
|
||||||
|
let yyyy = this.year.toString();
|
||||||
|
while (yyyy.length < 4) {
|
||||||
|
yyyy = '0' + yyyy;
|
||||||
|
}
|
||||||
|
value = yyyy;
|
||||||
|
}
|
||||||
|
if (this.month) {
|
||||||
|
const mm = this.month.toString().length === 1
|
||||||
|
? '0' + this.month.toString()
|
||||||
|
: this.month.toString();
|
||||||
|
value += DS_DATE_PICKER_SEPARATOR + mm;
|
||||||
|
}
|
||||||
|
if (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(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 = 'DSDATEPICKER';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,64 @@
|
|||||||
|
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 COMBOBOX_GROUP_SUFFIX = '_COMBO_GROUP';
|
||||||
|
export const COMBOBOX_METADATA_SUFFIX = '_COMBO_METADATA';
|
||||||
|
export const COMBOBOX_VALUE_SUFFIX = '_COMBO_VALUE';
|
||||||
|
|
||||||
|
export interface DsDynamicComboboxModelConfig extends DynamicFormGroupModelConfig {
|
||||||
|
languageCodes: LanguageCode[];
|
||||||
|
language: string;
|
||||||
|
readOnly: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DynamicComboboxModel extends DynamicFormGroupModel {
|
||||||
|
@serializable() private _language: string;
|
||||||
|
@serializable() private _languageCodes: LanguageCode[];
|
||||||
|
@serializable() languageUpdates: Subject<string>;
|
||||||
|
@serializable() hasLanguages = false;
|
||||||
|
@serializable() readOnly: boolean;
|
||||||
|
|
||||||
|
constructor(config: DsDynamicComboboxModelConfig, 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,50 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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 firstValue + this.separator + secondValue;
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set value(value: string | AuthorityValueModel) {
|
||||||
|
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,65 @@
|
|||||||
|
<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': !invalid, 'border border-danger': invalid}">
|
||||||
|
<div *ngIf="!(formCollapsed | async)" class="pl-2 row" @shrinkInOut>
|
||||||
|
<ds-form #formRef="formComponent"
|
||||||
|
class="col-sm-9 pl-0"
|
||||||
|
[formId]="formId"
|
||||||
|
[formModel]="formModel"
|
||||||
|
[displaySubmit]="false"></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" readonly class="border-0 form-control-plaintext tag-input mt-1 mb-1 pl-2 text-muted" 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,224 @@
|
|||||||
|
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 { DynamicLookupModel } from '../lookup/dynamic-lookup.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';
|
||||||
|
|
||||||
|
@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;
|
||||||
|
|
||||||
|
@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;
|
||||||
|
public invalid = 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() {
|
||||||
|
console.log(this.model.hasErrorMessages);
|
||||||
|
const config = {rows: this.model.formConfiguration} as SubmissionFormsModel;
|
||||||
|
if (isNotEmpty(this.model.value)) {
|
||||||
|
this.formCollapsed = Observable.of(true);
|
||||||
|
}
|
||||||
|
this.formId = this.formService.getUniqueId(this.model.id);
|
||||||
|
this.formModel = this.formBuilderService.modelFromConfiguration(config, this.model.scopeUUID, {});
|
||||||
|
this.chips = new Chips(this.model.value, '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 (isEmpty(items)) {
|
||||||
|
// 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({});
|
||||||
|
Object.keys(this.model.value[0])
|
||||||
|
.forEach((key) => {
|
||||||
|
emptyItem[key] = null;
|
||||||
|
});
|
||||||
|
items.push(emptyItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.model.valueUpdates.next(items);
|
||||||
|
this.change.emit();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
// Invalid state for year
|
||||||
|
this.group.get(this.model.id).statusChanges.subscribe((state) => {
|
||||||
|
if (state === 'INVALID') {
|
||||||
|
this.invalid = true;
|
||||||
|
} else {
|
||||||
|
this.invalid = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ? null : this.selectedChipItem.item[model.name];
|
||||||
|
if (model instanceof DynamicLookupModel) {
|
||||||
|
(model as DynamicLookupModel).valueUpdates.next(value);
|
||||||
|
} else if (model instanceof DynamicInputModel) {
|
||||||
|
model.valueUpdates.next(value);
|
||||||
|
} else {
|
||||||
|
(model as any).value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.editMode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
// this.change.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
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,41 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
export const DYNAMIC_FORM_CONTROL_TYPE_RELATION = 'RELATION';
|
||||||
|
export const PLACEHOLDER_PARENT_METADATA = '#PLACEHOLDER_PARENT_METADATA_VALUE#';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic Group Model configuration interface
|
||||||
|
*/
|
||||||
|
export interface DynamicGroupModelConfig extends DsDynamicInputModelConfig {
|
||||||
|
formConfiguration: FormRowModel[],
|
||||||
|
mandatoryField: string,
|
||||||
|
name: string,
|
||||||
|
relationFields: string[],
|
||||||
|
scopeUUID: string,
|
||||||
|
value?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic Group Model class
|
||||||
|
*/
|
||||||
|
export class DynamicGroupModel extends DsDynamicInputModel {
|
||||||
|
@serializable() formConfiguration: FormRowModel[];
|
||||||
|
@serializable() mandatoryField: string;
|
||||||
|
@serializable() relationFields: string[];
|
||||||
|
@serializable() scopeUUID: string;
|
||||||
|
@serializable() value: any[];
|
||||||
|
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_RELATION;
|
||||||
|
|
||||||
|
constructor(config: DynamicGroupModelConfig, layout?: DynamicFormControlLayout) {
|
||||||
|
super(config, layout);
|
||||||
|
|
||||||
|
this.formConfiguration = config.formConfiguration;
|
||||||
|
this.mandatoryField = config.mandatoryField;
|
||||||
|
this.relationFields = config.relationFields;
|
||||||
|
this.scopeUUID = config.scopeUUID;
|
||||||
|
const value = config.value || [];
|
||||||
|
this.valueUpdates.next(value)
|
||||||
|
}
|
||||||
|
}
|
@@ -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,18 @@
|
|||||||
|
import {
|
||||||
|
DynamicFormArrayModel, DynamicFormArrayModelConfig, DynamicFormControlLayout,
|
||||||
|
serializable
|
||||||
|
} from '@ng-dynamic-forms/core';
|
||||||
|
|
||||||
|
export interface DynamicRowArrayModelConfig extends DynamicFormArrayModelConfig {
|
||||||
|
notRepeteable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DynamicRowArrayModel extends DynamicFormArrayModel {
|
||||||
|
@serializable() notRepeteable = false;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,30 @@
|
|||||||
|
import {
|
||||||
|
DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA,
|
||||||
|
DynamicFormControlLayout, DynamicTextAreaModel, DynamicTextAreaModelConfig,
|
||||||
|
serializable
|
||||||
|
} from '@ng-dynamic-forms/core';
|
||||||
|
import { Subject } from 'rxjs/Subject';
|
||||||
|
import { LanguageCode } from '../../models/form-field-language-value.model';
|
||||||
|
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,59 @@
|
|||||||
|
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[];
|
||||||
|
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,35 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
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)="onBlurEvent($event)"
|
||||||
|
(change)="onChangeEvent($event)"
|
||||||
|
(focus)="onFocusEvent($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)="onChangeEvent($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)="onBlurEvent($event)"
|
||||||
|
(focus)="onFocusEvent($event)"/>
|
||||||
|
<span [ngClass]="model.layout.element?.label" [innerHTML]="item.label"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -0,0 +1,137 @@
|
|||||||
|
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 { ConfigData } from '../../../../../../core/config/config-data';
|
||||||
|
import { ConfigAuthorityModel } from '../../../../../../core/shared/config/config-authority.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';
|
||||||
|
|
||||||
|
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'
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO Fare questo componente da zero
|
||||||
|
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 authorityList: 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlurEvent(event: Event) {
|
||||||
|
this.blur.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocusEvent(event: Event) {
|
||||||
|
this.focus.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeEvent(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.authorityList[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.authorityList[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: ConfigData) => {
|
||||||
|
let groupCounter = 0;
|
||||||
|
let itemsPerGroup = 0;
|
||||||
|
let tempList: ListItem[] = [];
|
||||||
|
this.authorityList = authorities.payload as ConfigAuthorityModel[];
|
||||||
|
// Make a list of available options (checkbox/radio) and split in groups of 'model.groupLength'
|
||||||
|
(authorities.payload as ConfigAuthorityModel[]).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,130 @@
|
|||||||
|
<div #sdRef="ngbDropdown"
|
||||||
|
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); sdRef.close();"
|
||||||
|
(focus)="onFocusEvent($event); sdRef.close();"
|
||||||
|
(click)="$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
|
||||||
|
*ngIf="!isInputDisabled()" class="btn btn-secondary"
|
||||||
|
type="button"
|
||||||
|
[disabled]="model.readOnly || isSearchDisabled()"
|
||||||
|
(click)="search($event); sdRef.open();">{{'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); sdRef.close();"
|
||||||
|
(focus)="onFocusEvent($event); sdRef.close();"
|
||||||
|
(click)="$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]="name2"
|
||||||
|
[type]="model.inputType"
|
||||||
|
[(ngModel)]="secondInputValue"
|
||||||
|
[disabled]="firstInputValue.length === 0 || isInputDisabled()"
|
||||||
|
[placeholder]="model.placeholder2 | translate"
|
||||||
|
[readonly]="model.readOnly"
|
||||||
|
(change)="$event.preventDefault()"
|
||||||
|
(blur)="onBlurEvent($event); sdRef.close();"
|
||||||
|
(focus)="onFocusEvent($event); 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
|
||||||
|
*ngIf="!isInputDisabled()" class="btn btn-secondary"
|
||||||
|
type="button"
|
||||||
|
[disabled]="isSearchDisabled()"
|
||||||
|
(click)="search($event); sdRef.open();">{{'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)="clearFields(); sdRef.close();">{{'form.no-results' | translate}}
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item collection-item"
|
||||||
|
*ngFor="let listEntry of optionsList"
|
||||||
|
(click)="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,68 @@
|
|||||||
|
@import "../../../../../../../styles/variables";
|
||||||
|
|
||||||
|
/* 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: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-addon input {
|
||||||
|
padding-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host /deep/ .dropdown-menu {
|
||||||
|
width: 100% !important;
|
||||||
|
max-height: 200px;
|
||||||
|
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: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
overflow: visible;
|
||||||
|
//padding-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
//@media (min-width: $screen-sm-min) {
|
||||||
|
// .firstName {
|
||||||
|
// padding-left: 0 !important;
|
||||||
|
// margin-left: -5px !important;
|
||||||
|
// }
|
||||||
|
//}
|
@@ -0,0 +1,220 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
@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;
|
||||||
|
@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;
|
||||||
|
public isLookupName: boolean;
|
||||||
|
public name2: string;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Switch Lookup/LookupName
|
||||||
|
if (this.model.separator) {
|
||||||
|
this.isLookupName = true;
|
||||||
|
this.name2 = this.model.name + '2';
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,37 @@
|
|||||||
|
import { AUTOCOMPLETE_OFF, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
|
||||||
|
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
|
||||||
|
import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model';
|
||||||
|
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
|
||||||
|
|
||||||
|
export const DYNAMIC_FORM_CONTROL_TYPE_LOOKUP = 'LOOKUP';
|
||||||
|
|
||||||
|
export interface DynamicLookupModelConfig extends DsDynamicInputModelConfig {
|
||||||
|
maxOptions: number;
|
||||||
|
value: any;
|
||||||
|
separator: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DynamicLookupModel extends DsDynamicInputModel {
|
||||||
|
|
||||||
|
@serializable() maxOptions: number;
|
||||||
|
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_LOOKUP;
|
||||||
|
@serializable() value: any;
|
||||||
|
@serializable() separator: string; // Defined only for lookup-name
|
||||||
|
|
||||||
|
@serializable() placeholder: string;
|
||||||
|
@serializable() placeholder2: string;
|
||||||
|
|
||||||
|
constructor(config: DynamicLookupModelConfig, layout?: DynamicFormControlLayout) {
|
||||||
|
|
||||||
|
super(config, layout);
|
||||||
|
|
||||||
|
this.autoComplete = AUTOCOMPLETE_OFF;
|
||||||
|
this.maxOptions = config.maxOptions;
|
||||||
|
this.separator = config.separator; // Defined only for lookup-name
|
||||||
|
|
||||||
|
this.valueUpdates.next(config.value);
|
||||||
|
// this.valueUpdates.subscribe(() => {
|
||||||
|
// this.setInputsValue();
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
}
|
@@ -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)="onBlurEvent($event)"
|
||||||
|
(click)="$event.stopPropagation(); openDropdown(sdRef);"
|
||||||
|
(focus)="onFocusEvent($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">No results found</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>Loading...</p></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
@@ -0,0 +1,26 @@
|
|||||||
|
@import '../../../../form.component';
|
||||||
|
|
||||||
|
.scrollable-menu {
|
||||||
|
height: auto;
|
||||||
|
max-height: 200px;
|
||||||
|
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: 2rem !important;
|
||||||
|
line-height: 2rem;
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable-dropdown-menu {
|
||||||
|
left: 0 !important;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
@@ -0,0 +1,94 @@
|
|||||||
|
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();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlurEvent(event: Event) {
|
||||||
|
this.blur.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocusEvent(event) {
|
||||||
|
this.focus.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelect(event) {
|
||||||
|
this.group.markAsDirty();
|
||||||
|
// (this.model as DynamicScrollableDropdownModel).parent as
|
||||||
|
// this.group.get(this.model.id).setValue(event);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -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 mt-1 mb-1 chips-sort-ignore"
|
||||||
|
type="text"
|
||||||
|
[class.pl-3]="chips.hasItems()"
|
||||||
|
[placeholder]="model.label"
|
||||||
|
[readonly]="model.readOnly"
|
||||||
|
[(ngModel)]="currentValue"
|
||||||
|
(blur)="onBlurEvent($event)"
|
||||||
|
(keypress)="preventEventsPropagation($event)"
|
||||||
|
(keydown)="preventEventsPropagation($event)"
|
||||||
|
(keyup)="onKeyUp($event)" />
|
||||||
|
|
||||||
|
|
||||||
|
<input *ngIf="searchOptions"
|
||||||
|
class="border-0 form-control-plaintext tag-input 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.label"
|
||||||
|
[readonly]="model.readOnly"
|
||||||
|
[resultTemplate]="rt"
|
||||||
|
[type]="model.inputType"
|
||||||
|
(blur)="onBlurEvent($event)"
|
||||||
|
(focus)="onFocusEvent($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,34 @@
|
|||||||
|
@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: 200px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
left: 0 !important;
|
||||||
|
margin-top: 15px !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 {
|
||||||
|
flex-grow: 1;
|
||||||
|
outline: none;
|
||||||
|
width: auto !important;
|
||||||
|
}
|
@@ -0,0 +1,194 @@
|
|||||||
|
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, isEmpty, 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlurEvent(event: Event) {
|
||||||
|
if (isNotEmpty(this.currentValue)) {
|
||||||
|
this.addTagsToChips();
|
||||||
|
}
|
||||||
|
this.blur.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocusEvent($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) {
|
||||||
|
// Key: Enter or , or ;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeChips(event) {
|
||||||
|
// console.log("Removed chips index: "+event);
|
||||||
|
this.model.valueUpdates.next(this.chips.getChipsItems());
|
||||||
|
this.change.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeChips(event) {
|
||||||
|
this.model.valueUpdates.next(this.chips.getChipsItems());
|
||||||
|
this.change.emit(event);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,143 @@
|
|||||||
|
// import { AUTOCOMPLETE_OFF, DYNAMIC_FORM_CONTROL_INPUT_TYPE_TEXT } from '@ng-dynamic-forms/core';
|
||||||
|
// import { Observable } from 'rxjs/Observable';
|
||||||
|
//
|
||||||
|
// import {
|
||||||
|
// DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD, DynamicTypeaheadModel,
|
||||||
|
// DynamicTypeaheadResponseModel
|
||||||
|
// } from './dynamic-tag.model';
|
||||||
|
// import { PageInfo } from '../../../../../../core/shared/page-info.model';
|
||||||
|
//
|
||||||
|
// describe('DynamicTypeaheadModel test suite', () => {
|
||||||
|
//
|
||||||
|
// let model: any;
|
||||||
|
// const search = (text: string): Observable<DynamicTypeaheadResponseModel> =>
|
||||||
|
// Observable.of({
|
||||||
|
// list: ['One', 'Two', 'Three'],
|
||||||
|
// pageInfo: new PageInfo()
|
||||||
|
// });
|
||||||
|
// const config = {
|
||||||
|
// id: 'input',
|
||||||
|
// minChars: 3,
|
||||||
|
// search: search
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// beforeEach(() => model = new DynamicTypeaheadModel(config));
|
||||||
|
//
|
||||||
|
// it('tests if correct default type property is set', () => {
|
||||||
|
//
|
||||||
|
// expect(model.type).toEqual(DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('tests if correct default input type property is set', () => {
|
||||||
|
//
|
||||||
|
// expect(model.inputType).toEqual(DYNAMIC_FORM_CONTROL_INPUT_TYPE_TEXT);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('tests if correct default autoComplete property is set', () => {
|
||||||
|
//
|
||||||
|
// expect(model.autoComplete).toEqual(AUTOCOMPLETE_OFF);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('tests if correct default autoFocus property is set', () => {
|
||||||
|
//
|
||||||
|
// expect(model.autoFocus).toBe(false);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('tests if correct default cls properties aree set', () => {
|
||||||
|
//
|
||||||
|
// expect(model.cls).toBeDefined();
|
||||||
|
// expect(model.cls.element.container).toEqual('');
|
||||||
|
// expect(model.cls.element.control).toEqual('');
|
||||||
|
// expect(model.cls.element.errors).toEqual('');
|
||||||
|
// expect(model.cls.element.label).toEqual('');
|
||||||
|
// expect(model.cls.grid.container).toEqual('');
|
||||||
|
// expect(model.cls.grid.control).toEqual('');
|
||||||
|
// expect(model.cls.grid.errors).toEqual('');
|
||||||
|
// expect(model.cls.grid.label).toEqual('');
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('tests if correct default hint property is set', () => {
|
||||||
|
//
|
||||||
|
// expect(model.hint).toBeNull();
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('tests if correct default label property is set', () => {
|
||||||
|
//
|
||||||
|
// expect(model.label).toBeNull();
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('tests if correct default max property is set', () => {
|
||||||
|
//
|
||||||
|
// expect(model.max).toBeNull();
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('tests if correct default maxLength property is set', () => {
|
||||||
|
//
|
||||||
|
// expect(model.maxLength).toBeNull();
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('tests if correct default minLength property is set', () => {
|
||||||
|
//
|
||||||
|
// expect(model.minLength).toBeNull();
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('tests if correct minChars property is set', () => {
|
||||||
|
//
|
||||||
|
// expect(model.minChars).toEqual(3);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('tests if correct default min property is set', () => {
|
||||||
|
//
|
||||||
|
// expect(model.min).toBeNull();
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('tests if correct default placeholder property is set', () => {
|
||||||
|
//
|
||||||
|
// expect(model.placeholder).toEqual('');
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('tests if correct default readonly property is set', () => {
|
||||||
|
//
|
||||||
|
// expect(model.readOnly).toBe(false);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('tests if correct default required property is set', () => {
|
||||||
|
//
|
||||||
|
// expect(model.required).toBe(false);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('tests if correct default spellcheck property is set', () => {
|
||||||
|
//
|
||||||
|
// expect(model.spellCheck).toBe(false);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('tests if correct default step property is set', () => {
|
||||||
|
//
|
||||||
|
// expect(model.step).toBeNull();
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('tests if correct default prefix property is set', () => {
|
||||||
|
//
|
||||||
|
// expect(model.prefix).toBeNull();
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('tests if correct default suffix property is set', () => {
|
||||||
|
//
|
||||||
|
// expect(model.suffix).toBeNull();
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('tests if correct search function is set', () => {
|
||||||
|
//
|
||||||
|
// expect(model.search).toBe(search);
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// it('should serialize correctly', () => {
|
||||||
|
//
|
||||||
|
// const json = JSON.parse(JSON.stringify(model));
|
||||||
|
//
|
||||||
|
// expect(json.id).toEqual(model.id);
|
||||||
|
// expect(json.disabled).toEqual(model.disabled);
|
||||||
|
// expect(json.value).toBe(model.value);
|
||||||
|
// expect(json.type).toEqual(DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD);
|
||||||
|
// });
|
||||||
|
// });
|
@@ -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 = 'TYPETAG';
|
||||||
|
|
||||||
|
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;
|
||||||
|
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.label"
|
||||||
|
[readonly]="model.readOnly"
|
||||||
|
[resultTemplate]="rt"
|
||||||
|
[type]="model.inputType"
|
||||||
|
[(ngModel)]="currentValue"
|
||||||
|
(blur)="onBlurEvent($event)"
|
||||||
|
(focus)="onFocusEvent($event)"
|
||||||
|
(change)="onChangeEvent($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: 200px;
|
||||||
|
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,121 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
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.currentValue = valueObj;
|
||||||
|
this.model.valueUpdates.next(valueObj as any);
|
||||||
|
this.change.emit(valueObj);
|
||||||
|
}
|
||||||
|
if (event.data) {
|
||||||
|
// this.group.markAsDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlurEvent(event: Event) {
|
||||||
|
this.blur.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeEvent(event: Event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (isEmpty(this.currentValue)) {
|
||||||
|
this.model.valueUpdates.next(null);
|
||||||
|
this.change.emit(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocusEvent(event) {
|
||||||
|
this.focus.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectItem(event: NgbTypeaheadSelectItemEvent) {
|
||||||
|
this.currentValue = event.item;
|
||||||
|
this.model.valueUpdates.next(event.item);
|
||||||
|
this.change.emit(event.item);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,25 @@
|
|||||||
|
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_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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
212
src/app/shared/form/builder/form-builder.service.ts
Normal file
212
src/app/shared/form/builder/form-builder.service.ts
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { FormGroup } from '@angular/forms';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DynamicFormArrayGroupModel,
|
||||||
|
DynamicFormArrayModel,
|
||||||
|
DynamicFormControlModel,
|
||||||
|
DynamicFormGroupModel,
|
||||||
|
DynamicFormService,
|
||||||
|
DynamicPathable,
|
||||||
|
JSONUtils,
|
||||||
|
} from '@ng-dynamic-forms/core';
|
||||||
|
import { mergeWith } from 'lodash';
|
||||||
|
|
||||||
|
import { isEmpty, isNotEmpty, isNotNull, isNull } from '../../empty.util';
|
||||||
|
import { DynamicComboboxModel } from './ds-dynamic-form-ui/models/ds-dynamic-combobox.model';
|
||||||
|
import { SubmissionFormsModel } from '../../../core/shared/config/config-submission-forms.model';
|
||||||
|
import { DynamicConcatModel } from './ds-dynamic-form-ui/models/ds-dynamic-concat.model';
|
||||||
|
import { DynamicListCheckboxGroupModel } from './ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model';
|
||||||
|
import { DynamicGroupModel } from './ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.model';
|
||||||
|
import { DynamicTagModel } from './ds-dynamic-form-ui/models/tag/dynamic-tag.model';
|
||||||
|
import { DynamicListRadioGroupModel } from './ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model';
|
||||||
|
import { RowParser } from './parsers/row-parser';
|
||||||
|
|
||||||
|
import { DynamicRowArrayModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-array-model';
|
||||||
|
import { DynamicRowGroupModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-group-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[]): void => {
|
||||||
|
|
||||||
|
for (const controlModel of findGroupModel) {
|
||||||
|
|
||||||
|
if (controlModel.id === findId) {
|
||||||
|
if (controlModel instanceof DynamicFormArrayModel && isNotNull(arrayIndex)) {
|
||||||
|
result = controlModel.get(arrayIndex);
|
||||||
|
} else {
|
||||||
|
result = controlModel;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controlModel instanceof DynamicFormGroupModel) {
|
||||||
|
findByIdFn(findId, (controlModel as DynamicFormGroupModel).group);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controlModel instanceof DynamicFormArrayModel && (isNull(arrayIndex) || controlModel.size > (arrayIndex))) {
|
||||||
|
arrayIndex = (isNull(arrayIndex)) ? 0 : arrayIndex;
|
||||||
|
findByIdFn(findId, controlModel.get(arrayIndex).group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
findByIdFn(id, groupModel);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAllModelsValue(groupModel: DynamicFormControlModel[]): void {
|
||||||
|
|
||||||
|
const iterateControlModels = (findGroupModel: DynamicFormControlModel[]): void => {
|
||||||
|
|
||||||
|
for (const controlModel of findGroupModel) {
|
||||||
|
|
||||||
|
if (controlModel instanceof DynamicFormGroupModel) {
|
||||||
|
iterateControlModels((controlModel as DynamicFormGroupModel).group);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controlModel instanceof DynamicFormArrayModel) {
|
||||||
|
iterateControlModels(controlModel.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 iterateControlModels = (findGroupModel: DynamicFormControlModel[]): void => {
|
||||||
|
let iterateResult = Object.create({});
|
||||||
|
|
||||||
|
// Iterate over all group's controls
|
||||||
|
for (const controlModel of findGroupModel) {
|
||||||
|
|
||||||
|
if (controlModel instanceof DynamicRowGroupModel && !this.isCustomGroup(controlModel)) {
|
||||||
|
iterateResult = mergeWith(iterateResult, iterateControlModels((controlModel as DynamicFormGroupModel).group), customizer);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controlModel instanceof DynamicFormGroupModel && !this.isCustomGroup(controlModel)) {
|
||||||
|
iterateResult[controlModel.name] = iterateControlModels((controlModel as DynamicFormGroupModel).group);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controlModel instanceof DynamicRowArrayModel) {
|
||||||
|
for (const arrayItemModel of controlModel.groups) {
|
||||||
|
iterateResult = mergeWith(iterateResult, iterateControlModels(arrayItemModel.group), customizer);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controlModel instanceof DynamicFormArrayModel) {
|
||||||
|
iterateResult[controlModel.name] = [];
|
||||||
|
for (const arrayItemModel of controlModel.groups) {
|
||||||
|
iterateResult[controlModel.name].push(iterateControlModels(arrayItemModel.group));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let controlId;
|
||||||
|
// Get the field's name
|
||||||
|
if (controlModel instanceof DynamicComboboxModel) {
|
||||||
|
// If is instance of DynamicComboboxModel take the qualdrop id as field's name
|
||||||
|
controlId = controlModel.qualdropId;
|
||||||
|
} else {
|
||||||
|
controlId = controlModel.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const controlValue = (controlModel as any).value || null;
|
||||||
|
if (controlId && iterateResult.hasOwnProperty(controlId) && isNotNull(iterateResult[controlId])) {
|
||||||
|
iterateResult[controlId].push(controlValue);
|
||||||
|
} else {
|
||||||
|
iterateResult[controlId] = isNotEmpty(controlValue) ? (Array.isArray(controlValue) ? controlValue : [controlValue]) : 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) {
|
||||||
|
return model.parent &&
|
||||||
|
(model.parent instanceof DynamicConcatModel
|
||||||
|
|| model.parent instanceof DynamicComboboxModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMappedGroupValue(model: DynamicFormControlModel) {
|
||||||
|
return ((model.parent && model.parent instanceof DynamicComboboxModel)
|
||||||
|
|| model.parent instanceof DynamicGroupModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
isCustomGroup(model: DynamicFormControlModel) {
|
||||||
|
return model &&
|
||||||
|
(model instanceof DynamicConcatModel
|
||||||
|
|| model instanceof DynamicComboboxModel
|
||||||
|
|| model instanceof DynamicListCheckboxGroupModel
|
||||||
|
|| model instanceof DynamicListRadioGroupModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
isModelInAuthorityGroup(model: DynamicFormControlModel) {
|
||||||
|
return (model instanceof DynamicListCheckboxGroupModel || model instanceof DynamicTagModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFormControlById(id: string, formGroup: FormGroup, groupModel: DynamicFormControlModel[], index = 0) {
|
||||||
|
const fieldModel = this.findById(id, groupModel, index);
|
||||||
|
return isNotEmpty(fieldModel) ? formGroup.get(this.getPath(fieldModel)) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId(model: DynamicPathable) {
|
||||||
|
if (model instanceof DynamicFormArrayGroupModel) {
|
||||||
|
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,42 @@
|
|||||||
|
import { isNotEmpty } from '../../../empty.util';
|
||||||
|
|
||||||
|
export class FormFieldMetadataValueObject {
|
||||||
|
metadata?: string;
|
||||||
|
value: string;
|
||||||
|
display: string;
|
||||||
|
language: any;
|
||||||
|
authority: string;
|
||||||
|
confidence: number;
|
||||||
|
place: number;
|
||||||
|
closed: boolean;
|
||||||
|
label: string;
|
||||||
|
|
||||||
|
constructor(value: string,
|
||||||
|
language: any = null,
|
||||||
|
authority: string = null,
|
||||||
|
display: string = null,
|
||||||
|
confidence: number = -1,
|
||||||
|
place: number = -1,
|
||||||
|
metadata: string = null) {
|
||||||
|
this.value = value;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@@ -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
|
||||||
|
}
|
42
src/app/shared/form/builder/models/form-field.model.ts
Normal file
42
src/app/shared/form/builder/models/form-field.model.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
selectableMetadata: FormFieldMetadataValueObject[];
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
rows: FormRowModel[];
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
scope: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
value: any;
|
||||||
|
}
|
99
src/app/shared/form/builder/parsers/concat-field-parser.ts
Normal file
99
src/app/shared/form/builder/parsers/concat-field-parser.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
export class ConcatFieldParser extends FieldParser {
|
||||||
|
|
||||||
|
constructor(protected configData: FormFieldModel,
|
||||||
|
protected initFormValues,
|
||||||
|
protected readOnly: boolean,
|
||||||
|
private separator: string,
|
||||||
|
protected firstPlaceholder: string = null,
|
||||||
|
protected secondPlaceholder: string = null) {
|
||||||
|
super(configData, initFormValues, readOnly);
|
||||||
|
|
||||||
|
this.separator = separator;
|
||||||
|
this.firstPlaceholder = firstPlaceholder;
|
||||||
|
this.secondPlaceholder = secondPlaceholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public modelFactory(fieldValue: FormFieldMetadataValueObject | any): any {
|
||||||
|
|
||||||
|
let clsGroup: DynamicFormControlLayout;
|
||||||
|
let clsInput: DynamicFormControlLayout;
|
||||||
|
const newId = this.configData.selectableMetadata[0].metadata
|
||||||
|
.split('.')
|
||||||
|
.slice(0, this.configData.selectableMetadata[0].metadata.split('.').length - 1)
|
||||||
|
.join('.');
|
||||||
|
|
||||||
|
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, true, false, false);
|
||||||
|
const input2ModelConfig: DynamicInputModelConfig = this.initModel(newId + CONCAT_SECOND_INPUT_SUFFIX, true, 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.split(this.separator);
|
||||||
|
|
||||||
|
if (values.length > 1) {
|
||||||
|
input1ModelConfig.value = values[0];
|
||||||
|
input2ModelConfig.value = values[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()[0];
|
||||||
|
|
||||||
|
return concatModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
49
src/app/shared/form/builder/parsers/date-field-parser.ts
Normal file
49
src/app/shared/form/builder/parsers/date-field-parser.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { FieldParser } from './field-parser';
|
||||||
|
import { DynamicDatePickerModelConfig } from '@ng-dynamic-forms/core';
|
||||||
|
import { FormFieldModel } from '../models/form-field.model';
|
||||||
|
import { DynamicDsDatePickerModel } from '../ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.model';
|
||||||
|
import { isNotEmpty } from '../../../empty.util';
|
||||||
|
import { DS_DATE_PICKER_SEPARATOR } from '../ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component';
|
||||||
|
|
||||||
|
export class DateFieldParser extends FieldParser {
|
||||||
|
|
||||||
|
public modelFactory(): any {
|
||||||
|
const inputDateModelConfig: DynamicDatePickerModelConfig = this.initModel();
|
||||||
|
|
||||||
|
inputDateModelConfig.toggleIcon = 'fa fa-calendar';
|
||||||
|
|
||||||
|
const dateModel = new DynamicDsDatePickerModel(inputDateModelConfig);
|
||||||
|
|
||||||
|
// Init Data and validity check
|
||||||
|
if (isNotEmpty(this.getInitFieldValue())) {
|
||||||
|
let malformedData = false;
|
||||||
|
const value = this.getInitFieldValue().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) {
|
||||||
|
malformedData = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!malformedData) {
|
||||||
|
dateModel.valueUpdates.next(this.getInitFieldValue());
|
||||||
|
} else {
|
||||||
|
// TODO Set error message
|
||||||
|
dateModel.malformedDate = true;
|
||||||
|
// TODO
|
||||||
|
// const errorMessage = 'The stored date is not compliant';
|
||||||
|
// dateModel.validators = Object.assign({}, dateModel.validators, {malformedDate: null});
|
||||||
|
// dateModel.errorMessages = Object.assign({}, dateModel.errorMessages, {malformedDate: errorMessage});
|
||||||
|
|
||||||
|
// this.formService.addErrorToField(this.group.get(this.model.id), this.model, errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return dateModel;
|
||||||
|
}
|
||||||
|
}
|
44
src/app/shared/form/builder/parsers/dropdown-field-parser.ts
Normal file
44
src/app/shared/form/builder/parsers/dropdown-field-parser.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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 { FormFieldModel } from '../models/form-field.model';
|
||||||
|
import { isNotEmpty } from '../../../empty.util';
|
||||||
|
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||||
|
|
||||||
|
export class DropdownFieldParser extends FieldParser {
|
||||||
|
|
||||||
|
constructor(protected configData: FormFieldModel,
|
||||||
|
protected initFormValues,
|
||||||
|
protected readOnly: boolean,
|
||||||
|
protected authorityUuid: string) {
|
||||||
|
super(configData, initFormValues, readOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
public modelFactory(fieldValue: FormFieldMetadataValueObject): any {
|
||||||
|
const dropdownModelConfig: DynamicScrollableDropdownModelConfig = this.initModel();
|
||||||
|
let layout: DynamicFormControlLayout;
|
||||||
|
|
||||||
|
if (isNotEmpty(this.configData.selectableMetadata[0].authority)) {
|
||||||
|
this.setAuthorityOptions(dropdownModelConfig, this.authorityUuid);
|
||||||
|
dropdownModelConfig.maxOptions = 10;
|
||||||
|
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.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
272
src/app/shared/form/builder/parsers/field-parser.ts
Normal file
272
src/app/shared/form/builder/parsers/field-parser.ts
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
export abstract class FieldParser {
|
||||||
|
|
||||||
|
protected fieldId: string;
|
||||||
|
|
||||||
|
constructor(protected configData: FormFieldModel, protected initFormValues, protected readOnly: boolean) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract modelFactory(fieldValue?: FormFieldMetadataValueObject): 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',
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
setLayout(model, 'element', 'host', 'col');
|
||||||
|
if (model.hasLanguages) {
|
||||||
|
setLayout(model, 'grid', 'control', 'col');
|
||||||
|
}
|
||||||
|
return [model];
|
||||||
|
}
|
||||||
|
} as DynamicRowArrayModelConfig;
|
||||||
|
|
||||||
|
const layout: DynamicFormControlLayout = {
|
||||||
|
grid: {
|
||||||
|
group: 'dsgridgroup 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.getFieldId();
|
||||||
|
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.getFieldId();
|
||||||
|
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.getFieldId();
|
||||||
|
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.create({});
|
||||||
|
valueObj.metadata = id;
|
||||||
|
valueObj.value = this.initFormValues[id][innerIndex];
|
||||||
|
values.push(valueObj);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return values[outerIndex];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getInitArrayIndex() {
|
||||||
|
const fieldIds: any = this.getFieldId();
|
||||||
|
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[] {
|
||||||
|
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()[0];
|
||||||
|
|
||||||
|
// 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.readOnly;
|
||||||
|
controlModel.disabled = this.readOnly;
|
||||||
|
|
||||||
|
if (label) {
|
||||||
|
controlModel.label = (labelEmpty) ? ' ' : this.configData.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
controlModel.placeholder = this.configData.label;
|
||||||
|
|
||||||
|
if (this.configData.mandatory && setErrors) {
|
||||||
|
this.setErrors(controlModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Available Languages
|
||||||
|
if (this.configData.languageCodes && this.configData.languageCodes.length > 0) {
|
||||||
|
(controlModel as DsDynamicInputModel).languageCodes = this.configData.languageCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return controlModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setErrors(controlModel) {
|
||||||
|
controlModel.required = true;
|
||||||
|
controlModel.validators = Object.assign({}, controlModel.validators, {required: null});
|
||||||
|
controlModel.errorMessages = Object.assign(
|
||||||
|
{},
|
||||||
|
controlModel.errorMessages,
|
||||||
|
{required: this.configData.mandatoryMessage});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
67
src/app/shared/form/builder/parsers/group-field-parser.ts
Normal file
67
src/app/shared/form/builder/parsers/group-field-parser.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
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/ds-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 {
|
||||||
|
|
||||||
|
constructor(protected configData: FormFieldModel,
|
||||||
|
protected initFormValues,
|
||||||
|
protected readOnly: boolean,
|
||||||
|
protected authorityUuid: string) {
|
||||||
|
super(configData, initFormValues, readOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
public modelFactory(fieldValue: FormFieldMetadataValueObject) {
|
||||||
|
const modelConfiguration: DynamicGroupModelConfig = this.initModel();
|
||||||
|
|
||||||
|
modelConfiguration.scopeUUID = this.authorityUuid;
|
||||||
|
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.relationFields.concat(modelConfiguration.mandatoryField);
|
||||||
|
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()[0];
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
52
src/app/shared/form/builder/parsers/list-field-parser.ts
Normal file
52
src/app/shared/form/builder/parsers/list-field-parser.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { FieldParser } from './field-parser';
|
||||||
|
import { FormFieldModel } from '../models/form-field.model';
|
||||||
|
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;
|
||||||
|
|
||||||
|
constructor(protected configData: FormFieldModel,
|
||||||
|
protected initFormValues,
|
||||||
|
protected readOnly: boolean,
|
||||||
|
protected authorityUuid: string) {
|
||||||
|
super(configData, initFormValues, readOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
public modelFactory(fieldValue: FormFieldMetadataValueObject): any {
|
||||||
|
const listModelConfig = this.initModel();
|
||||||
|
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.authorityUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
let listModel;
|
||||||
|
if (listModelConfig.repeatable) {
|
||||||
|
listModelConfig.group = [];
|
||||||
|
listModel = new DynamicListCheckboxGroupModel(listModelConfig);
|
||||||
|
} else {
|
||||||
|
listModelConfig.options = [];
|
||||||
|
listModel = new DynamicListRadioGroupModel(listModelConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
return listModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
30
src/app/shared/form/builder/parsers/lookup-field-parser.ts
Normal file
30
src/app/shared/form/builder/parsers/lookup-field-parser.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { FieldParser } from './field-parser';
|
||||||
|
import { FormFieldModel } from '../models/form-field.model';
|
||||||
|
import { AuthorityValueModel } from '../../../../core/integration/models/authority-value.model';
|
||||||
|
import { isNotEmpty } from '../../../empty.util';
|
||||||
|
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||||
|
import { DynamicLookupModel, DynamicLookupModelConfig } from '../ds-dynamic-form-ui/models/lookup/dynamic-lookup.model';
|
||||||
|
|
||||||
|
export class LookupFieldParser extends FieldParser {
|
||||||
|
|
||||||
|
constructor(protected configData: FormFieldModel,
|
||||||
|
protected initFormValues,
|
||||||
|
protected readOnly: boolean,
|
||||||
|
protected authorityUuid: string) {
|
||||||
|
super(configData, initFormValues, readOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
public modelFactory(fieldValue: any): any {
|
||||||
|
if (this.configData.selectableMetadata[0].authority) {
|
||||||
|
const lookupModelConfig: DynamicLookupModelConfig = this.initModel();
|
||||||
|
|
||||||
|
this.setAuthorityOptions(lookupModelConfig, this.authorityUuid);
|
||||||
|
lookupModelConfig.maxOptions = 10;
|
||||||
|
|
||||||
|
this.setValues(lookupModelConfig, fieldValue, true);
|
||||||
|
|
||||||
|
const lookupModel = new DynamicLookupModel(lookupModelConfig);
|
||||||
|
return lookupModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,22 @@
|
|||||||
|
import { FormFieldModel } from '../models/form-field.model';
|
||||||
|
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||||
|
import { LookupFieldParser } from './lookup-field-parser';
|
||||||
|
|
||||||
|
export class LookupNameFieldParser extends LookupFieldParser {
|
||||||
|
|
||||||
|
constructor(protected configData: FormFieldModel,
|
||||||
|
protected initFormValues,
|
||||||
|
protected readOnly: boolean,
|
||||||
|
protected authorityUuid: string) {
|
||||||
|
super(configData, initFormValues, readOnly, authorityUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public modelFactory(fieldValue: FormFieldMetadataValueObject | any): any {
|
||||||
|
const lookupModel = super.modelFactory(fieldValue);
|
||||||
|
lookupModel.separator = ',';
|
||||||
|
lookupModel.placeholder = 'form.last-name';
|
||||||
|
lookupModel.placeholder2 = 'form.first-name';
|
||||||
|
return lookupModel;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
10
src/app/shared/form/builder/parsers/name-field-parser.ts
Normal file
10
src/app/shared/form/builder/parsers/name-field-parser.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { FormFieldModel } from '../models/form-field.model';
|
||||||
|
|
||||||
|
import { ConcatFieldParser } from './concat-field-parser';
|
||||||
|
|
||||||
|
export class NameFieldParser extends ConcatFieldParser {
|
||||||
|
|
||||||
|
constructor(protected configData: FormFieldModel, protected initFormValues, protected readOnly: boolean) {
|
||||||
|
super(configData, initFormValues, readOnly, ',', 'form.last-name', 'form.first-name');
|
||||||
|
}
|
||||||
|
}
|
95
src/app/shared/form/builder/parsers/onebox-field-parser.ts
Normal file
95
src/app/shared/form/builder/parsers/onebox-field-parser.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { DynamicSelectModel, DynamicSelectModelConfig } from '@ng-dynamic-forms/core';
|
||||||
|
|
||||||
|
import { FieldParser } from './field-parser';
|
||||||
|
import { FormFieldModel } from '../models/form-field.model';
|
||||||
|
import {
|
||||||
|
COMBOBOX_GROUP_SUFFIX,
|
||||||
|
COMBOBOX_METADATA_SUFFIX,
|
||||||
|
COMBOBOX_VALUE_SUFFIX,
|
||||||
|
DsDynamicComboboxModelConfig,
|
||||||
|
DynamicComboboxModel
|
||||||
|
} from '../ds-dynamic-form-ui/models/ds-dynamic-combobox.model';
|
||||||
|
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||||
|
import { isNotEmpty } from '../../../empty.util';
|
||||||
|
import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model';
|
||||||
|
import {
|
||||||
|
DsDynamicTypeaheadModelConfig,
|
||||||
|
DynamicTypeaheadModel
|
||||||
|
} from '../ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.model';
|
||||||
|
|
||||||
|
export class OneboxFieldParser extends FieldParser {
|
||||||
|
|
||||||
|
constructor(protected configData: FormFieldModel,
|
||||||
|
protected initFormValues,
|
||||||
|
protected readOnly: boolean,
|
||||||
|
protected authorityUuid: string) {
|
||||||
|
super(configData, initFormValues, readOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
public modelFactory(fieldValue: FormFieldMetadataValueObject): any {
|
||||||
|
if (this.configData.selectableMetadata.length > 1) {
|
||||||
|
// Case ComboBox
|
||||||
|
const clsGroup = {
|
||||||
|
element: {
|
||||||
|
control: 'form-row',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clsSelect = {
|
||||||
|
element: {
|
||||||
|
control: 'input-group-addon ds-form-input-addon',
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
host: 'col-sm-4 pr-0'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clsInput = {
|
||||||
|
element: {
|
||||||
|
control: 'ds-form-input-value',
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
host: 'col-sm-8 pl-0'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const newId = this.configData.selectableMetadata[0].metadata
|
||||||
|
.split('.')
|
||||||
|
.slice(0, this.configData.selectableMetadata[0].metadata.split('.').length - 1)
|
||||||
|
.join('.');
|
||||||
|
|
||||||
|
const inputSelectGroup: DsDynamicComboboxModelConfig = Object.create(null);
|
||||||
|
inputSelectGroup.id = newId.replace(/\./g, '_') + COMBOBOX_GROUP_SUFFIX;
|
||||||
|
inputSelectGroup.group = [];
|
||||||
|
inputSelectGroup.legend = this.configData.label;
|
||||||
|
|
||||||
|
const selectModelConfig: DynamicSelectModelConfig<any> = this.initModel(newId + COMBOBOX_METADATA_SUFFIX);
|
||||||
|
this.setOptions(selectModelConfig);
|
||||||
|
if (isNotEmpty(fieldValue)) {
|
||||||
|
selectModelConfig.value = fieldValue.metadata;
|
||||||
|
}
|
||||||
|
selectModelConfig.disabled = true;
|
||||||
|
inputSelectGroup.group.push(new DynamicSelectModel(selectModelConfig, clsSelect));
|
||||||
|
|
||||||
|
const inputModelConfig: DsDynamicInputModelConfig = this.initModel(newId + COMBOBOX_VALUE_SUFFIX, true, true);
|
||||||
|
this.setValues(inputModelConfig, fieldValue);
|
||||||
|
|
||||||
|
inputSelectGroup.readOnly = selectModelConfig.disabled && inputModelConfig.readOnly;
|
||||||
|
inputSelectGroup.group.push(new DsDynamicInputModel(inputModelConfig, clsInput));
|
||||||
|
|
||||||
|
return new DynamicComboboxModel(inputSelectGroup, clsGroup);
|
||||||
|
} else if (this.configData.selectableMetadata[0].authority) {
|
||||||
|
const typeaheadModelConfig: DsDynamicTypeaheadModelConfig = this.initModel();
|
||||||
|
this.setAuthorityOptions(typeaheadModelConfig, this.authorityUuid);
|
||||||
|
this.setValues(typeaheadModelConfig, fieldValue, true);
|
||||||
|
typeaheadModelConfig.minChars = 3;
|
||||||
|
const typeaheadModel = new DynamicTypeaheadModel(typeaheadModelConfig);
|
||||||
|
return typeaheadModel;
|
||||||
|
} else {
|
||||||
|
const inputModelConfig: DsDynamicInputModelConfig = this.initModel();
|
||||||
|
this.setValues(inputModelConfig, fieldValue);
|
||||||
|
const inputModel = new DsDynamicInputModel(inputModelConfig);
|
||||||
|
return inputModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
src/app/shared/form/builder/parsers/parser.utils.ts
Normal file
17
src/app/shared/form/builder/parsers/parser.utils.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { isNull, isUndefined } from '../../../empty.util';
|
||||||
|
import { DynamicFormControlLayout, DynamicFormControlLayoutConfig } from '@ng-dynamic-forms/core';
|
||||||
|
|
||||||
|
export function setLayout(model: any, controlLayout: string, controlLayoutConfig: string, style: string) {
|
||||||
|
if (isNull(model.layout)) {
|
||||||
|
model.layout = {} as DynamicFormControlLayout;
|
||||||
|
model.layout[controlLayout] = {} as DynamicFormControlLayoutConfig;
|
||||||
|
model.layout[controlLayout][controlLayoutConfig] = style;
|
||||||
|
} else if (isUndefined(model.layout[controlLayout])) {
|
||||||
|
model.layout[controlLayout] = {} as DynamicFormControlLayoutConfig;
|
||||||
|
model.layout[controlLayout][controlLayoutConfig] = style;
|
||||||
|
} else if (isUndefined(model.layout[controlLayout][controlLayoutConfig])) {
|
||||||
|
model.layout[controlLayout][controlLayoutConfig] = style;
|
||||||
|
} else {
|
||||||
|
model.layout[controlLayout][controlLayoutConfig] = model.layout[controlLayout][controlLayoutConfig].concat(` ${style}`);
|
||||||
|
}
|
||||||
|
}
|
162
src/app/shared/form/builder/parsers/row-parser.ts
Normal file
162
src/app/shared/form/builder/parsers/row-parser.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import { DynamicFormArrayModel, DynamicFormControlModel, DynamicFormGroupModelConfig } from '@ng-dynamic-forms/core';
|
||||||
|
import { uniqueId } from 'lodash';
|
||||||
|
|
||||||
|
import { DateFieldParser } from './date-field-parser';
|
||||||
|
import { DropdownFieldParser } from './dropdown-field-parser';
|
||||||
|
import { ListFieldParser } from './list-field-parser';
|
||||||
|
import { OneboxFieldParser } from './onebox-field-parser';
|
||||||
|
import { NameFieldParser } from './name-field-parser';
|
||||||
|
import { SeriesFieldParser } from './series-field-parser';
|
||||||
|
import { TagFieldParser } from './tag-field-parser';
|
||||||
|
import { TextareaFieldParser } from './textarea-field-parser';
|
||||||
|
import { GroupFieldParser } from './group-field-parser';
|
||||||
|
import { IntegrationSearchOptions } from '../../../../core/integration/models/integration-options.model';
|
||||||
|
import { DynamicGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.model';
|
||||||
|
import { DynamicRowGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-group-model';
|
||||||
|
import { isEmpty } from '../../../empty.util';
|
||||||
|
import { LookupFieldParser } from './lookup-field-parser';
|
||||||
|
import { LookupNameFieldParser } from './lookup-name-field-parser';
|
||||||
|
import { DsDynamicInputModel } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model';
|
||||||
|
import { setLayout } from './parser.utils';
|
||||||
|
import { FormFieldModel } from '../models/form-field.model';
|
||||||
|
|
||||||
|
export const ROW_ID_PREFIX = 'df-row-group-config-';
|
||||||
|
|
||||||
|
export class RowParser {
|
||||||
|
protected authorityOptions: IntegrationSearchOptions;
|
||||||
|
|
||||||
|
constructor(protected rowData,
|
||||||
|
protected scopeUUID,
|
||||||
|
protected initFormValues: any,
|
||||||
|
protected submissionScope,
|
||||||
|
protected readOnly: boolean) {
|
||||||
|
this.authorityOptions = new IntegrationSearchOptions(scopeUUID);
|
||||||
|
}
|
||||||
|
|
||||||
|
public parse(): DynamicRowGroupModel {
|
||||||
|
let fieldModel: any = null;
|
||||||
|
let parsedResult = null;
|
||||||
|
const config: DynamicFormGroupModelConfig = {
|
||||||
|
id: uniqueId(ROW_ID_PREFIX),
|
||||||
|
group: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const scopedFields: FormFieldModel[] = this.filterScopedFields(this.rowData.fields);
|
||||||
|
|
||||||
|
const layoutGridClass = ' col-sm-' + Math.trunc(12 / scopedFields.length) + ' d-flex flex-column justify-content-start';
|
||||||
|
|
||||||
|
// Iterate over row's fields
|
||||||
|
scopedFields.forEach((fieldData: FormFieldModel) => {
|
||||||
|
|
||||||
|
switch (fieldData.input.type) {
|
||||||
|
case 'date':
|
||||||
|
fieldModel = (new DateFieldParser(fieldData, this.initFormValues, this.readOnly).parse());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'dropdown':
|
||||||
|
fieldModel = (new DropdownFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'list':
|
||||||
|
fieldModel = (new ListFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'lookup':
|
||||||
|
fieldModel = (new LookupFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'onebox':
|
||||||
|
fieldModel = (new OneboxFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'lookup-name':
|
||||||
|
fieldModel = (new LookupNameFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'name':
|
||||||
|
fieldModel = (new NameFieldParser(fieldData, this.initFormValues, this.readOnly).parse());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'series':
|
||||||
|
fieldModel = (new SeriesFieldParser(fieldData, this.initFormValues, this.readOnly).parse());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'tag':
|
||||||
|
fieldModel = (new TagFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'textarea':
|
||||||
|
fieldModel = (new TextareaFieldParser(fieldData, this.initFormValues, this.readOnly).parse());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'group':
|
||||||
|
fieldModel = new GroupFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'twobox':
|
||||||
|
// group.push(new TwoboxFieldParser(fieldData).parse());
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`unknown form control model type defined on JSON object with label "${fieldData.label}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldModel) {
|
||||||
|
if (fieldModel instanceof DynamicFormArrayModel || fieldModel instanceof DynamicGroupModel) {
|
||||||
|
if (this.rowData.fields.length > 1) {
|
||||||
|
setLayout(fieldModel, 'grid', 'host', layoutGridClass);
|
||||||
|
config.group.push(fieldModel);
|
||||||
|
// if (isEmpty(parsedResult)) {
|
||||||
|
// parsedResult = [];
|
||||||
|
// }
|
||||||
|
// parsedResult.push(fieldModel);
|
||||||
|
} else {
|
||||||
|
parsedResult = fieldModel;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
if (fieldModel instanceof Array) {
|
||||||
|
fieldModel.forEach((model) => {
|
||||||
|
parsedResult = model;
|
||||||
|
return;
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setLayout(fieldModel, 'grid', 'host', layoutGridClass);
|
||||||
|
config.group.push(fieldModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fieldModel = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (config && !isEmpty(config.group)) {
|
||||||
|
const clsGroup = {
|
||||||
|
element: {
|
||||||
|
control: 'form-row',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const groupModel = new DynamicRowGroupModel(config, clsGroup);
|
||||||
|
if (Array.isArray(parsedResult)) {
|
||||||
|
parsedResult.push(groupModel)
|
||||||
|
} else {
|
||||||
|
parsedResult = groupModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parsedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
checksFieldScope(fieldScope) {
|
||||||
|
return (isEmpty(fieldScope) || isEmpty(this.submissionScope) || fieldScope === this.submissionScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
filterScopedFields(fields: FormFieldModel[]): FormFieldModel[] {
|
||||||
|
const filteredFields: FormFieldModel[] = [];
|
||||||
|
fields.forEach((field: FormFieldModel) => {
|
||||||
|
// Whether field scope doesn't match the submission scope, skip it
|
||||||
|
if (this.checksFieldScope(field.scope)) {
|
||||||
|
filteredFields.push(field);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return filteredFields;
|
||||||
|
}
|
||||||
|
}
|
10
src/app/shared/form/builder/parsers/series-field-parser.ts
Normal file
10
src/app/shared/form/builder/parsers/series-field-parser.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { FormFieldModel } from '../models/form-field.model';
|
||||||
|
|
||||||
|
import { ConcatFieldParser } from './concat-field-parser';
|
||||||
|
|
||||||
|
export class SeriesFieldParser extends ConcatFieldParser {
|
||||||
|
|
||||||
|
constructor(protected configData: FormFieldModel, protected initFormValues, protected readOnly: boolean) {
|
||||||
|
super(configData, initFormValues, readOnly, ';');
|
||||||
|
}
|
||||||
|
}
|
31
src/app/shared/form/builder/parsers/tag-field-parser.ts
Normal file
31
src/app/shared/form/builder/parsers/tag-field-parser.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { FieldParser } from './field-parser';
|
||||||
|
import { FormFieldModel } from '../models/form-field.model';
|
||||||
|
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||||
|
import { isNotEmpty } from '../../../empty.util';
|
||||||
|
import { DynamicTagModel, DynamicTagModelConfig } from '../ds-dynamic-form-ui/models/tag/dynamic-tag.model';
|
||||||
|
|
||||||
|
export class TagFieldParser extends FieldParser {
|
||||||
|
|
||||||
|
constructor(protected configData: FormFieldModel,
|
||||||
|
protected initFormValues,
|
||||||
|
protected readOnly: boolean,
|
||||||
|
protected authorityUuid: string) {
|
||||||
|
super(configData, initFormValues, readOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
public modelFactory(fieldValue: FormFieldMetadataValueObject): any {
|
||||||
|
const tagModelConfig: DynamicTagModelConfig = this.initModel();
|
||||||
|
if (this.configData.selectableMetadata[0].authority
|
||||||
|
&& this.configData.selectableMetadata[0].authority.length > 0) {
|
||||||
|
this.setAuthorityOptions(tagModelConfig, this.authorityUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
tagModelConfig.minChars = 3;
|
||||||
|
this.setValues(tagModelConfig, fieldValue, null, true);
|
||||||
|
|
||||||
|
const tagModel = new DynamicTagModel(tagModelConfig);
|
||||||
|
|
||||||
|
return tagModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
32
src/app/shared/form/builder/parsers/textarea-field-parser.ts
Normal file
32
src/app/shared/form/builder/parsers/textarea-field-parser.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { FieldParser } from './field-parser';
|
||||||
|
import {
|
||||||
|
DynamicFormControlLayout, DynamicTextAreaModel, DynamicTextAreaModelConfig
|
||||||
|
} from '@ng-dynamic-forms/core';
|
||||||
|
import { FormFieldModel } from '../models/form-field.model';
|
||||||
|
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||||
|
import { isNotEmpty } from '../../../empty.util';
|
||||||
|
import {
|
||||||
|
DsDynamicTextAreaModel,
|
||||||
|
DsDynamicTextAreaModelConfig
|
||||||
|
} from '../ds-dynamic-form-ui/models/ds-dynamic-textarea.model';
|
||||||
|
|
||||||
|
export class TextareaFieldParser extends FieldParser {
|
||||||
|
|
||||||
|
public modelFactory(fieldValue: FormFieldMetadataValueObject | any): any {
|
||||||
|
const textAreaModelConfig: DsDynamicTextAreaModelConfig = this.initModel();
|
||||||
|
|
||||||
|
let layout: DynamicFormControlLayout;
|
||||||
|
|
||||||
|
layout = {
|
||||||
|
element: {
|
||||||
|
label: 'col-form-label'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
textAreaModelConfig.rows = 10;
|
||||||
|
this.setValues(textAreaModelConfig, fieldValue);
|
||||||
|
const textAreaModel = new DsDynamicTextAreaModel(textAreaModelConfig, layout);
|
||||||
|
|
||||||
|
return textAreaModel;
|
||||||
|
}
|
||||||
|
}
|
10
src/app/shared/form/builder/parsers/twobox-field-parser.ts
Normal file
10
src/app/shared/form/builder/parsers/twobox-field-parser.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { FieldParser } from './field-parser';
|
||||||
|
import { FormFieldModel } from '../models/form-field.model';
|
||||||
|
|
||||||
|
// @TODO to be implemented
|
||||||
|
export class TwoboxFieldParser extends FieldParser {
|
||||||
|
|
||||||
|
public modelFactory(): any {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
126
src/app/shared/form/form.actions.ts
Normal file
126
src/app/shared/form/form.actions.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { Action } from '@ngrx/store';
|
||||||
|
|
||||||
|
import { type } from '../ngrx/type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For each action type in an action group, make a simple
|
||||||
|
* enum object for all of this group's action types.
|
||||||
|
*
|
||||||
|
* The 'type' utility function coerces strings into string
|
||||||
|
* literal types and runs a simple check to guarantee all
|
||||||
|
* action types in the application are unique.
|
||||||
|
*/
|
||||||
|
export const FormActionTypes = {
|
||||||
|
FORM_INIT: type('dspace/form/FORM_INIT'),
|
||||||
|
FORM_CHANGE: type('dspace/form/FORM_CHANGE'),
|
||||||
|
FORM_REMOVE: type('dspace/form/FORM_REMOVE'),
|
||||||
|
FORM_STATUS_CHANGE: type('dspace/form/FORM_STATUS_CHANGE'),
|
||||||
|
FORM_ADD_ERROR: type('dspace/form/ADD_ERROR'),
|
||||||
|
FORM_REMOVE_ERROR: type('dspace/form/REMOVE_ERROR'),
|
||||||
|
CLEAR_ERRORS: type('dspace/form/CLEAR_ERRORS'),
|
||||||
|
};
|
||||||
|
|
||||||
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
export class FormInitAction implements Action {
|
||||||
|
type = FormActionTypes.FORM_INIT;
|
||||||
|
payload: {
|
||||||
|
formId: string;
|
||||||
|
formData: any;
|
||||||
|
valid: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new FormInitAction
|
||||||
|
*
|
||||||
|
* @param formId
|
||||||
|
* the Form's ID
|
||||||
|
* @param formData
|
||||||
|
* the FormGroup Object
|
||||||
|
* @param valid
|
||||||
|
* the Form validation status
|
||||||
|
*/
|
||||||
|
constructor(formId: string, formData: any, valid: boolean) {
|
||||||
|
this.payload = {formId, formData, valid};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FormChangeAction implements Action {
|
||||||
|
type = FormActionTypes.FORM_CHANGE;
|
||||||
|
payload: {
|
||||||
|
formId: string;
|
||||||
|
formData: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new FormInitAction
|
||||||
|
*
|
||||||
|
* @param formId
|
||||||
|
* the Form's ID
|
||||||
|
* @param formData
|
||||||
|
* the FormGroup Object
|
||||||
|
*/
|
||||||
|
constructor(formId: string, formData: any) {
|
||||||
|
this.payload = {formId, formData};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FormRemoveAction implements Action {
|
||||||
|
type = FormActionTypes.FORM_REMOVE;
|
||||||
|
payload: {
|
||||||
|
formId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new FormRemoveAction
|
||||||
|
*
|
||||||
|
* @param formId
|
||||||
|
* the Form's ID
|
||||||
|
*/
|
||||||
|
constructor(formId: string) {
|
||||||
|
this.payload = {formId};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FormStatusChangeAction implements Action {
|
||||||
|
type = FormActionTypes.FORM_STATUS_CHANGE;
|
||||||
|
payload: {
|
||||||
|
formId: string;
|
||||||
|
valid: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new FormInitAction
|
||||||
|
*
|
||||||
|
* @param formId
|
||||||
|
* the Form's ID
|
||||||
|
* @param valid
|
||||||
|
* the Form validation status
|
||||||
|
*/
|
||||||
|
constructor(formId: string, valid: boolean) {
|
||||||
|
this.payload = {formId, valid};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FormAddError implements Action {
|
||||||
|
type = FormActionTypes.FORM_ADD_ERROR;
|
||||||
|
payload: {
|
||||||
|
formId: string,
|
||||||
|
fieldId: string,
|
||||||
|
errorMessage: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(formId: string, fieldId: string, errorMessage: string) {
|
||||||
|
this.payload = {formId, fieldId, errorMessage};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export a type alias of all actions in this action group
|
||||||
|
* so that reducers can easily compose action types
|
||||||
|
*/
|
||||||
|
export type FormAction = FormInitAction
|
||||||
|
| FormChangeAction
|
||||||
|
| FormStatusChangeAction
|
||||||
|
| FormAddError
|
61
src/app/shared/form/form.component.html
Normal file
61
src/app/shared/form/form.component.html
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<div class="container-fluid">
|
||||||
|
<form [formGroup]="formGroup">
|
||||||
|
|
||||||
|
<ds-dynamic-form
|
||||||
|
[formId]="formId"
|
||||||
|
[formGroup]="formGroup"
|
||||||
|
[formModel]="formModel"
|
||||||
|
(dfBlur)="onBlur($event)"
|
||||||
|
(dfChange)="onChange($event)"
|
||||||
|
(dfFocus)="onFocus($event)">
|
||||||
|
|
||||||
|
<ng-template modelType="ARRAY" let-group let-index="index" let-context="context">
|
||||||
|
|
||||||
|
<!--Array with repeteable items-->
|
||||||
|
<div *ngIf="!context.notRepeteable"
|
||||||
|
class="col-xs-2 d-flex flex-column justify-content-sm-center align-items-end mt-3">
|
||||||
|
<div class="btn-group" role="group" aria-label="Basic example">
|
||||||
|
<button type="button" class="btn btn-secondary"
|
||||||
|
[disabled]="isItemReadOnly(context, index)"
|
||||||
|
(click)="insertItem($event, group.context, group.index + 1)">
|
||||||
|
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary"
|
||||||
|
(click)="removeItem($event, context, index)"
|
||||||
|
[disabled]="group.context.groups.length === 1 || isItemReadOnly(context, index)">
|
||||||
|
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--Array with non repeteable items - Only delete button-->
|
||||||
|
<div *ngIf="context.notRepeteable && group.context.groups.length > 1"
|
||||||
|
class="col-xs-2 d-flex flex-column justify-content-sm-center align-items-end mt-3">
|
||||||
|
<div class="btn-group" role="group" aria-label="Basic example">
|
||||||
|
<button type="button" class="btn btn-secondary"
|
||||||
|
(click)="removeItem($event, context, index)"
|
||||||
|
[disabled]="group.context.groups.length === 1 || isItemReadOnly(context, index)">
|
||||||
|
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
</ds-dynamic-form>
|
||||||
|
|
||||||
|
<div *ngIf="displaySubmit">
|
||||||
|
<hr>
|
||||||
|
<div class="form-group row">
|
||||||
|
|
||||||
|
<div class="col text-right">
|
||||||
|
<button type="reset" class="btn btn-default" (click)="reset()">{{'form.cancel' | translate}}</button>
|
||||||
|
<button type="submit" class="btn btn-primary" (click)="onSubmit($event)"
|
||||||
|
[disabled]="!(isValid() | async)">{{'form.submit' | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
23
src/app/shared/form/form.component.scss
Normal file
23
src/app/shared/form/form.component.scss
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
@import "../../../styles/_variables.scss";
|
||||||
|
|
||||||
|
.ds-form-input-addon {
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ds-form-input-btn {
|
||||||
|
border: $input-btn-border-width solid $input-border-color;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ds-form-input-btn:focus {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ds-form-input-value {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
175
src/app/shared/form/form.component.spec.ts
Normal file
175
src/app/shared/form/form.component.spec.ts
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
// Load the implementations that should be tested
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA,
|
||||||
|
DebugElement
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
async,
|
||||||
|
ComponentFixture,
|
||||||
|
inject,
|
||||||
|
TestBed,
|
||||||
|
} from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { StoreModule } from '@ngrx/store';
|
||||||
|
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
import Spy = jasmine.Spy;
|
||||||
|
|
||||||
|
import { FormComponent } from './form.component';
|
||||||
|
import { FormService } from './form.service';
|
||||||
|
import { DynamicFormControlModel, DynamicFormValidationService, DynamicInputModel } from '@ng-dynamic-forms/core';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { FormBuilderService } from './builder/form-builder.service';
|
||||||
|
import { SubmissionFormsConfigService } from '../../core/config/submission-forms-config.service';
|
||||||
|
import { ResponseCacheService } from '../../core/cache/response-cache.service';
|
||||||
|
import { RequestService } from '../../core/data/request.service';
|
||||||
|
import { ObjectCacheService } from '../../core/cache/object-cache.service';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
|
function createTestComponent<T>(html: string, type: { new(...args: any[]): T }): ComponentFixture<T> {
|
||||||
|
TestBed.overrideComponent(type, {
|
||||||
|
set: { template: html }
|
||||||
|
});
|
||||||
|
const fixture = TestBed.createComponent(type);
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
return fixture as ComponentFixture<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TEST_FORM_MODEL = [
|
||||||
|
|
||||||
|
new DynamicInputModel(
|
||||||
|
{
|
||||||
|
id: 'dc_title',
|
||||||
|
label: 'Title',
|
||||||
|
placeholder: 'Title',
|
||||||
|
validators: {
|
||||||
|
required: null
|
||||||
|
},
|
||||||
|
errorMessages: {
|
||||||
|
required: 'You must enter a main title for this item.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
new DynamicInputModel(
|
||||||
|
{
|
||||||
|
id: 'dc_title_alternative',
|
||||||
|
label: 'Other Titles',
|
||||||
|
placeholder: 'Other Titles',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
new DynamicInputModel(
|
||||||
|
{
|
||||||
|
id: 'dc_publisher',
|
||||||
|
label: 'Publisher',
|
||||||
|
placeholder: 'Publisher',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
new DynamicInputModel(
|
||||||
|
{
|
||||||
|
id: 'dc_identifier_citation',
|
||||||
|
label: 'Citation',
|
||||||
|
placeholder: 'Citation',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
new DynamicInputModel(
|
||||||
|
{
|
||||||
|
id: 'dc_identifier_issn',
|
||||||
|
label: 'Identifiers',
|
||||||
|
placeholder: 'Identifiers',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TEST_FORM_GROUP = {
|
||||||
|
dc_title: new FormControl(),
|
||||||
|
dc_title_alternative: new FormControl(),
|
||||||
|
dc_publisher: new FormControl(),
|
||||||
|
dc_identifier_citation: new FormControl(),
|
||||||
|
dc_identifier_issn: new FormControl()
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Form component', () => {
|
||||||
|
|
||||||
|
let testComp: TestComponent;
|
||||||
|
let testFixture: ComponentFixture<TestComponent>;
|
||||||
|
let html;
|
||||||
|
const formServiceStub = {
|
||||||
|
getFormData: (formId) => Observable.of([])
|
||||||
|
}
|
||||||
|
const formBuilderServiceStub = {
|
||||||
|
createFormGroup: (formModel) => new FormGroup(TEST_FORM_GROUP)
|
||||||
|
}
|
||||||
|
const submissionFormsConfigServiceStub = { }
|
||||||
|
|
||||||
|
// async beforeEach
|
||||||
|
beforeEach(async(() => {
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
StoreModule.forRoot({}),
|
||||||
|
NgbModule.forRoot(),
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
FormComponent,
|
||||||
|
TestComponent,
|
||||||
|
], // declare the test component
|
||||||
|
providers: [
|
||||||
|
FormComponent,
|
||||||
|
{ provide: FormService, useValue: formServiceStub },
|
||||||
|
{ provide: FormBuilderService, useValue: formBuilderServiceStub },
|
||||||
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
});
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
// synchronous beforeEach
|
||||||
|
beforeEach(() => {
|
||||||
|
html = `
|
||||||
|
<ds-form #formRef="formComponent"
|
||||||
|
[formId]="formId"
|
||||||
|
[formModel]="formModel"
|
||||||
|
[displaySubmit]="false"></ds-form>`;
|
||||||
|
|
||||||
|
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||||
|
testComp = testFixture.componentInstance;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create Form Component', inject([FormComponent], (app: FormComponent) => {
|
||||||
|
expect(app).toBeDefined();
|
||||||
|
}));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// declare a test component
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-test-cmp',
|
||||||
|
template: ``
|
||||||
|
})
|
||||||
|
class TestComponent {
|
||||||
|
|
||||||
|
public formId;
|
||||||
|
public formModel: DynamicFormControlModel[];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.formId = 'testForm';
|
||||||
|
this.formModel = TEST_FORM_MODEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
274
src/app/shared/form/form.component.ts
Normal file
274
src/app/shared/form/form.component.ts
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||||
|
import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DynamicFormArrayModel,
|
||||||
|
DynamicFormControlEvent,
|
||||||
|
DynamicFormControlModel,
|
||||||
|
DynamicFormGroupModel,
|
||||||
|
} from '@ng-dynamic-forms/core';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
import { FormChangeAction, FormInitAction, FormRemoveAction, FormStatusChangeAction } from './form.actions';
|
||||||
|
import { FormBuilderService } from './builder/form-builder.service';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { Subscription } from 'rxjs/Subscription';
|
||||||
|
import { hasValue, isNotNull, isNull } from '../empty.util';
|
||||||
|
import { FormService } from './form.service';
|
||||||
|
import { formObjectFromIdSelector } from './selectors';
|
||||||
|
import { FormEntry, FormError } from './form.reducers';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default form component.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
exportAs: 'formComponent',
|
||||||
|
selector: 'ds-form',
|
||||||
|
styleUrls: ['form.component.scss'],
|
||||||
|
templateUrl: 'form.component.html',
|
||||||
|
})
|
||||||
|
export class FormComponent implements OnDestroy, OnInit {
|
||||||
|
|
||||||
|
private formValid: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean that indicate if to display form's submit and cancel buttons
|
||||||
|
*/
|
||||||
|
@Input() displaySubmit = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The form unique ID
|
||||||
|
*/
|
||||||
|
@Input() formId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of DynamicFormControlModel type
|
||||||
|
*/
|
||||||
|
@Input() formModel: DynamicFormControlModel[];
|
||||||
|
@Input() parentFormModel: DynamicFormGroupModel | DynamicFormGroupModel[];
|
||||||
|
@Input() formGroup: FormGroup;
|
||||||
|
|
||||||
|
/* 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 */
|
||||||
|
@Output() addArrayItem: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||||
|
@Output() removeArrayItem: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when form is valid and submitted .
|
||||||
|
* Event's payload equals to the form content.
|
||||||
|
*/
|
||||||
|
@Output() submit: EventEmitter<Observable<any>> = new EventEmitter<Observable<any>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object of FormGroup type
|
||||||
|
*/
|
||||||
|
// public formGroup: FormGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||||
|
* @type {Array}
|
||||||
|
*/
|
||||||
|
private subs: Subscription[] = [];
|
||||||
|
|
||||||
|
constructor(private formService: FormService,
|
||||||
|
protected changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private formBuilderService: FormBuilderService,
|
||||||
|
private store: Store<AppState>) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method provided by Angular. Invoked after the view has been initialized.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*ngAfterViewChecked(): void {
|
||||||
|
this.subs.push(this.formGroup.valueChanges
|
||||||
|
.filter((formGroup) => this.formGroup.dirty)
|
||||||
|
.subscribe(() => {
|
||||||
|
// Dispatch a FormChangeAction if the user has changed the value in the UI
|
||||||
|
this.store.dispatch(new FormChangeAction(this.formId, this.formGroup.value));
|
||||||
|
this.formGroup.markAsPristine();
|
||||||
|
}));
|
||||||
|
}*/
|
||||||
|
|
||||||
|
private getFormGroup(): FormGroup {
|
||||||
|
if (!!this.parentFormModel) {
|
||||||
|
return this.formGroup.parent as FormGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.formGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFormGroupValue() {
|
||||||
|
return this.getFormGroup().value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFormGroupValidStatus() {
|
||||||
|
return this.getFormGroup().valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method provided by Angular. Invoked after the constructor
|
||||||
|
*/
|
||||||
|
ngOnInit() {
|
||||||
|
if (!this.formGroup) {
|
||||||
|
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||||
|
} else {
|
||||||
|
this.formModel.forEach((model) => {
|
||||||
|
this.formBuilderService.addFormGroupControl(this.formGroup, this.parentFormModel, model);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.store.dispatch(new FormInitAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel), this.getFormGroupValidStatus()));
|
||||||
|
|
||||||
|
// TODO: take a look to the following method:
|
||||||
|
// this.keepSync();
|
||||||
|
|
||||||
|
this.formValid = this.getFormGroupValidStatus();
|
||||||
|
|
||||||
|
this.subs.push(this.formGroup.statusChanges
|
||||||
|
.filter((currentStatus) => this.formValid !== this.getFormGroupValidStatus())
|
||||||
|
.subscribe((currentStatus) => {
|
||||||
|
// Dispatch a FormStatusChangeAction if the form status has changed
|
||||||
|
this.store.dispatch(new FormStatusChangeAction(this.formId, this.getFormGroupValidStatus()));
|
||||||
|
this.formValid = this.getFormGroupValidStatus();
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.subs.push(
|
||||||
|
this.store.select(formObjectFromIdSelector(this.formId))
|
||||||
|
.filter((formState: FormEntry) => !!formState && !isEmpty(formState.errors))
|
||||||
|
.map((formState) => formState.errors)
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.delay(100) // this terrible delay is here to prevent the detection change error
|
||||||
|
.subscribe((errors: FormError[]) => {
|
||||||
|
const {formGroup, formModel} = this;
|
||||||
|
|
||||||
|
errors.forEach((error: FormError) => {
|
||||||
|
const {fieldId} = error;
|
||||||
|
let field: AbstractControl;
|
||||||
|
if (!!this.parentFormModel) {
|
||||||
|
field = this.formBuilderService.getFormControlById(fieldId, formGroup.parent as FormGroup, formModel);
|
||||||
|
} else {
|
||||||
|
field = this.formBuilderService.getFormControlById(fieldId, formGroup, formModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field) {
|
||||||
|
const model: DynamicFormControlModel = this.formBuilderService.findById(fieldId, formModel);
|
||||||
|
this.formService.addErrorToField(field, model, error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.changeDetectorRef.detectChanges();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method provided by Angular. Invoked when the instance is destroyed
|
||||||
|
*/
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.store.dispatch(new FormRemoveAction(this.formId));
|
||||||
|
this.subs
|
||||||
|
.filter((sub) => hasValue(sub))
|
||||||
|
.forEach((sub) => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to check if the form status is valid or not
|
||||||
|
*/
|
||||||
|
public isValid(): Observable<boolean> {
|
||||||
|
return this.formService.isValid(this.formId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to keep synchronized form controls values with form state
|
||||||
|
*/
|
||||||
|
private keepSync() {
|
||||||
|
this.subs.push(this.formService.getFormData(this.formId)
|
||||||
|
.subscribe((stateFormData) => {
|
||||||
|
if (!Object.is(stateFormData, this.formGroup.value) && this.formGroup) {
|
||||||
|
this.formGroup.setValue(stateFormData);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlur(event) {
|
||||||
|
this.blur.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocus(event) {
|
||||||
|
this.focus.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(event) {
|
||||||
|
const action: FormChangeAction = new FormChangeAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel));
|
||||||
|
|
||||||
|
this.store.dispatch(action);
|
||||||
|
this.formGroup.markAsPristine();
|
||||||
|
|
||||||
|
this.change.emit(event);
|
||||||
|
const control: FormControl = event.control;
|
||||||
|
|
||||||
|
control.setErrors(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called on submit.
|
||||||
|
* Emit a new submit Event whether the form is valid, mark fields with error otherwise
|
||||||
|
*/
|
||||||
|
onSubmit() {
|
||||||
|
if (this.getFormGroupValidStatus()) {
|
||||||
|
this.submit.emit(this.formService.getFormData(this.formId));
|
||||||
|
} else {
|
||||||
|
this.formService.validateAllFormFields(this.formGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to reset form fields
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.formGroup.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
isItemReadOnly(arrayContext: DynamicFormArrayModel, index: number): boolean {
|
||||||
|
const context = arrayContext.groups[index];
|
||||||
|
const model = context.group[0] as any;
|
||||||
|
return model.readOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem($event, arrayContext: DynamicFormArrayModel, index: number) {
|
||||||
|
const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray;
|
||||||
|
this.removeArrayItem.emit(this.getEvent($event, arrayContext, index, 'remove'));
|
||||||
|
this.formBuilderService.removeFormArrayGroup(index, formArrayControl, arrayContext);
|
||||||
|
this.store.dispatch(new FormChangeAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel)));
|
||||||
|
}
|
||||||
|
|
||||||
|
insertItem($event, arrayContext: DynamicFormArrayModel, index: number) {
|
||||||
|
const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray;
|
||||||
|
this.formBuilderService.insertFormArrayGroup(index, formArrayControl, arrayContext);
|
||||||
|
this.addArrayItem.emit(this.getEvent($event, arrayContext, index, 'add'));
|
||||||
|
this.store.dispatch(new FormChangeAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel)));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getEvent($event: any, arrayContext: DynamicFormArrayModel, index: number, type: string): DynamicFormControlEvent {
|
||||||
|
const context = arrayContext.groups[index];
|
||||||
|
const itemGroupModel = context.context;
|
||||||
|
let group = this.formGroup.get(itemGroupModel.id) as FormGroup;
|
||||||
|
if (isNull(group)) {
|
||||||
|
for (const key of Object.keys(this.formGroup.controls)) {
|
||||||
|
group = this.formGroup.controls[key].get(itemGroupModel.id) as FormGroup;
|
||||||
|
if (isNotNull(group)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const model = context.group[0] as DynamicFormControlModel;
|
||||||
|
const control = group.controls[index] as FormControl;
|
||||||
|
return {$event, context, control, group, model, type};
|
||||||
|
}
|
||||||
|
}
|
11
src/app/shared/form/form.effects.ts
Normal file
11
src/app/shared/form/form.effects.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Actions, Effect } from '@ngrx/effects';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FormEffects {
|
||||||
|
|
||||||
|
constructor(private actions$: Actions) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
179
src/app/shared/form/form.reducers.ts
Normal file
179
src/app/shared/form/form.reducers.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import {
|
||||||
|
FormAction, FormActionTypes, FormAddError, FormChangeAction, FormInitAction, FormRemoveAction,
|
||||||
|
FormStatusChangeAction
|
||||||
|
} from './form.actions';
|
||||||
|
import { hasValue } from '../empty.util';
|
||||||
|
import { uniqWith, isEqual } from 'lodash';
|
||||||
|
|
||||||
|
export interface FormError {
|
||||||
|
message: string;
|
||||||
|
fieldId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormEntry {
|
||||||
|
data: any;
|
||||||
|
valid: boolean;
|
||||||
|
errors: FormError[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormState {
|
||||||
|
[formId: string]: FormEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: FormState = Object.create(null);
|
||||||
|
|
||||||
|
export function formReducer(state = initialState, action: FormAction): FormState {
|
||||||
|
switch (action.type) {
|
||||||
|
|
||||||
|
case FormActionTypes.FORM_INIT: {
|
||||||
|
return initForm(state, action as FormInitAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
case FormActionTypes.FORM_CHANGE: {
|
||||||
|
return changeDataForm(state, action as FormChangeAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
case FormActionTypes.FORM_REMOVE: {
|
||||||
|
return removeForm(state, action as FormRemoveAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
case FormActionTypes.FORM_STATUS_CHANGE: {
|
||||||
|
return changeStatusForm(state, action as FormStatusChangeAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
case FormActionTypes.FORM_ADD_ERROR: {
|
||||||
|
return addFormErrors(state, action as FormAddError)
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addFormErrors(state: FormState, action: FormAddError) {
|
||||||
|
const formId = action.payload.formId;
|
||||||
|
if (hasValue(state[formId])) {
|
||||||
|
const error: FormError = {
|
||||||
|
fieldId: action.payload.fieldId,
|
||||||
|
message: action.payload.errorMessage
|
||||||
|
};
|
||||||
|
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[ formId ]: {
|
||||||
|
data: state[formId].data,
|
||||||
|
valid: state[formId].valid,
|
||||||
|
errors: state[formId].errors ? uniqWith(state[formId].errors.concat(error), isEqual) : [].concat(error),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init form state.
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* the current state
|
||||||
|
* @param action
|
||||||
|
* an FormInitAction
|
||||||
|
* @return FormState
|
||||||
|
* the new state, with the form initialized.
|
||||||
|
*/
|
||||||
|
function initForm(state: FormState, action: FormInitAction): FormState {
|
||||||
|
if (!hasValue(state[ action.payload.formId ])) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[ action.payload.formId ]: {
|
||||||
|
data: action.payload.formData,
|
||||||
|
valid: action.payload.valid
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const newState = Object.assign({}, state);
|
||||||
|
newState[ action.payload.formId ] = Object.assign({}, newState[ action.payload.formId ], {
|
||||||
|
data: action.payload.formData,
|
||||||
|
valid: action.payload.valid
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set form data.
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* the current state
|
||||||
|
* @param action
|
||||||
|
* an FormChangeAction
|
||||||
|
* @return FormState
|
||||||
|
* the new state, with the data changed.
|
||||||
|
*/
|
||||||
|
function changeDataForm(state: FormState, action: FormChangeAction): FormState {
|
||||||
|
if (!hasValue(state[ action.payload.formId ])) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[ action.payload.formId ]: {
|
||||||
|
data: action.payload.formData,
|
||||||
|
valid: state[ action.payload.formId ].valid
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const newState = Object.assign({}, state);
|
||||||
|
newState[ action.payload.formId ] = Object.assign({}, newState[ action.payload.formId ], {
|
||||||
|
data: action.payload.formData,
|
||||||
|
valid: state[ action.payload.formId ].valid
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set form status.
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* the current state
|
||||||
|
* @param action
|
||||||
|
* an FormStatusChangeAction
|
||||||
|
* @return FormState
|
||||||
|
* the new state, with the status changed.
|
||||||
|
*/
|
||||||
|
function changeStatusForm(state: FormState, action: FormStatusChangeAction): FormState {
|
||||||
|
if (!hasValue(state[ action.payload.formId ])) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[ action.payload.formId ]: {
|
||||||
|
data: state[ action.payload.formId ].data,
|
||||||
|
valid: action.payload.valid
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const newState = Object.assign({}, state);
|
||||||
|
newState[ action.payload.formId ] = Object.assign({}, newState[ action.payload.formId ], {
|
||||||
|
data: state[ action.payload.formId ].data,
|
||||||
|
valid: action.payload.valid
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove form state.
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* the current state
|
||||||
|
* @param action
|
||||||
|
* an FormRemoveAction
|
||||||
|
* @return FormState
|
||||||
|
* the new state, with the form initialized.
|
||||||
|
*/
|
||||||
|
function removeForm(state: FormState, action: FormRemoveAction): FormState {
|
||||||
|
if (hasValue(state[ action.payload.formId ])) {
|
||||||
|
const newState = Object.assign({}, state);
|
||||||
|
delete newState[ action.payload.formId ];
|
||||||
|
return newState;
|
||||||
|
} else {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
118
src/app/shared/form/form.service.ts
Normal file
118
src/app/shared/form/form.service.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { AbstractControl, FormControl, FormGroup } from '@angular/forms';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
import { formObjectFromIdSelector } from './selectors';
|
||||||
|
import { FormBuilderService } from './builder/form-builder.service';
|
||||||
|
import { DynamicFormControlModel, DynamicFormGroupModel } from '@ng-dynamic-forms/core';
|
||||||
|
import { isNotEmpty, isNotUndefined } from '../empty.util';
|
||||||
|
import { find, uniqueId } from 'lodash';
|
||||||
|
import { FormChangeAction } from './form.actions';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FormService {
|
||||||
|
|
||||||
|
constructor(private formBuilderService: FormBuilderService,
|
||||||
|
private store: Store<AppState>) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to retrieve form's status from state
|
||||||
|
*/
|
||||||
|
public isValid(formId: string): Observable<boolean> {
|
||||||
|
return this.store.select(formObjectFromIdSelector(formId))
|
||||||
|
.filter((state) => isNotUndefined(state))
|
||||||
|
.map((state) => state.valid)
|
||||||
|
.distinctUntilChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to retrieve form's data from state
|
||||||
|
*/
|
||||||
|
public getFormData(formId: string): Observable<FormControl> {
|
||||||
|
return this.store.select(formObjectFromIdSelector(formId))
|
||||||
|
.filter((state) => isNotUndefined(state))
|
||||||
|
.map((state) => state.data)
|
||||||
|
.distinctUntilChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to retrieve form's data from state
|
||||||
|
*/
|
||||||
|
public isFormInitialized(formId: string): Observable<boolean> {
|
||||||
|
return this.store.select(formObjectFromIdSelector(formId))
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.map((state) => isNotUndefined(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
public getUniqueId(formId): string {
|
||||||
|
return uniqueId() + '_' + formId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to validate form's fields
|
||||||
|
*/
|
||||||
|
public validateAllFormFields(formGroup: FormGroup) {
|
||||||
|
Object.keys(formGroup.controls).forEach((field) => {
|
||||||
|
const control = formGroup.get(field);
|
||||||
|
if (control instanceof FormControl) {
|
||||||
|
control.markAsTouched({onlySelf: true});
|
||||||
|
} else if (control instanceof FormGroup) {
|
||||||
|
this.validateAllFormFields(control);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public setValue(formGroup: FormGroup, fieldModel: DynamicFormControlModel, fieldId: string, value: any) {
|
||||||
|
if (isNotEmpty(fieldModel)) {
|
||||||
|
const path = this.formBuilderService.getPath(fieldModel);
|
||||||
|
const fieldControl = formGroup.get(path);
|
||||||
|
fieldControl.markAsDirty();
|
||||||
|
fieldControl.setValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public addErrorToField(field: AbstractControl, model: DynamicFormControlModel, message: string) {
|
||||||
|
const errorFound = !!(find(field.errors, (err) => err === message));
|
||||||
|
|
||||||
|
// search for the same error in the formControl.errors property
|
||||||
|
if (!errorFound) {
|
||||||
|
const errorKey = uniqueId('error-'); // create a single key for the error
|
||||||
|
const error = {}; // create the error object
|
||||||
|
|
||||||
|
error[errorKey] = message; // assign message
|
||||||
|
|
||||||
|
// if form control model has errorMessages object, create it
|
||||||
|
if (!model.errorMessages) {
|
||||||
|
model.errorMessages = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// put the error in the form control model
|
||||||
|
model.errorMessages[errorKey] = message;
|
||||||
|
|
||||||
|
// Use correct error messages from the model
|
||||||
|
const lastArray = message.split('.');
|
||||||
|
if (lastArray && lastArray.length > 0) {
|
||||||
|
const last = lastArray[lastArray.length - 1];
|
||||||
|
const modelMsg = model.errorMessages[last];
|
||||||
|
if (modelMsg && modelMsg.length > 0) {
|
||||||
|
model.errorMessages[errorKey] = modelMsg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the error in the form control
|
||||||
|
field.setErrors(error);
|
||||||
|
|
||||||
|
// formGroup.markAsDirty();
|
||||||
|
field.markAsTouched();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetForm(formGroup: FormGroup, groupModel: DynamicFormControlModel[], formId: string) {
|
||||||
|
this.formBuilderService.clearAllModelsValue(groupModel);
|
||||||
|
formGroup.reset();
|
||||||
|
this.store.dispatch(new FormChangeAction(formId, formGroup.value));
|
||||||
|
}
|
||||||
|
}
|
10
src/app/shared/form/selectors.ts
Normal file
10
src/app/shared/form/selectors.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { createSelector, MemoizedSelector, Selector } from '@ngrx/store';
|
||||||
|
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
import { FormEntry, FormState } from './form.reducers';
|
||||||
|
|
||||||
|
export const formStateSelector = (state: AppState) => state.forms;
|
||||||
|
|
||||||
|
export function formObjectFromIdSelector(formId: string): MemoizedSelector<AppState, FormEntry> {
|
||||||
|
return createSelector(formStateSelector, (forms: FormState) => forms[formId]);
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user