mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge pull request #264 from 4Science/dynamic_forms
Dynamic forms Components
This commit is contained in:
@@ -10,8 +10,8 @@ addons:
|
||||
language: node_js
|
||||
|
||||
node_js:
|
||||
- "6"
|
||||
- "8"
|
||||
- "9"
|
||||
|
||||
cache:
|
||||
yarn: true
|
||||
|
@@ -14,7 +14,7 @@ If you're looking for the 2016 Angular 2 DSpace UI prototype, you can find it [h
|
||||
Quick start
|
||||
-----------
|
||||
|
||||
**Ensure you're running [Node](https://nodejs.org) >= `v6.9.x`, [npm](https://www.npmjs.com/) >= `v3.x` and [yarn](https://yarnpkg.com) >= `v0.20.x`**
|
||||
**Ensure you're running [Node](https://nodejs.org) >= `v8.0.x`, [npm](https://www.npmjs.com/) >= `v3.x` and [yarn](https://yarnpkg.com) >= `v0.20.x`**
|
||||
|
||||
```bash
|
||||
# clone the repo
|
||||
|
@@ -22,6 +22,14 @@ module.exports = {
|
||||
// msToLive: 1000, // 15 minutes
|
||||
control: 'max-age=60' // revalidate browser
|
||||
},
|
||||
// Form settings
|
||||
form: {
|
||||
// NOTE: Map server-side validators to comparative Angular form validators
|
||||
validatorMap: {
|
||||
required: 'required',
|
||||
regex: 'pattern'
|
||||
}
|
||||
},
|
||||
// Notifications
|
||||
notifications: {
|
||||
rtl: false,
|
||||
|
13
package.json
13
package.json
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"global": "npm install -g @angular/cli marked node-gyp nodemon node-nightly npm-check-updates npm-run-all rimraf typescript ts-node typedoc webpack webpack-bundle-analyzer pm2 rollup",
|
||||
@@ -80,13 +80,18 @@
|
||||
"@angular/router": "^5.2.5",
|
||||
"@angularclass/bootloader": "1.0.1",
|
||||
"@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/router-store": "^5.0.1",
|
||||
"@ngrx/store": "^5.1.0",
|
||||
"@nguniversal/express-engine": "5.0.0-beta.5",
|
||||
"@nguniversal/express-engine": "5.0.0",
|
||||
"@ngx-translate/core": "9.1.1",
|
||||
"@ngx-translate/http-loader": "2.0.1",
|
||||
"@nicky-lenaers/ngx-scroll-to": "^0.6.0",
|
||||
"angular-idle-preload": "2.0.4",
|
||||
"angular-sortablejs": "^2.5.0",
|
||||
"angular2-text-mask": "8.0.4",
|
||||
"angulartics2": "^5.2.0",
|
||||
"body-parser": "1.18.2",
|
||||
"bootstrap": "^4.0.0",
|
||||
@@ -105,10 +110,14 @@
|
||||
"jwt-decode": "^2.2.0",
|
||||
"methods": "1.1.2",
|
||||
"morgan": "1.9.0",
|
||||
"ng2-file-upload": "1.2.1",
|
||||
"ngx-infinite-scroll": "0.8.2",
|
||||
"ngx-pagination": "3.0.3",
|
||||
"pem": "1.12.3",
|
||||
"reflect-metadata": "0.1.12",
|
||||
"rxjs": "5.5.6",
|
||||
"sortablejs": "1.7.0",
|
||||
"text-mask-core": "5.0.1",
|
||||
"ts-md5": "^1.2.4",
|
||||
"uuid": "^3.2.1",
|
||||
"webfontloader": "1.6.28",
|
||||
|
@@ -204,7 +204,28 @@
|
||||
"recent-submissions": "Error fetching recent submissions",
|
||||
"item": "Error fetching item",
|
||||
"objects": "Error fetching objects",
|
||||
"search-results": "Error fetching search results"
|
||||
"search-results": "Error fetching search results",
|
||||
"validation": {
|
||||
"pattern": "This input is restricted by the current pattern: {{ pattern }}.",
|
||||
"license": {
|
||||
"notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission."
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"login": {
|
||||
"title": "Login",
|
||||
|
@@ -3,6 +3,7 @@ import * as fromRouter from '@ngrx/router-store';
|
||||
|
||||
import { headerReducer, HeaderState } from './header/header.reducer';
|
||||
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
|
||||
import { formReducer, FormState } from './shared/form/form.reducer';
|
||||
import {
|
||||
SearchSidebarState,
|
||||
sidebarReducer
|
||||
@@ -18,6 +19,7 @@ export interface AppState {
|
||||
router: fromRouter.RouterReducerState;
|
||||
hostWindow: HostWindowState;
|
||||
header: HeaderState;
|
||||
forms: FormState;
|
||||
notifications: NotificationsState;
|
||||
searchSidebar: SearchSidebarState;
|
||||
searchFilter: SearchFiltersState;
|
||||
@@ -28,6 +30,7 @@ export const appReducers: ActionReducerMap<AppState> = {
|
||||
router: fromRouter.routerReducer,
|
||||
hostWindow: hostWindowReducer,
|
||||
header: headerReducer,
|
||||
forms: formReducer,
|
||||
notifications: notificationsReducer,
|
||||
searchSidebar: sidebarReducer,
|
||||
searchFilter: filterReducer,
|
||||
|
20
src/app/core/cache/response-cache.models.ts
vendored
20
src/app/core/cache/response-cache.models.ts
vendored
@@ -1,26 +1,25 @@
|
||||
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
|
||||
import { RequestError } from '../data/request.models';
|
||||
import { BrowseEntry } from '../shared/browse-entry.model';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||
import { ConfigObject } from '../shared/config/config.model';
|
||||
import { FacetValue } from '../../+search-page/search-service/facet-value.model';
|
||||
import { SearchFilterConfig } from '../../+search-page/search-service/search-filter-config.model';
|
||||
import { IntegrationModel } from '../integration/models/integration.model';
|
||||
import { RegistryMetadataschemasResponse } from '../registry/registry-metadataschemas-response.model';
|
||||
import { MetadataSchema } from '../metadata/metadataschema.model';
|
||||
import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model';
|
||||
import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model';
|
||||
import { AuthTokenInfo } from '../auth/models/auth-token-info.model';
|
||||
import { NormalizedAuthStatus } from '../auth/models/normalized-auth-status.model';
|
||||
import { AuthStatus } from '../auth/models/auth-status.model';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
export class RestResponse {
|
||||
public toCache = true;
|
||||
|
||||
constructor(
|
||||
public isSuccessful: boolean,
|
||||
public statusCode: string,
|
||||
) { }
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
export class DSOSuccessResponse extends RestResponse {
|
||||
@@ -158,6 +157,7 @@ export class ConfigSuccessResponse extends RestResponse {
|
||||
|
||||
export class AuthStatusResponse extends RestResponse {
|
||||
public toCache = false;
|
||||
|
||||
constructor(
|
||||
public response: AuthStatus,
|
||||
public statusCode: string
|
||||
@@ -166,4 +166,14 @@ export class AuthStatusResponse extends RestResponse {
|
||||
}
|
||||
}
|
||||
|
||||
export class IntegrationSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public dataDefinition: IntegrationModel[],
|
||||
public statusCode: string,
|
||||
public pageInfo?: PageInfo
|
||||
) {
|
||||
super(true, statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
@@ -8,6 +8,7 @@ import { CommonModule } from '@angular/common';
|
||||
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { DynamicFormLayoutService, DynamicFormService, DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
||||
|
||||
import { coreEffects } from './core.effects';
|
||||
import { coreReducers } from './core.reducers';
|
||||
@@ -22,6 +23,8 @@ import { DebugResponseParsingService } from './data/debug-response-parsing.servi
|
||||
import { DSOResponseParsingService } from './data/dso-response-parsing.service';
|
||||
import { SearchResponseParsingService } from './data/search-response-parsing.service';
|
||||
import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { FormBuilderService } from '../shared/form/builder/form-builder.service';
|
||||
import { FormService } from '../shared/form/form.service';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { ItemDataService } from './data/item-data.service';
|
||||
import { MetadataService } from './metadata/metadata.service';
|
||||
@@ -40,6 +43,8 @@ import { RouteService } from '../shared/services/route.service';
|
||||
import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service';
|
||||
import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
|
||||
import { SubmissionSectionsConfigService } from './config/submission-sections-config.service';
|
||||
import { AuthorityService } from './integration/authority.service';
|
||||
import { IntegrationResponseParsingService } from './integration/integration-response-parsing.service';
|
||||
import { UUIDService } from './shared/uuid.service';
|
||||
import { AuthenticatedGuard } from './auth/authenticated.guard';
|
||||
import { AuthRequestService } from './auth/auth-request.service';
|
||||
@@ -56,6 +61,7 @@ import { MetadataschemaParsingService } from './data/metadataschema-parsing.serv
|
||||
import { RegistryMetadatafieldsResponseParsingService } from './data/registry-metadatafields-response-parsing.service';
|
||||
import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service';
|
||||
import { NotificationsService } from '../shared/notifications/notifications.service';
|
||||
import { UploaderService } from '../shared/uploader/uploader.service';
|
||||
|
||||
const IMPORTS = [
|
||||
CommonModule,
|
||||
@@ -80,6 +86,11 @@ const PROVIDERS = [
|
||||
CollectionDataService,
|
||||
DSOResponseParsingService,
|
||||
DSpaceRESTv2Service,
|
||||
DynamicFormLayoutService,
|
||||
DynamicFormService,
|
||||
DynamicFormValidationService,
|
||||
FormBuilderService,
|
||||
FormService,
|
||||
HALEndpointService,
|
||||
HostWindowService,
|
||||
ItemDataService,
|
||||
@@ -109,6 +120,9 @@ const PROVIDERS = [
|
||||
SubmissionDefinitionsConfigService,
|
||||
SubmissionFormsConfigService,
|
||||
SubmissionSectionsConfigService,
|
||||
AuthorityService,
|
||||
IntegrationResponseParsingService,
|
||||
UploaderService,
|
||||
UUIDService,
|
||||
// register AuthInterceptor as HttpInterceptor
|
||||
{
|
||||
|
@@ -27,7 +27,7 @@ export class ConfigResponseParsingService extends BaseResponseParsingService imp
|
||||
}
|
||||
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && data.statusCode === '200') {
|
||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '201' || data.statusCode === '200' || data.statusCode === 'OK')) {
|
||||
const configDefinition = this.process<ConfigObject,ConfigType>(data.payload, request.href);
|
||||
return new ConfigSuccessResponse(configDefinition[Object.keys(configDefinition)[0]], data.statusCode, this.processPageInfo(data.payload));
|
||||
} else {
|
||||
|
@@ -11,6 +11,7 @@ import { ConfigResponseParsingService } from './config-response-parsing.service'
|
||||
import { AuthResponseParsingService } from '../auth/auth-response-parsing.service';
|
||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
|
||||
@@ -212,6 +213,15 @@ export class AuthGetRequest extends GetRequest {
|
||||
}
|
||||
}
|
||||
|
||||
export class IntegrationRequest extends GetRequest {
|
||||
constructor(uuid: string, href: string) {
|
||||
super(uuid, href);
|
||||
}
|
||||
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
return IntegrationResponseParsingService;
|
||||
}
|
||||
}
|
||||
export class RequestError extends Error {
|
||||
statusText: string;
|
||||
}
|
||||
|
19
src/app/core/integration/authority.service.ts
Normal file
19
src/app/core/integration/authority.service.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { IntegrationService } from './integration.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthorityService extends IntegrationService {
|
||||
protected linkPath = 'authorities';
|
||||
protected browseEndpoint = 'entries';
|
||||
|
||||
constructor(
|
||||
protected responseCache: ResponseCacheService,
|
||||
protected requestService: RequestService,
|
||||
protected halService: HALEndpointService) {
|
||||
super();
|
||||
}
|
||||
}
|
12
src/app/core/integration/integration-data.ts
Normal file
12
src/app/core/integration/integration-data.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { IntegrationModel } from './models/integration.model';
|
||||
|
||||
/**
|
||||
* A class to represent the data retrieved by an Integration service
|
||||
*/
|
||||
export class IntegrationData {
|
||||
constructor(
|
||||
public pageInfo: PageInfo,
|
||||
public payload: IntegrationModel[]
|
||||
) { }
|
||||
}
|
17
src/app/core/integration/integration-object-factory.ts
Normal file
17
src/app/core/integration/integration-object-factory.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { GenericConstructor } from '../shared/generic-constructor';
|
||||
import { IntegrationType } from './intergration-type';
|
||||
import { AuthorityValueModel } from './models/authority-value.model';
|
||||
import { IntegrationModel } from './models/integration.model';
|
||||
|
||||
export class IntegrationObjectFactory {
|
||||
public static getConstructor(type): GenericConstructor<IntegrationModel> {
|
||||
switch (type) {
|
||||
case IntegrationType.Authority: {
|
||||
return AuthorityValueModel;
|
||||
}
|
||||
default: {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,196 @@
|
||||
import { ErrorResponse, IntegrationSuccessResponse } from '../cache/response-cache.models';
|
||||
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||
|
||||
import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { IntegrationResponseParsingService } from './integration-response-parsing.service';
|
||||
import { IntegrationRequest } from '../data/request.models';
|
||||
import { AuthorityValueModel } from './models/authority-value.model';
|
||||
|
||||
describe('IntegrationResponseParsingService', () => {
|
||||
let service: IntegrationResponseParsingService;
|
||||
|
||||
const EnvConfig = {} as GlobalConfig;
|
||||
const store = {} as Store<CoreState>;
|
||||
const objectCacheService = new ObjectCacheService(store);
|
||||
const name = 'type';
|
||||
const metadata = 'dc.type';
|
||||
const query = '';
|
||||
const uuid = 'd9d30c0c-69b7-4369-8397-ca67c888974d';
|
||||
const integrationEndpoint = 'https://rest.api/integration/authorities';
|
||||
const entriesEndpoint = `${integrationEndpoint}/${name}/entries?query=${query}&metadata=${metadata}&uuid=${uuid}`;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new IntegrationResponseParsingService(EnvConfig, objectCacheService);
|
||||
});
|
||||
|
||||
describe('parse', () => {
|
||||
const validRequest = new IntegrationRequest('69f375b5-19f4-4453-8c7a-7dc5c55aafbb', entriesEndpoint);
|
||||
|
||||
const validResponse = {
|
||||
payload: {
|
||||
page: {
|
||||
number: 0,
|
||||
size: 5,
|
||||
totalElements: 5,
|
||||
totalPages: 1
|
||||
},
|
||||
_embedded: {
|
||||
authorityEntries: [
|
||||
{
|
||||
display: 'One',
|
||||
id: 'One',
|
||||
otherInformation: {},
|
||||
type: 'authority',
|
||||
value: 'One'
|
||||
},
|
||||
{
|
||||
display: 'Two',
|
||||
id: 'Two',
|
||||
otherInformation: {},
|
||||
type: 'authority',
|
||||
value: 'Two'
|
||||
},
|
||||
{
|
||||
display: 'Three',
|
||||
id: 'Three',
|
||||
otherInformation: {},
|
||||
type: 'authority',
|
||||
value: 'Three'
|
||||
},
|
||||
{
|
||||
display: 'Four',
|
||||
id: 'Four',
|
||||
otherInformation: {},
|
||||
type: 'authority',
|
||||
value: 'Four'
|
||||
},
|
||||
{
|
||||
display: 'Five',
|
||||
id: 'Five',
|
||||
otherInformation: {},
|
||||
type: 'authority',
|
||||
value: 'Five'
|
||||
},
|
||||
],
|
||||
|
||||
},
|
||||
_links: {
|
||||
self: 'https://rest.api/integration/authorities/type/entries'
|
||||
}
|
||||
},
|
||||
statusCode: '200'
|
||||
};
|
||||
|
||||
const invalidResponse1 = {
|
||||
payload: {},
|
||||
statusCode: '200'
|
||||
};
|
||||
|
||||
const invalidResponse2 = {
|
||||
payload: {
|
||||
page: {
|
||||
number: 0,
|
||||
size: 5,
|
||||
totalElements: 5,
|
||||
totalPages: 1
|
||||
},
|
||||
_embedded: {
|
||||
authorityEntries: [
|
||||
{
|
||||
display: 'One',
|
||||
id: 'One',
|
||||
otherInformation: {},
|
||||
type: 'authority',
|
||||
value: 'One'
|
||||
},
|
||||
{
|
||||
display: 'Two',
|
||||
id: 'Two',
|
||||
otherInformation: {},
|
||||
type: 'authority',
|
||||
value: 'Two'
|
||||
},
|
||||
{
|
||||
display: 'Three',
|
||||
id: 'Three',
|
||||
otherInformation: {},
|
||||
type: 'authority',
|
||||
value: 'Three'
|
||||
},
|
||||
{
|
||||
display: 'Four',
|
||||
id: 'Four',
|
||||
otherInformation: {},
|
||||
type: 'authority',
|
||||
value: 'Four'
|
||||
},
|
||||
{
|
||||
display: 'Five',
|
||||
id: 'Five',
|
||||
otherInformation: {},
|
||||
type: 'authority',
|
||||
value: 'Five'
|
||||
},
|
||||
],
|
||||
|
||||
},
|
||||
_links: {}
|
||||
},
|
||||
statusCode: '200'
|
||||
};
|
||||
|
||||
const definitions = [
|
||||
Object.assign(new AuthorityValueModel(), {
|
||||
display: 'One',
|
||||
id: 'One',
|
||||
otherInformation: {},
|
||||
value: 'One'
|
||||
}),
|
||||
Object.assign(new AuthorityValueModel(), {
|
||||
display: 'Two',
|
||||
id: 'Two',
|
||||
otherInformation: {},
|
||||
value: 'Two'
|
||||
}),
|
||||
Object.assign(new AuthorityValueModel(), {
|
||||
display: 'Three',
|
||||
id: 'Three',
|
||||
otherInformation: {},
|
||||
value: 'Three'
|
||||
}),
|
||||
Object.assign(new AuthorityValueModel(), {
|
||||
display: 'Four',
|
||||
id: 'Four',
|
||||
otherInformation: {},
|
||||
value: 'Four'
|
||||
}),
|
||||
Object.assign(new AuthorityValueModel(), {
|
||||
display: 'Five',
|
||||
id: 'Five',
|
||||
otherInformation: {},
|
||||
value: 'Five'
|
||||
})
|
||||
];
|
||||
|
||||
it('should return a IntegrationSuccessResponse if data contains a valid endpoint response', () => {
|
||||
const response = service.parse(validRequest, validResponse);
|
||||
expect(response.constructor).toBe(IntegrationSuccessResponse);
|
||||
});
|
||||
|
||||
it('should return an ErrorResponse if data contains an invalid config endpoint response', () => {
|
||||
const response1 = service.parse(validRequest, invalidResponse1);
|
||||
const response2 = service.parse(validRequest, invalidResponse2);
|
||||
expect(response1.constructor).toBe(ErrorResponse);
|
||||
expect(response2.constructor).toBe(ErrorResponse);
|
||||
});
|
||||
|
||||
it('should return a IntegrationSuccessResponse with data definition', () => {
|
||||
const response = service.parse(validRequest, validResponse);
|
||||
expect((response as any).dataDefinition).toEqual(definitions);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
@@ -0,0 +1,47 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { RestRequest } from '../data/request.models';
|
||||
import { ResponseParsingService } from '../data/parsing.service';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import {
|
||||
ErrorResponse,
|
||||
IntegrationSuccessResponse,
|
||||
RestResponse
|
||||
} from '../cache/response-cache.models';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { IntegrationObjectFactory } from './integration-object-factory';
|
||||
|
||||
import { BaseResponseParsingService } from '../data/base-response-parsing.service';
|
||||
import { GLOBAL_CONFIG } from '../../../config';
|
||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { IntegrationModel } from './models/integration.model';
|
||||
import { IntegrationType } from './intergration-type';
|
||||
|
||||
@Injectable()
|
||||
export class IntegrationResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
|
||||
|
||||
protected objectFactory = IntegrationObjectFactory;
|
||||
protected toCache = false;
|
||||
|
||||
constructor(
|
||||
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||
protected objectCache: ObjectCacheService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) {
|
||||
const dataDefinition = this.process<IntegrationModel,IntegrationType>(data.payload, request.href);
|
||||
return new IntegrationSuccessResponse(dataDefinition[Object.keys(dataDefinition)[0]], data.statusCode, this.processPageInfo(data.payload.page));
|
||||
} else {
|
||||
return new ErrorResponse(
|
||||
Object.assign(
|
||||
new Error('Unexpected response from Integration endpoint'),
|
||||
{statusText: data.statusCode}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
83
src/app/core/integration/integration.service.spec.ts
Normal file
83
src/app/core/integration/integration.service.spec.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { cold, getTestScheduler } from 'jasmine-marbles';
|
||||
import { TestScheduler } from 'rxjs/Rx';
|
||||
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { IntegrationRequest } from '../data/request.models';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
||||
import { IntegrationService } from './integration.service';
|
||||
import { IntegrationSearchOptions } from './models/integration-options.model';
|
||||
|
||||
const LINK_NAME = 'authorities';
|
||||
const BROWSE = 'entries';
|
||||
|
||||
class TestService extends IntegrationService {
|
||||
protected linkPath = LINK_NAME;
|
||||
protected browseEndpoint = BROWSE;
|
||||
|
||||
constructor(
|
||||
protected responseCache: ResponseCacheService,
|
||||
protected requestService: RequestService,
|
||||
protected halService: HALEndpointService) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
describe('IntegrationService', () => {
|
||||
let scheduler: TestScheduler;
|
||||
let service: TestService;
|
||||
let responseCache: ResponseCacheService;
|
||||
let requestService: RequestService;
|
||||
let halService: any;
|
||||
let findOptions: IntegrationSearchOptions;
|
||||
|
||||
const name = 'type';
|
||||
const metadata = 'dc.type';
|
||||
const query = '';
|
||||
const uuid = 'd9d30c0c-69b7-4369-8397-ca67c888974d';
|
||||
const integrationEndpoint = 'https://rest.api/integration';
|
||||
const serviceEndpoint = `${integrationEndpoint}/${LINK_NAME}`;
|
||||
const entriesEndpoint = `${serviceEndpoint}/${name}/entries?query=${query}&metadata=${metadata}&uuid=${uuid}`;
|
||||
|
||||
findOptions = new IntegrationSearchOptions(uuid, name, metadata);
|
||||
|
||||
function initMockResponseCacheService(isSuccessful: boolean): ResponseCacheService {
|
||||
return jasmine.createSpyObj('responseCache', {
|
||||
get: cold('c-', {
|
||||
c: {response: {isSuccessful}}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function initTestService(): TestService {
|
||||
return new TestService(
|
||||
responseCache,
|
||||
requestService,
|
||||
halService
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
responseCache = initMockResponseCacheService(true);
|
||||
requestService = getMockRequestService();
|
||||
scheduler = getTestScheduler();
|
||||
halService = new HALEndpointServiceStub(integrationEndpoint);
|
||||
findOptions = new IntegrationSearchOptions(uuid, name, metadata, query);
|
||||
service = initTestService();
|
||||
|
||||
});
|
||||
|
||||
describe('getEntriesByName', () => {
|
||||
|
||||
it('should configure a new IntegrationRequest', () => {
|
||||
const expected = new IntegrationRequest(requestService.generateRequestId(), entriesEndpoint);
|
||||
scheduler.schedule(() => service.getEntriesByName(findOptions).subscribe());
|
||||
scheduler.flush();
|
||||
|
||||
expect(requestService.configure).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
85
src/app/core/integration/integration.service.ts
Normal file
85
src/app/core/integration/integration.service.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||
import { ErrorResponse, IntegrationSuccessResponse, RestResponse } from '../cache/response-cache.models';
|
||||
import { GetRequest, IntegrationRequest } from '../data/request.models';
|
||||
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { IntegrationData } from './integration-data';
|
||||
import { IntegrationSearchOptions } from './models/integration-options.model';
|
||||
|
||||
export abstract class IntegrationService {
|
||||
protected request: IntegrationRequest;
|
||||
protected abstract responseCache: ResponseCacheService;
|
||||
protected abstract requestService: RequestService;
|
||||
protected abstract linkPath: string;
|
||||
protected abstract browseEndpoint: string;
|
||||
protected abstract halService: HALEndpointService;
|
||||
|
||||
protected getData(request: GetRequest): Observable<IntegrationData> {
|
||||
const [successResponse, errorResponse] = this.responseCache.get(request.href)
|
||||
.map((entry: ResponseCacheEntry) => entry.response)
|
||||
.partition((response: RestResponse) => response.isSuccessful);
|
||||
return Observable.merge(
|
||||
errorResponse.flatMap((response: ErrorResponse) =>
|
||||
Observable.throw(new Error(`Couldn't retrieve the integration data`))),
|
||||
successResponse
|
||||
.filter((response: IntegrationSuccessResponse) => isNotEmpty(response))
|
||||
.map((response: IntegrationSuccessResponse) => new IntegrationData(response.pageInfo, response.dataDefinition))
|
||||
.distinctUntilChanged());
|
||||
}
|
||||
|
||||
protected getIntegrationHref(endpoint, options: IntegrationSearchOptions = new IntegrationSearchOptions()): string {
|
||||
let result;
|
||||
const args = [];
|
||||
|
||||
if (hasValue(options.name)) {
|
||||
result = `${endpoint}/${options.name}/${this.browseEndpoint}`;
|
||||
} else {
|
||||
result = endpoint;
|
||||
}
|
||||
|
||||
if (hasValue(options.query)) {
|
||||
args.push(`query=${options.query}`);
|
||||
}
|
||||
|
||||
if (hasValue(options.metadata)) {
|
||||
args.push(`metadata=${options.metadata}`);
|
||||
}
|
||||
|
||||
if (hasValue(options.uuid)) {
|
||||
args.push(`uuid=${options.uuid}`);
|
||||
}
|
||||
|
||||
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
|
||||
/* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */
|
||||
args.push(`page=${options.currentPage - 1}`);
|
||||
}
|
||||
|
||||
if (hasValue(options.elementsPerPage)) {
|
||||
args.push(`size=${options.elementsPerPage}`);
|
||||
}
|
||||
|
||||
if (hasValue(options.sort)) {
|
||||
args.push(`sort=${options.sort.field},${options.sort.direction}`);
|
||||
}
|
||||
|
||||
if (isNotEmpty(args)) {
|
||||
result = `${result}?${args.join('&')}`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public getEntriesByName(options: IntegrationSearchOptions): Observable<IntegrationData> {
|
||||
return this.halService.getEndpoint(this.linkPath)
|
||||
.map((endpoint: string) => this.getIntegrationHref(endpoint, options))
|
||||
.filter((href: string) => isNotEmpty(href))
|
||||
.distinctUntilChanged()
|
||||
.map((endpointURL: string) => new IntegrationRequest(this.requestService.generateRequestId(), endpointURL))
|
||||
.do((request: GetRequest) => this.requestService.configure(request))
|
||||
.flatMap((request: GetRequest) => this.getData(request))
|
||||
.distinctUntilChanged();
|
||||
}
|
||||
|
||||
}
|
4
src/app/core/integration/intergration-type.ts
Normal file
4
src/app/core/integration/intergration-type.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
export enum IntegrationType {
|
||||
Authority = 'authority'
|
||||
}
|
16
src/app/core/integration/models/authority-options.model.ts
Normal file
16
src/app/core/integration/models/authority-options.model.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export class AuthorityOptions {
|
||||
name: string;
|
||||
metadata: string;
|
||||
scope: string;
|
||||
closed: boolean;
|
||||
|
||||
constructor(name: string,
|
||||
metadata: string,
|
||||
scope: string,
|
||||
closed: boolean = false) {
|
||||
this.name = name;
|
||||
this.metadata = metadata;
|
||||
this.scope = scope;
|
||||
this.closed = closed;
|
||||
}
|
||||
}
|
20
src/app/core/integration/models/authority-value.model.ts
Normal file
20
src/app/core/integration/models/authority-value.model.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { IntegrationModel } from './integration.model';
|
||||
import { autoserialize } from 'cerialize';
|
||||
|
||||
export class AuthorityValueModel extends IntegrationModel {
|
||||
|
||||
@autoserialize
|
||||
id: string;
|
||||
|
||||
@autoserialize
|
||||
display: string;
|
||||
|
||||
@autoserialize
|
||||
value: string;
|
||||
|
||||
@autoserialize
|
||||
otherInformation: any;
|
||||
|
||||
@autoserialize
|
||||
language: string;
|
||||
}
|
14
src/app/core/integration/models/integration-options.model.ts
Normal file
14
src/app/core/integration/models/integration-options.model.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { SortOptions } from '../../cache/models/sort-options.model';
|
||||
|
||||
export class IntegrationSearchOptions {
|
||||
|
||||
constructor(public uuid: string = '',
|
||||
public name: string = '',
|
||||
public metadata: string = '',
|
||||
public query: string = '',
|
||||
public elementsPerPage?: number,
|
||||
public currentPage?: number,
|
||||
public sort?: SortOptions) {
|
||||
|
||||
}
|
||||
}
|
12
src/app/core/integration/models/integration.model.ts
Normal file
12
src/app/core/integration/models/integration.model.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { autoserialize } from 'cerialize';
|
||||
|
||||
export abstract class IntegrationModel {
|
||||
|
||||
@autoserialize
|
||||
public type: string;
|
||||
|
||||
@autoserialize
|
||||
public _links: {
|
||||
[name: string]: string
|
||||
}
|
||||
}
|
23
src/app/core/shared/config/config-authority.model.ts
Normal file
23
src/app/core/shared/config/config-authority.model.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
|
||||
import { ConfigObject } from './config.model';
|
||||
import { SubmissionSectionModel } from './config-submission-section.model';
|
||||
|
||||
@inheritSerialization(ConfigObject)
|
||||
export class ConfigAuthorityModel extends ConfigObject {
|
||||
|
||||
@autoserialize
|
||||
id: string;
|
||||
|
||||
@autoserialize
|
||||
display: string;
|
||||
|
||||
@autoserialize
|
||||
value: string;
|
||||
|
||||
@autoserialize
|
||||
otherInformation: any;
|
||||
|
||||
@autoserialize
|
||||
language: string;
|
||||
|
||||
}
|
@@ -6,6 +6,7 @@ import { SubmissionFormsModel } from './config-submission-forms.model';
|
||||
import { SubmissionDefinitionsModel } from './config-submission-definitions.model';
|
||||
import { ConfigType } from './config-type';
|
||||
import { ConfigObject } from './config.model';
|
||||
import { ConfigAuthorityModel } from './config-authority.model';
|
||||
|
||||
export class ConfigObjectFactory {
|
||||
public static getConstructor(type): GenericConstructor<ConfigObject> {
|
||||
@@ -22,6 +23,9 @@ export class ConfigObjectFactory {
|
||||
case ConfigType.SubmissionSections: {
|
||||
return SubmissionSectionModel
|
||||
}
|
||||
case ConfigType.Authority: {
|
||||
return ConfigAuthorityModel
|
||||
}
|
||||
default: {
|
||||
return undefined;
|
||||
}
|
||||
|
@@ -1,10 +1,14 @@
|
||||
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
|
||||
import { autoserialize, inheritSerialization } from 'cerialize';
|
||||
import { ConfigObject } from './config.model';
|
||||
import { FormFieldModel } from '../../../shared/form/builder/models/form-field.model';
|
||||
|
||||
export interface FormRowModel {
|
||||
fields: FormFieldModel[];
|
||||
}
|
||||
|
||||
@inheritSerialization(ConfigObject)
|
||||
export class SubmissionFormsModel extends ConfigObject {
|
||||
|
||||
@autoserialize
|
||||
fields: any[];
|
||||
|
||||
rows: FormRowModel[];
|
||||
}
|
||||
|
@@ -10,5 +10,6 @@ export enum ConfigType {
|
||||
SubmissionForm = 'submissionform',
|
||||
SubmissionForms = 'submissionforms',
|
||||
SubmissionSections = 'submissionsections',
|
||||
SubmissionSection = 'submissionsection'
|
||||
SubmissionSection = 'submissionsection',
|
||||
Authority = 'authority'
|
||||
}
|
||||
|
13
src/app/shared/animations/shrink.ts
Normal file
13
src/app/shared/animations/shrink.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
|
||||
export const shrinkInOut = trigger('shrinkInOut', [
|
||||
state('in', style({height: '100%', opacity: 1})),
|
||||
transition('* => void', [
|
||||
style({height: '!', opacity: 1}),
|
||||
animate(200, style({height: 0, opacity: 0}))
|
||||
]),
|
||||
transition('void => *', [
|
||||
style({height: 0, opacity: 0}),
|
||||
animate(200, style({height: '*', opacity: 1}))
|
||||
])
|
||||
]);
|
34
src/app/shared/chips/chips.component.html
Normal file
34
src/app/shared/chips/chips.component.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<div [className]="'float-left w-100 ' + wrapperClass">
|
||||
<ul class="nav nav-pills d-flex flex-column flex-sm-row" [sortablejs]="chips.getChips()" [sortablejsOptions]="options">
|
||||
<ng-container *ngFor="let c of chips.getChips(); let i = index">
|
||||
<ng-template #tipContent>{{tipText}}</ng-template>
|
||||
<li class="nav-item mr-2 mb-1"
|
||||
(dragstart)="onDragStart(i)"
|
||||
(dragend)="onDragEnd(i)">
|
||||
<a class="flex-sm-fill text-sm-center nav-link active"
|
||||
href="#"
|
||||
[ngClass]="{'chip-selected disabled': (editable && c.editMode) || dragged == i}"
|
||||
(click)="chipsSelected($event, i);">
|
||||
<span>
|
||||
<ng-container *ngIf="c.hasIcons()">
|
||||
<i *ngFor="let icon of c.icons; let l = last"
|
||||
[ngbTooltip]="tipContent"
|
||||
triggers="manual"
|
||||
#t="ngbTooltip"
|
||||
class="fa {{icon.style}}"
|
||||
[class.mr-1]="!l"
|
||||
[class.mr-2]="l"
|
||||
aria-hidden="true"
|
||||
(dragstart)="tooltip.close();"
|
||||
(mouseover)="showTooltip(t, i, icon.metadata)"
|
||||
(mouseout)="t.close()"></i>
|
||||
</ng-container>
|
||||
<p class="chip-label text-truncate d-table-cell">{{c.display}}</p><i class="fa fa-times ml-2" (click)="removeChips($event, i)"></i>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ng-container>
|
||||
|
||||
<ng-content></ng-content>
|
||||
</ul>
|
||||
</div>
|
9
src/app/shared/chips/chips.component.scss
Normal file
9
src/app/shared/chips/chips.component.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
@import "../../../styles/variables";
|
||||
|
||||
.chip-selected {
|
||||
background-color: map-get($theme-colors, info) !important;
|
||||
}
|
||||
|
||||
.chip-label {
|
||||
max-width: 10rem;
|
||||
}
|
227
src/app/shared/chips/chips.component.spec.ts
Normal file
227
src/app/shared/chips/chips.component.spec.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
// Load the implementations that should be tested
|
||||
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick, } from '@angular/core/testing';
|
||||
import 'rxjs/add/observable/of';
|
||||
|
||||
import { Chips } from './models/chips.model';
|
||||
import { UploaderService } from '../uploader/uploader.service';
|
||||
import { ChipsComponent } from './chips.component';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { SortablejsModule } from 'angular-sortablejs';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { FormFieldMetadataValueObject } from '../form/builder/models/form-field-metadata-value.model';
|
||||
import { createTestComponent, hasClass } from '../testing/utils';
|
||||
|
||||
describe('ChipsComponent test suite', () => {
|
||||
|
||||
let testComp: TestComponent;
|
||||
let chipsComp: ChipsComponent;
|
||||
let testFixture: ComponentFixture<TestComponent>;
|
||||
let chipsFixture: ComponentFixture<ChipsComponent>;
|
||||
let html;
|
||||
let chips: Chips;
|
||||
|
||||
// async beforeEach
|
||||
beforeEach(async(() => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
NgbModule.forRoot(),
|
||||
SortablejsModule.forRoot({animation: 150}),
|
||||
],
|
||||
declarations: [
|
||||
ChipsComponent,
|
||||
TestComponent,
|
||||
], // declare the test component
|
||||
providers: [
|
||||
ChangeDetectorRef,
|
||||
ChipsComponent,
|
||||
UploaderService
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
describe('', () => {
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
html = `
|
||||
<ds-chips
|
||||
*ngIf="chips.hasItems()"
|
||||
[chips]="chips"
|
||||
[editable]="editable"
|
||||
(selected)="onChipSelected($event)"></ds-chips>`;
|
||||
|
||||
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||
testComp = testFixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create Chips Component', inject([ChipsComponent], (app: ChipsComponent) => {
|
||||
expect(app).toBeDefined();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when has items as string', () => {
|
||||
beforeEach(() => {
|
||||
chips = new Chips(['a', 'b', 'c']);
|
||||
chipsFixture = TestBed.createComponent(ChipsComponent);
|
||||
chipsComp = chipsFixture.componentInstance; // TruncatableComponent test instance
|
||||
chipsComp.editable = true;
|
||||
chipsComp.chips = chips;
|
||||
chipsFixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
chipsFixture.destroy();
|
||||
chipsComp = null;
|
||||
});
|
||||
|
||||
it('should set edit mode when a chip item is selected', fakeAsync(() => {
|
||||
|
||||
spyOn(chipsComp.selected, 'emit');
|
||||
|
||||
chipsComp.chipsSelected(new Event('click'), 1);
|
||||
chipsFixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const item = chipsComp.chips.getChipByIndex(1);
|
||||
|
||||
expect(item.editMode).toBe(true);
|
||||
expect(chipsComp.selected.emit).toHaveBeenCalledWith(1);
|
||||
}));
|
||||
|
||||
it('should not set edit mode when a chip item is selected and editable is false', fakeAsync(() => {
|
||||
chipsComp.editable = false;
|
||||
spyOn(chipsComp.selected, 'emit');
|
||||
|
||||
chipsComp.chipsSelected(new Event('click'), 1);
|
||||
chipsFixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const item = chipsComp.chips.getChipByIndex(1);
|
||||
|
||||
expect(item.editMode).toBe(false);
|
||||
expect(chipsComp.selected.emit).not.toHaveBeenCalledWith(1);
|
||||
}));
|
||||
|
||||
it('should emit when a chip item is removed and editable is true', fakeAsync(() => {
|
||||
|
||||
spyOn(chipsComp.chips, 'remove');
|
||||
|
||||
const item = chipsComp.chips.getChipByIndex(1);
|
||||
|
||||
chipsComp.removeChips(new Event('click'), 1);
|
||||
chipsFixture.detectChanges();
|
||||
tick();
|
||||
|
||||
expect(chipsComp.chips.remove).toHaveBeenCalledWith(item);
|
||||
}));
|
||||
|
||||
it('should save chips item index when drag and drop start', fakeAsync(() => {
|
||||
const de = chipsFixture.debugElement.query(By.css('li.nav-item'));
|
||||
|
||||
de.triggerEventHandler('dragstart', null);
|
||||
|
||||
expect(chipsComp.dragged).toBe(0);
|
||||
}));
|
||||
|
||||
it('should update chips item order when drag and drop end', fakeAsync(() => {
|
||||
spyOn(chipsComp.chips, 'updateOrder');
|
||||
const de = chipsFixture.debugElement.query(By.css('li.nav-item'));
|
||||
|
||||
de.triggerEventHandler('dragend', null);
|
||||
|
||||
expect(chipsComp.dragged).toBe(-1);
|
||||
expect(chipsComp.chips.updateOrder).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when has items as object', () => {
|
||||
beforeEach(() => {
|
||||
const item = {
|
||||
mainField: new FormFieldMetadataValueObject('main test', null, 'test001'),
|
||||
relatedField: new FormFieldMetadataValueObject('related test', null, 'test002'),
|
||||
otherRelatedField: new FormFieldMetadataValueObject('other related test')
|
||||
};
|
||||
const iconsConfig = [
|
||||
{
|
||||
name: 'mainField',
|
||||
config: {
|
||||
withAuthority:{
|
||||
style: 'fa-user'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'relatedField',
|
||||
config: {
|
||||
withAuthority:{
|
||||
style: 'fa-user-alt'
|
||||
},
|
||||
withoutAuthority:{
|
||||
style: 'fa-user-alt text-muted'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'otherRelatedField',
|
||||
config: {
|
||||
withAuthority:{
|
||||
style: 'fa-user-alt'
|
||||
},
|
||||
withoutAuthority:{
|
||||
style: 'fa-user-alt text-muted'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'default',
|
||||
config: {}
|
||||
}
|
||||
];
|
||||
|
||||
chips = new Chips([item], 'display', 'mainField', iconsConfig);
|
||||
chipsFixture = TestBed.createComponent(ChipsComponent);
|
||||
chipsComp = chipsFixture.componentInstance; // TruncatableComponent test instance
|
||||
chipsComp.editable = true;
|
||||
chipsComp.chips = chips;
|
||||
chipsFixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show icon for every field that has a configured icon', () => {
|
||||
const de = chipsFixture.debugElement.query(By.css('li.nav-item'));
|
||||
const icons = de.queryAll(By.css('i.fa'));
|
||||
|
||||
expect(icons.length).toBe(4);
|
||||
|
||||
});
|
||||
|
||||
it('should has text-muted on icon style when field value had not authority', () => {
|
||||
const de = chipsFixture.debugElement.query(By.css('li.nav-item'));
|
||||
const icons = de.queryAll(By.css('i.fa'));
|
||||
|
||||
expect(hasClass(icons[2].nativeElement, 'text-muted')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should show tooltip on mouse over an icon', () => {
|
||||
const de = chipsFixture.debugElement.query(By.css('li.nav-item'));
|
||||
const icons = de.queryAll(By.css('i.fa'));
|
||||
|
||||
icons[0].triggerEventHandler('mouseover', null);
|
||||
|
||||
expect(chipsComp.tipText).toBe('main test')
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// declare a test component
|
||||
@Component({
|
||||
selector: 'ds-test-cmp',
|
||||
template: ``
|
||||
})
|
||||
class TestComponent {
|
||||
|
||||
public chips = new Chips(['a', 'b', 'c']);
|
||||
public editable = true;
|
||||
}
|
95
src/app/shared/chips/chips.component.ts
Normal file
95
src/app/shared/chips/chips.component.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, } from '@angular/core';
|
||||
|
||||
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { SortablejsOptions } from 'angular-sortablejs';
|
||||
import { isObject } from 'lodash';
|
||||
|
||||
import { Chips } from './models/chips.model';
|
||||
import { ChipsItem } from './models/chips-item.model';
|
||||
import { UploaderService } from '../uploader/uploader.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-chips',
|
||||
styleUrls: ['./chips.component.scss'],
|
||||
templateUrl: './chips.component.html',
|
||||
})
|
||||
|
||||
export class ChipsComponent implements OnChanges {
|
||||
@Input() chips: Chips;
|
||||
@Input() wrapperClass: string;
|
||||
@Input() editable = true;
|
||||
|
||||
@Output() selected: EventEmitter<number> = new EventEmitter<number>();
|
||||
@Output() remove: EventEmitter<number> = new EventEmitter<number>();
|
||||
@Output() change: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
options: SortablejsOptions;
|
||||
dragged = -1;
|
||||
tipText: string;
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef, private uploaderService: UploaderService) {
|
||||
this.options = {
|
||||
animation: 300,
|
||||
chosenClass: 'm-0',
|
||||
dragClass: 'm-0',
|
||||
filter: '.chips-sort-ignore',
|
||||
ghostClass: 'm-0'
|
||||
};
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes.chips && !changes.chips.isFirstChange()) {
|
||||
this.chips = changes.chips.currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
chipsSelected(event: Event, index: number) {
|
||||
event.preventDefault();
|
||||
if (this.editable) {
|
||||
this.chips.getChips().forEach((item: ChipsItem, i: number) => {
|
||||
if (i === index) {
|
||||
item.setEditMode();
|
||||
} else {
|
||||
item.unsetEditMode();
|
||||
}
|
||||
});
|
||||
this.selected.emit(index);
|
||||
}
|
||||
}
|
||||
|
||||
removeChips(event: Event, index: number) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
// Can't remove if this element is in editMode
|
||||
if (!this.chips.getChipByIndex(index).editMode) {
|
||||
this.chips.remove(this.chips.getChipByIndex(index));
|
||||
}
|
||||
}
|
||||
|
||||
onDragStart(index) {
|
||||
this.uploaderService.overrideDragOverPage();
|
||||
this.dragged = index;
|
||||
}
|
||||
|
||||
onDragEnd(event) {
|
||||
this.uploaderService.allowDragOverPage();
|
||||
this.dragged = -1;
|
||||
this.chips.updateOrder();
|
||||
}
|
||||
|
||||
showTooltip(tooltip: NgbTooltip, index, field?) {
|
||||
tooltip.close();
|
||||
const item = this.chips.getChipByIndex(index);
|
||||
if (!item.editMode && this.dragged === -1) {
|
||||
if (field) {
|
||||
this.tipText = (isObject(item.item[field])) ? item.item[field].display : item.item[field];
|
||||
} else {
|
||||
this.tipText = item.display;
|
||||
}
|
||||
|
||||
this.cdr.detectChanges();
|
||||
tooltip.open();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
78
src/app/shared/chips/models/chips-item.model.spec.ts
Normal file
78
src/app/shared/chips/models/chips-item.model.spec.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ChipsItem, ChipsItemIcon } from './chips-item.model';
|
||||
import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model';
|
||||
|
||||
describe('ChipsItem model test suite', () => {
|
||||
let item: ChipsItem;
|
||||
|
||||
beforeEach(() => {
|
||||
item = new ChipsItem('a');
|
||||
});
|
||||
|
||||
it('should init ChipsItem object properly', () => {
|
||||
expect(item.item).toBe('a');
|
||||
expect(item.display).toBe('a');
|
||||
expect(item.editMode).toBe(false);
|
||||
expect(item.icons).toEqual([]);
|
||||
});
|
||||
|
||||
it('should update item', () => {
|
||||
item.updateItem('b');
|
||||
|
||||
expect(item.item).toBe('b');
|
||||
});
|
||||
|
||||
it('should set editMode', () => {
|
||||
item.setEditMode();
|
||||
|
||||
expect(item.editMode).toBe(true);
|
||||
});
|
||||
|
||||
it('should unset editMode', () => {
|
||||
item.unsetEditMode();
|
||||
|
||||
expect(item.editMode).toBe(false);
|
||||
});
|
||||
|
||||
it('should update icons', () => {
|
||||
const icons: ChipsItemIcon[] = [{metadata: 'test', hasAuthority: false, style: 'fa fa-plus'}];
|
||||
item.updateIcons(icons);
|
||||
|
||||
expect(item.icons).toEqual(icons);
|
||||
});
|
||||
|
||||
it('should return true if has icons', () => {
|
||||
const icons: ChipsItemIcon[] = [{metadata: 'test', hasAuthority: false, style: 'fa fa-plus'}];
|
||||
item.updateIcons(icons);
|
||||
const hasIcons = item.hasIcons();
|
||||
|
||||
expect(hasIcons).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if has not icons', () => {
|
||||
const hasIcons = item.hasIcons();
|
||||
|
||||
expect(hasIcons).toBe(false);
|
||||
});
|
||||
|
||||
it('should set display property with a different fieldToDisplay', () => {
|
||||
item = new ChipsItem(
|
||||
{
|
||||
label: 'A',
|
||||
value: 'a'
|
||||
},
|
||||
'label');
|
||||
|
||||
expect(item.display).toBe('A');
|
||||
});
|
||||
|
||||
it('should set display property with a different objToDisplay', () => {
|
||||
item = new ChipsItem(
|
||||
{
|
||||
toDisplay: new FormFieldMetadataValueObject('a', null, 'a'),
|
||||
otherProperty: 'other'
|
||||
},
|
||||
'value', 'toDisplay');
|
||||
|
||||
expect(item.display).toBe('a');
|
||||
});
|
||||
});
|
72
src/app/shared/chips/models/chips-item.model.ts
Normal file
72
src/app/shared/chips/models/chips-item.model.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { uniqueId, isObject } from 'lodash';
|
||||
import { isNotEmpty } from '../../empty.util';
|
||||
|
||||
export interface ChipsItemIcon {
|
||||
metadata: string;
|
||||
hasAuthority: boolean;
|
||||
style: string;
|
||||
tooltip?: any;
|
||||
}
|
||||
|
||||
export class ChipsItem {
|
||||
public id: string;
|
||||
public display: string;
|
||||
public item: any;
|
||||
public editMode?: boolean;
|
||||
public icons?: ChipsItemIcon[];
|
||||
|
||||
private fieldToDisplay: string;
|
||||
private objToDisplay: string;
|
||||
|
||||
constructor(item: any,
|
||||
fieldToDisplay: string = 'display',
|
||||
objToDisplay?: string,
|
||||
icons?: ChipsItemIcon[],
|
||||
editMode?: boolean) {
|
||||
|
||||
this.id = uniqueId();
|
||||
this.item = item;
|
||||
this.fieldToDisplay = fieldToDisplay;
|
||||
this.objToDisplay = objToDisplay;
|
||||
this.setDisplayText();
|
||||
this.editMode = editMode || false;
|
||||
this.icons = icons || [];
|
||||
}
|
||||
|
||||
hasIcons(): boolean {
|
||||
return isNotEmpty(this.icons);
|
||||
}
|
||||
|
||||
setEditMode(): void {
|
||||
this.editMode = true;
|
||||
}
|
||||
|
||||
updateIcons(icons: ChipsItemIcon[]): void {
|
||||
this.icons = icons;
|
||||
}
|
||||
|
||||
updateItem(item: any): void {
|
||||
this.item = item;
|
||||
this.setDisplayText();
|
||||
}
|
||||
|
||||
unsetEditMode(): void {
|
||||
this.editMode = false;
|
||||
}
|
||||
|
||||
private setDisplayText(): void {
|
||||
let value = this.item;
|
||||
if (isObject(this.item)) {
|
||||
// Check If displayField is in an internal object
|
||||
const obj = this.objToDisplay ? this.item[this.objToDisplay] : this.item;
|
||||
|
||||
if (isObject(obj) && obj) {
|
||||
value = obj[this.fieldToDisplay] || obj.value;
|
||||
} else {
|
||||
value = obj;
|
||||
}
|
||||
}
|
||||
|
||||
this.display = value;
|
||||
}
|
||||
}
|
126
src/app/shared/chips/models/chips.model.spec.ts
Normal file
126
src/app/shared/chips/models/chips.model.spec.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { Chips } from './chips.model';
|
||||
import { ChipsItem } from './chips-item.model';
|
||||
import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model';
|
||||
|
||||
describe('Chips model test suite', () => {
|
||||
let items: any[];
|
||||
let item: ChipsItem;
|
||||
let chips: Chips;
|
||||
|
||||
beforeEach(() => {
|
||||
items = ['a', 'b', 'c'];
|
||||
chips = new Chips(items);
|
||||
});
|
||||
|
||||
it('should init Chips object properly', () => {
|
||||
expect(chips.getChipsItems()).toEqual(items);
|
||||
expect(chips.displayField).toBe('display');
|
||||
expect(chips.displayObj).toBe(undefined);
|
||||
expect(chips.iconsConfig).toEqual([]);
|
||||
});
|
||||
|
||||
it('should add an element to items', () => {
|
||||
items = ['a', 'b', 'c', 'd'];
|
||||
chips.add('d');
|
||||
expect(chips.getChipsItems()).toEqual(items);
|
||||
});
|
||||
|
||||
it('should remove an element from items', () => {
|
||||
items = ['a', 'c'];
|
||||
item = chips.getChipByIndex(1);
|
||||
chips.remove(item);
|
||||
expect(chips.getChipsItems()).toEqual(items);
|
||||
});
|
||||
|
||||
it('should update an item', () => {
|
||||
items = ['a', 'd', 'c'];
|
||||
const id = chips.getChipByIndex(1).id;
|
||||
chips.update(id, 'd');
|
||||
expect(chips.getChipsItems()).toEqual(items);
|
||||
});
|
||||
|
||||
it('should update items order', () => {
|
||||
items = ['a', 'c', 'b'];
|
||||
const chipsItems = chips.getChips();
|
||||
const b = chipsItems[1];
|
||||
chipsItems[1] = chipsItems[2];
|
||||
chipsItems[2] = b;
|
||||
chips.updateOrder();
|
||||
expect(chips.getChipsItems()).toEqual(items);
|
||||
});
|
||||
|
||||
it('should set a different displayField', () => {
|
||||
items = [
|
||||
{
|
||||
label: 'A',
|
||||
value: 'a'
|
||||
},
|
||||
{
|
||||
label: 'B',
|
||||
value: 'b'
|
||||
},
|
||||
{
|
||||
label: 'C',
|
||||
value: 'c'
|
||||
},
|
||||
];
|
||||
chips = new Chips(items, 'label');
|
||||
expect(chips.displayField).toBe('label');
|
||||
expect(chips.getChipsItems()).toEqual(items);
|
||||
});
|
||||
|
||||
it('should set a different displayObj', () => {
|
||||
items = [
|
||||
{
|
||||
toDisplay: new FormFieldMetadataValueObject('a', null, 'a'),
|
||||
otherProperty: 'a'
|
||||
},
|
||||
{
|
||||
toDisplay: new FormFieldMetadataValueObject('a', null, 'a'),
|
||||
otherProperty: 'a'
|
||||
},
|
||||
{
|
||||
toDisplay: new FormFieldMetadataValueObject('a', null, 'a'),
|
||||
otherProperty: 'a'
|
||||
},
|
||||
];
|
||||
chips = new Chips(items, 'value', 'toDisplay');
|
||||
expect(chips.displayField).toBe('value');
|
||||
expect(chips.displayObj).toBe('toDisplay');
|
||||
expect(chips.getChipsItems()).toEqual(items);
|
||||
});
|
||||
|
||||
it('should set iconsConfig', () => {
|
||||
items = [
|
||||
{
|
||||
toDisplay: new FormFieldMetadataValueObject('a', null, 'a'),
|
||||
otherProperty: 'a'
|
||||
},
|
||||
{
|
||||
toDisplay: new FormFieldMetadataValueObject('a', null, 'a'),
|
||||
otherProperty: 'a'
|
||||
},
|
||||
{
|
||||
toDisplay: new FormFieldMetadataValueObject('a', null, 'a'),
|
||||
otherProperty: 'a'
|
||||
},
|
||||
];
|
||||
const iconsConfig = [{
|
||||
name: 'toDisplay',
|
||||
config: {
|
||||
withAuthority:{
|
||||
style: 'fa-user'
|
||||
},
|
||||
withoutAuthority:{
|
||||
style: 'fa-user text-muted'
|
||||
}
|
||||
}
|
||||
}];
|
||||
chips = new Chips(items, 'value', 'toDisplay', iconsConfig);
|
||||
|
||||
expect(chips.displayField).toBe('value');
|
||||
expect(chips.displayObj).toBe('toDisplay');
|
||||
expect(chips.iconsConfig).toEqual(iconsConfig);
|
||||
expect(chips.getChipsItems()).toEqual(items);
|
||||
});
|
||||
});
|
159
src/app/shared/chips/models/chips.model.ts
Normal file
159
src/app/shared/chips/models/chips.model.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { findIndex, isEqual, isObject } from 'lodash';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
import { ChipsItem, ChipsItemIcon } from './chips-item.model';
|
||||
import { hasValue, isNotEmpty } from '../../empty.util';
|
||||
|
||||
export interface IconsConfig {
|
||||
withAuthority?: {
|
||||
style: string;
|
||||
};
|
||||
|
||||
withoutAuthority?: {
|
||||
style: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MetadataIconsConfig {
|
||||
name: string;
|
||||
config: IconsConfig;
|
||||
}
|
||||
|
||||
export class Chips {
|
||||
chipsItems: BehaviorSubject<ChipsItem[]>;
|
||||
displayField: string;
|
||||
displayObj: string;
|
||||
iconsConfig: MetadataIconsConfig[];
|
||||
|
||||
private _items: ChipsItem[];
|
||||
|
||||
constructor(items: any[] = [],
|
||||
displayField: string = 'display',
|
||||
displayObj?: string,
|
||||
iconsConfig?: MetadataIconsConfig[]) {
|
||||
|
||||
this.displayField = displayField;
|
||||
this.displayObj = displayObj;
|
||||
this.iconsConfig = iconsConfig || [];
|
||||
if (Array.isArray(items)) {
|
||||
this.setInitialItems(items);
|
||||
}
|
||||
}
|
||||
|
||||
public add(item: any): void {
|
||||
const icons = this.getChipsIcons(item);
|
||||
const chipsItem = new ChipsItem(item, this.displayField, this.displayObj, icons);
|
||||
|
||||
const duplicatedIndex = findIndex(this._items, {display: chipsItem.display.trim()});
|
||||
if (duplicatedIndex === -1 || !isEqual(item, this.getChipByIndex(duplicatedIndex).item)) {
|
||||
this._items.push(chipsItem);
|
||||
this.chipsItems.next(this._items);
|
||||
}
|
||||
}
|
||||
|
||||
public getChipById(id): ChipsItem {
|
||||
const index = findIndex(this._items, {id: id});
|
||||
return this.getChipByIndex(index);
|
||||
}
|
||||
|
||||
public getChipByIndex(index): ChipsItem {
|
||||
if (this._items.length > 0 && this._items[index]) {
|
||||
return this._items[index];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public getChips(): ChipsItem[] {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
/**
|
||||
* To use to get items before to store it
|
||||
* @returns {any[]}
|
||||
*/
|
||||
public getChipsItems(): any[] {
|
||||
const out = [];
|
||||
this._items.forEach((item) => {
|
||||
out.push(item.item);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
public hasItems(): boolean {
|
||||
return this._items.length > 0;
|
||||
}
|
||||
|
||||
public remove(chipsItem: ChipsItem): void {
|
||||
const index = findIndex(this._items, {id: chipsItem.id});
|
||||
this._items.splice(index, 1);
|
||||
this.chipsItems.next(this._items);
|
||||
}
|
||||
|
||||
public update(id: string, item: any): void {
|
||||
const chipsItemTarget = this.getChipById(id);
|
||||
const icons = this.getChipsIcons(item);
|
||||
|
||||
chipsItemTarget.updateItem(item);
|
||||
chipsItemTarget.updateIcons(icons);
|
||||
chipsItemTarget.unsetEditMode();
|
||||
this.chipsItems.next(this._items);
|
||||
}
|
||||
|
||||
public updateOrder(): void {
|
||||
this.chipsItems.next(this._items);
|
||||
}
|
||||
|
||||
private getChipsIcons(item) {
|
||||
const icons = [];
|
||||
const defaultConfigIndex: number = findIndex(this.iconsConfig, {name: 'default'});
|
||||
const defaultConfig: IconsConfig = (defaultConfigIndex !== -1) ? this.iconsConfig[defaultConfigIndex].config : undefined;
|
||||
let config: IconsConfig;
|
||||
let configIndex: number;
|
||||
let value: any;
|
||||
|
||||
Object.keys(item)
|
||||
.forEach((metadata) => {
|
||||
|
||||
value = item[metadata];
|
||||
configIndex = findIndex(this.iconsConfig, {name: metadata});
|
||||
|
||||
config = (configIndex !== -1) ? this.iconsConfig[configIndex].config : defaultConfig;
|
||||
|
||||
if (hasValue(value) && isNotEmpty(config)) {
|
||||
|
||||
let icon: ChipsItemIcon;
|
||||
const hasAuthority: boolean = !!(isObject(value) && ((value.hasOwnProperty('authority') && value.authority) || (value.hasOwnProperty('id') && value.id)));
|
||||
|
||||
// Set icons
|
||||
if ((this.displayObj && this.displayObj === metadata && hasAuthority)
|
||||
|| (this.displayObj && this.displayObj !== metadata)) {
|
||||
|
||||
icon = {
|
||||
metadata,
|
||||
hasAuthority: hasAuthority,
|
||||
style: (hasAuthority) ? config.withAuthority.style : config.withoutAuthority.style
|
||||
};
|
||||
}
|
||||
if (icon) {
|
||||
icons.push(icon);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return icons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets initial items, used in edit mode
|
||||
*/
|
||||
private setInitialItems(items: any[]): void {
|
||||
this._items = [];
|
||||
items.forEach((item) => {
|
||||
const icons = this.getChipsIcons(item);
|
||||
const chipsItem = new ChipsItem(item, this.displayField, this.displayObj, icons);
|
||||
this._items.push(chipsItem);
|
||||
});
|
||||
|
||||
this.chipsItems = new BehaviorSubject<ChipsItem[]>(this._items);
|
||||
}
|
||||
}
|
19
src/app/shared/date.util.ts
Normal file
19
src/app/shared/date.util.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
export function dateToGMTString(date: Date | NgbDateStruct) {
|
||||
let year = ((date instanceof Date) ? date.getFullYear() : date.year).toString();
|
||||
let month = ((date instanceof Date) ? date.getMonth() + 1 : date.month).toString();
|
||||
let day = ((date instanceof Date) ? date.getDate() : date.day).toString();
|
||||
let hour = ((date instanceof Date) ? date.getHours() : 0).toString();
|
||||
let min = ((date instanceof Date) ? date.getMinutes() : 0).toString();
|
||||
let sec = ((date instanceof Date) ? date.getSeconds() : 0).toString();
|
||||
|
||||
year = (year.length === 1) ? '0' + year : year;
|
||||
month = (month.length === 1) ? '0' + month : month;
|
||||
day = (day.length === 1) ? '0' + day : day;
|
||||
hour = (hour.length === 1) ? '0' + hour : hour;
|
||||
min = (min.length === 1) ? '0' + min : min;
|
||||
sec = (sec.length === 1) ? '0' + sec : sec;
|
||||
return `${year}-${month}-${day}T${hour}:${min}:${sec}Z`;
|
||||
|
||||
}
|
@@ -0,0 +1,461 @@
|
||||
<div [class.form-group]="(type !== 6 && asBootstrapFormGroup) || getClass('element', 'container').includes('form-group')"
|
||||
[formGroup]="group"
|
||||
[ngClass]="[getClass('element', 'container'), getClass('grid', 'container')]">
|
||||
|
||||
<label *ngIf="type !== 3 && model.label"
|
||||
[for]="model.id"
|
||||
[innerHTML]="(model.required && model.label) ? (model.label | translate) + ' *' : (model.label | translate)"
|
||||
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]"></label>
|
||||
|
||||
<ng-container *ngTemplateOutlet="templates[0]?.templateRef; context: model"></ng-container>
|
||||
|
||||
<div [ngClass]="{'form-row': model.hasLanguages }">
|
||||
<div [ngClass]="getClass('grid', 'control')">
|
||||
|
||||
<ng-container [ngSwitch]="type">
|
||||
|
||||
<!-- FORM ARRAY ------------------------------------------------------------------------------------------->
|
||||
<div *ngSwitchCase="1"
|
||||
[dynamicId]="bindId && model.id"
|
||||
[formArrayName]="model.id"
|
||||
[ngClass]="getClass('element', 'control')">
|
||||
|
||||
<div *ngFor="let groupModel of model.groups; let idx = index" role="group"
|
||||
[formGroupName]="idx" [ngClass]="[getClass('element', 'group'), getClass('grid', 'group')]">
|
||||
|
||||
<ds-dynamic-form-control *ngFor="let _model of groupModel.group"
|
||||
[bindId]="false"
|
||||
[formId]="formId"
|
||||
[context]="groupModel"
|
||||
[group]="control.at(idx)"
|
||||
[hasErrorMessaging]="_model.hasErrorMessages"
|
||||
[hidden]="_model.hidden"
|
||||
[layout]="layout"
|
||||
[model]="_model"
|
||||
[templates]="templateList"
|
||||
[ngClass]="[getClass('element', 'host', _model), getClass('grid', 'host', _model)]"
|
||||
(dfBlur)="onBlur($event)"
|
||||
(dfChange)="onValueChange($event)"
|
||||
(dfFocus)="onFocus($event)"></ds-dynamic-form-control>
|
||||
|
||||
<ng-container *ngTemplateOutlet="templates[2]?.templateRef; context: groupModel"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- CALENDAR --------------------------------------------------------------------------------------------->
|
||||
<ngb-datepicker *ngSwitchCase="2"
|
||||
[displayMonths]="getAdditional('displayMonths', 1)"
|
||||
[dynamicId]="bindId && model.id"
|
||||
[firstDayOfWeek]="getAdditional('firstDayOfWeek', 1)"
|
||||
[formControlName]="model.id"
|
||||
[maxDate]="model.max"
|
||||
[minDate]="model.min"
|
||||
[navigation]="getAdditional('navigation', 'select')"
|
||||
[ngClass]="getClass('element', 'control')"
|
||||
[outsideDays]="getAdditional('outsideDays', 'visible')"
|
||||
[showWeekdays]="getAdditional('showWeekdays', true)"
|
||||
[showWeekNumbers]="getAdditional('showWeekNumbers', false)"
|
||||
[startDate]="model.focusedDate"
|
||||
(select)="onValueChange($event)"></ngb-datepicker>
|
||||
|
||||
<!-- CHECKBOX --------------------------------------------------------------------------------------------->
|
||||
<div *ngSwitchCase="3" class="custom-control custom-checkbox" [class.disabled]="model.disabled">
|
||||
<input type="checkbox" class="custom-control-input"
|
||||
[checked]="model.checked"
|
||||
[class.is-invalid]="showErrorMessages"
|
||||
[id]="bindId && model.id"
|
||||
[dynamicId]="bindId && model.id"
|
||||
[formControlName]="model.id"
|
||||
[indeterminate]="model.indeterminate"
|
||||
[name]="model.name"
|
||||
[ngClass]="getClass('element', 'control')"
|
||||
[required]="model.required"
|
||||
[tabindex]="model.tabIndex"
|
||||
[value]="model.value"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onValueChange($event)"
|
||||
(focus)="onFocus($event)" >
|
||||
<label class="custom-control-label" [for]="bindId && model.id">
|
||||
<span [innerHTML]="model.label"
|
||||
[ngClass]="[getClass('element', 'label'), getClass('grid', 'label')]">
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- CHECKBOX GROUP --------------------------------------------------------------------------------------->
|
||||
<div *ngSwitchCase="4" class="btn-group btn-group-toggle" data-toggle="buttons"
|
||||
[dynamicId]="bindId && model.id"
|
||||
[formGroupName]="model.id"
|
||||
[ngClass]="getClass('element', 'control')">
|
||||
<label *ngFor="let checkboxModel of model.group" ngbButtonLabel
|
||||
[hidden]="checkboxModel.hidden"
|
||||
[ngClass]="getClass('element', 'control', checkboxModel)">
|
||||
<input type="checkbox" ngbButton
|
||||
[checked]="checkboxModel.checked"
|
||||
[id]="bindId && checkboxModel.id"
|
||||
[dynamicId]="bindId && checkboxModel.id"
|
||||
[formControlName]="checkboxModel.id"
|
||||
[indeterminate]="checkboxModel.indeterminate"
|
||||
[name]="checkboxModel.name"
|
||||
[required]="checkboxModel.required"
|
||||
[tabindex]="checkboxModel.tabIndex"
|
||||
[value]="checkboxModel.value"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onValueChange($event)"
|
||||
(focus)="onFocus($event)"/>
|
||||
<span [ngClass]="getClass('element', 'label', checkboxModel)"
|
||||
[innerHTML]="checkboxModel.label"></span></label>
|
||||
</div>
|
||||
|
||||
<!-- DATEPICKER ------------------------------------------------------------------------------------------->
|
||||
<div *ngSwitchCase="5" class="input-group">
|
||||
<input ngbDatepicker class="form-control" #datepicker="ngbDatepicker"
|
||||
[class.is-invalid]="showErrorMessages"
|
||||
[displayMonths]="getAdditional('displayMonths', 1)"
|
||||
[dynamicId]="bindId && model.id"
|
||||
[firstDayOfWeek]="getAdditional('firstDayOfWeek', 1)"
|
||||
[formControlName]="model.id"
|
||||
[maxDate]="model.max"
|
||||
[minDate]="model.min"
|
||||
[name]="model.name"
|
||||
[navigation]="getAdditional('navigation', 'select')"
|
||||
[ngClass]="getClass('element', 'control')"
|
||||
[outsideDays]="getAdditional('outsideDays', 'visible')"
|
||||
[placeholder]="(model.placeholder | translate)"
|
||||
[placement]="getAdditional('placement', 'bottom-left')"
|
||||
[showWeekdays]="getAdditional('showWeekdays', true)"
|
||||
[showWeekNumbers]="getAdditional('showWeekNumbers', false)"
|
||||
[startDate]="model.focusedDate"
|
||||
(dateSelect)="onValueChange($event)"
|
||||
(blur)="onBlur($event)"
|
||||
(focus)="onFocus($event)">
|
||||
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary"
|
||||
type="button"
|
||||
[class.disabled]="model.disabled"
|
||||
[disabled]="model.disabled"
|
||||
(click)="datepicker.toggle()">
|
||||
<i *ngIf="model.toggleIcon" class="{{model.toggleIcon}}" aria-hidden="true"></i>
|
||||
<span *ngIf="model.toggleLabel">{{ model.toggleLabel }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FORM GROUP ------------------------------------------------------------------------------------------->
|
||||
<div *ngSwitchCase="6" role="group"
|
||||
[dynamicId]="bindId && model.id"
|
||||
[formGroupName]="model.id"
|
||||
[ngClass]="getClass('element','control')">
|
||||
|
||||
<ds-dynamic-form-control *ngFor="let _model of model.group"
|
||||
[asBootstrapFormGroup]="true"
|
||||
[formId]="formId"
|
||||
[group]="control"
|
||||
[hasErrorMessaging]="_model.hasErrorMessages"
|
||||
[hidden]="_model.hidden"
|
||||
[layout]="layout"
|
||||
[model]="_model"
|
||||
[templates]="templateList"
|
||||
[ngClass]="[getClass('element', 'host', _model), getClass('grid', 'host', _model)]"
|
||||
(dfBlur)="onBlur($event)"
|
||||
(dfChange)="onValueChange($event)"
|
||||
(dfFocus)="onFocus($event)"></ds-dynamic-form-control>
|
||||
</div>
|
||||
|
||||
<!-- INPUT ------------------------------------------------------------------------------------------------>
|
||||
<div *ngSwitchCase="7" [class.input-group]="model.prefix || model.suffix">
|
||||
|
||||
<div *ngIf="model.prefix" class="input-group-prepend">
|
||||
<span class="input-group-text" [innerHTML]="model.prefix"></span>
|
||||
</div>
|
||||
|
||||
<ng-container *ngTemplateOutlet="inputTemplate;
|
||||
context:{bindId: bindId, model: model, showErrorMessages: showErrorMessages}">
|
||||
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="model.suffix" class="input-group-append">
|
||||
<span class="input-group-text" [innerHTML]="model.suffix"></span>
|
||||
</div>
|
||||
|
||||
<datalist *ngIf="model.list" [id]="model.listId">
|
||||
<option *ngFor="let option of model.list" [value]="option">
|
||||
</datalist>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- RADIO GROUP ------------------------------------------------------------------------------------------>
|
||||
<div *ngSwitchCase="8" ngbRadioGroup class="btn-group btn-group-toggle" role="radiogroup"
|
||||
[dynamicId]="bindId && model.id"
|
||||
[formControlName]="model.id"
|
||||
[ngClass]="getClass('element', 'control')"
|
||||
[tabindex]="model.tabIndex"
|
||||
(change)="onValueChange($event)">
|
||||
|
||||
<legend *ngIf="model.legend" [innerHTML]="model.legend"></legend>
|
||||
|
||||
<label *ngFor="let option of model.options$ | async" ngbButtonLabel
|
||||
[ngClass]="[getClass('element', 'option'), getClass('grid', 'option')]">
|
||||
|
||||
<input type="radio" ngbButton
|
||||
[disabled]="option.disabled"
|
||||
[name]="model.name"
|
||||
[value]="option.value"
|
||||
(blur)="onBlur($event)"
|
||||
(focus)="onFocus($event)"/><span [innerHTML]="option.label"></span>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- SELECT ----------------------------------------------------------------------------------------------->
|
||||
<ng-container *ngSwitchCase="9">
|
||||
<ng-container *ngTemplateOutlet="selectTemplate;
|
||||
context:{bindId: bindId, model: model, showErrorMessages: showErrorMessages}">
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<!-- TEXTAREA --------------------------------------------------------------------------------------------->
|
||||
<textarea *ngSwitchCase="10" class="form-control"
|
||||
[class.is-invalid]="showErrorMessages"
|
||||
[dynamicId]="bindId && model.id"
|
||||
[cols]="model.cols"
|
||||
[formControlName]="model.id"
|
||||
[maxlength]="model.maxLength"
|
||||
[minlength]="model.minLength"
|
||||
[name]="model.name"
|
||||
[ngClass]="getClass('element', 'control')"
|
||||
[placeholder]="(model.placeholder | translate)"
|
||||
[readonly]="model.readOnly"
|
||||
[required]="model.required"
|
||||
[rows]="model.rows"
|
||||
[spellcheck]="model.spellCheck"
|
||||
[tabindex]="model.tabIndex"
|
||||
[wrap]="model.wrap"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onValueChange($event)"
|
||||
(focus)="onFocus($event)"></textarea>
|
||||
|
||||
<!-- TIMEPICKER ------------------------------------------------------------------------------------------->
|
||||
<ngb-timepicker *ngSwitchCase="11"
|
||||
[dynamicId]="bindId && model.id"
|
||||
[formControlName]="model.id"
|
||||
[hourStep]="getAdditional('hourStep', 1)"
|
||||
[meridian]="model.meridian"
|
||||
[minuteStep]="getAdditional('minuteStep', 1)"
|
||||
[ngClass]="getClass('element', 'control')"
|
||||
[seconds]="model.showSeconds"
|
||||
[secondStep]="getAdditional('secondStep', 1)"
|
||||
[size]="getAdditional('size', 'medium')"
|
||||
[spinners]="getAdditional('spinners', true)"></ngb-timepicker>
|
||||
|
||||
|
||||
|
||||
<ng-container *ngSwitchCase="12">
|
||||
<ng-container *ngTemplateOutlet="typeaheadTemplate;
|
||||
context:{bindId: bindId, model: model, showErrorMessages: showErrorMessages}">
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<ng-container *ngSwitchCase="13">
|
||||
<ng-container *ngTemplateOutlet="scrollableDropdownTemplate;
|
||||
context:{bindId: bindId, model: model, showErrorMessages: showErrorMessages}">
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="14">
|
||||
<ng-container *ngTemplateOutlet="tagTemplate;
|
||||
context:{bindId: bindId, model: model, showErrorMessages: showErrorMessages}">
|
||||
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="15">
|
||||
<ng-container *ngTemplateOutlet="listTemplate;
|
||||
context:{bindId: bindId, model: model, showErrorMessages: showErrorMessages}">
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="16">
|
||||
<ds-dynamic-group [model]="model"
|
||||
[formId]="formId"
|
||||
[group]="group"
|
||||
[showErrorMessages]="showErrorMessages"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onValueChange($event)"
|
||||
(focus)="onFocus($event)"></ds-dynamic-group>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="17">
|
||||
<ds-date-picker
|
||||
[bindId]="bindId"
|
||||
[group]="group"
|
||||
[model]="model"
|
||||
[showErrorMessages]="showErrorMessages"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onValueChange($event)"
|
||||
(focus)="onFocus($event)"></ds-date-picker>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="18">
|
||||
<ds-dynamic-lookup
|
||||
[bindId]="bindId"
|
||||
[group]="group"
|
||||
[model]="model"
|
||||
[showErrorMessages]="showErrorMessages"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onValueChange($event)"
|
||||
(focus)="onFocus($event)"
|
||||
></ds-dynamic-lookup>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngSwitchCase="19">
|
||||
<ds-dynamic-lookup
|
||||
[bindId]="bindId"
|
||||
[group]="group"
|
||||
[model]="model"
|
||||
[showErrorMessages]="showErrorMessages"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onValueChange($event)"
|
||||
(focus)="onFocus($event)"
|
||||
></ds-dynamic-lookup>
|
||||
</ng-container>
|
||||
|
||||
<small *ngIf="model.hint" class="text-muted" [innerHTML]="model.hint"
|
||||
[ngClass]="getClass('element', 'hint')"></small>
|
||||
|
||||
<div *ngIf="showErrorMessages" [ngClass]="[getClass('element', 'errors'), getClass('grid', 'errors')]">
|
||||
<small *ngFor="let message of errorMessages" class="invalid-feedback d-block">{{ message | translate:model.validators }}</small>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
|
||||
<ng-template #inputTemplate let-bindId="bindId" let-model="model"
|
||||
let-showErrorMessages="showErrorMessages">
|
||||
<input [attr.accept]="model.accept"
|
||||
[attr.list]="model.listId"
|
||||
[attr.max]="model.max"
|
||||
[attr.min]="model.min"
|
||||
[attr.multiple]="model.multiple"
|
||||
[attr.step]="model.step"
|
||||
[autocomplete]="model.autoComplete"
|
||||
[autofocus]="model.autoFocus"
|
||||
[class.form-control]="model.inputType !== 'file'"
|
||||
[class.form-control-file]="model.inputType === 'file'"
|
||||
[class.is-invalid]="showErrorMessages"
|
||||
[dynamicId]="bindId && model.id"
|
||||
[formControlName]="model.id"
|
||||
[maxlength]="model.maxLength"
|
||||
[minlength]="model.minLength"
|
||||
[name]="model.name"
|
||||
[ngClass]="getClass('element', 'control')"
|
||||
[pattern]="model.pattern"
|
||||
[placeholder]="(model.placeholder | translate)"
|
||||
[readonly]="model.readOnly"
|
||||
[required]="model.required"
|
||||
[spellcheck]="model.spellCheck"
|
||||
[tabindex]="model.tabIndex"
|
||||
[textMask]="{mask: (model.mask || false), showMask: model.mask && !(model.placeholder)}"
|
||||
[type]="model.inputType"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onValueChange($event)"
|
||||
(focus)="onFocus($event)"/>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #selectTemplate let-bindId="bindId" let-model="model"
|
||||
let-showErrorMessages="showErrorMessages">
|
||||
<select class="form-control"
|
||||
[class.is-invalid]="showErrorMessages"
|
||||
[dynamicId]="bindId && model.id"
|
||||
[formControlName]="model.id"
|
||||
[name]="model.name"
|
||||
[ngClass]="getClass('element', 'control')"
|
||||
[required]="model.required"
|
||||
[tabindex]="model.tabIndex"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onValueChange($event)"
|
||||
(focus)="onFocus($event)">
|
||||
|
||||
<option *ngFor="let option of model.options$ | async"
|
||||
[disabled]="option.disabled"
|
||||
[ngValue]="option.value">{{ option.label }}</option>
|
||||
|
||||
</select>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #typeaheadTemplate let-bindId="bindId" let-model="model"
|
||||
let-showErrorMessages="showErrorMessages">
|
||||
<ds-dynamic-typeahead [bindId]="bindId"
|
||||
[group]="group"
|
||||
[model]="model"
|
||||
[showErrorMessages]="showErrorMessages"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onValueChange($event)"
|
||||
(focus)="onFocus($event)"></ds-dynamic-typeahead>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #scrollableDropdownTemplate let-bindId="bindId" let-model="model"
|
||||
let-showErrorMessages="showErrorMessages">
|
||||
<ds-dynamic-scrollable-dropdown [bindId]="bindId"
|
||||
[group]="group"
|
||||
[model]="model"
|
||||
[showErrorMessages]="showErrorMessages"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onValueChange($event)"
|
||||
(focus)="onFocus($event)"></ds-dynamic-scrollable-dropdown>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #tagTemplate let-bindId="bindId" let-model="model"
|
||||
let-showErrorMessages="showErrorMessages">
|
||||
<ds-dynamic-tag [bindId]="bindId"
|
||||
[group]="group"
|
||||
[model]="model"
|
||||
[showErrorMessages]="showErrorMessages"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onValueChange($event)"
|
||||
(focus)="onFocus($event)"></ds-dynamic-tag>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #listTemplate let-bindId="bindId" let-model="model"
|
||||
let-showErrorMessages="showErrorMessages">
|
||||
<ds-dynamic-list [bindId]="bindId"
|
||||
[group]="group"
|
||||
[model]="model"
|
||||
[showErrorMessages]="showErrorMessages"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onValueChange($event)"
|
||||
(focus)="onFocus($event)"></ds-dynamic-list>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
|
||||
<div *ngIf="model.languageCodes && model.languageCodes.length > 0" class="col-xs-2">
|
||||
<select
|
||||
#language="ngModel"
|
||||
[disabled]="model.readOnly"
|
||||
[(ngModel)]="model.language"
|
||||
class="form-control"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onChangeLanguage($event)"
|
||||
[ngModelOptions]="{standalone: true}"
|
||||
required>
|
||||
<!--<option [value]="null" disabled>Language</option>-->
|
||||
<option *ngFor="let lang of model.languageCodes" [value]="lang.code">{{lang.display}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngTemplateOutlet="templates[1]?.templateRef; context: model"></ng-container>
|
||||
|
||||
<ng-content></ng-content>
|
||||
|
||||
</div>
|
@@ -0,0 +1,281 @@
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, DebugElement, SimpleChange } from '@angular/core';
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TextMaskModule } from 'angular2-text-mask';
|
||||
import {
|
||||
DynamicCheckboxGroupModel,
|
||||
DynamicCheckboxModel,
|
||||
DynamicColorPickerModel,
|
||||
DynamicDatePickerModel,
|
||||
DynamicEditorModel,
|
||||
DynamicFileUploadModel,
|
||||
DynamicFormArrayModel,
|
||||
DynamicFormControlModel,
|
||||
DynamicFormGroupModel,
|
||||
DynamicFormsCoreModule,
|
||||
DynamicFormService,
|
||||
DynamicInputModel,
|
||||
DynamicRadioGroupModel,
|
||||
DynamicRatingModel,
|
||||
DynamicSelectModel,
|
||||
DynamicSliderModel,
|
||||
DynamicSwitchModel,
|
||||
DynamicTextAreaModel,
|
||||
DynamicTimePickerModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { DsDynamicFormControlComponent, NGBootstrapFormControlType } from './ds-dynamic-form-control.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { SharedModule } from '../../../shared.module';
|
||||
import { DynamicDsDatePickerModel } from './models/date-picker/date-picker.model';
|
||||
import { DynamicGroupModel } from './models/dynamic-group/dynamic-group.model';
|
||||
import { DynamicListCheckboxGroupModel } from './models/list/dynamic-list-checkbox-group.model';
|
||||
import { AuthorityOptions } from '../../../../core/integration/models/authority-options.model';
|
||||
import { DynamicListRadioGroupModel } from './models/list/dynamic-list-radio-group.model';
|
||||
import { DynamicLookupModel } from './models/lookup/dynamic-lookup.model';
|
||||
import { DynamicScrollableDropdownModel } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
|
||||
import { DynamicTagModel } from './models/tag/dynamic-tag.model';
|
||||
import { DynamicTypeaheadModel } from './models/typeahead/dynamic-typeahead.model';
|
||||
import { DynamicQualdropModel } from './models/ds-dynamic-qualdrop.model';
|
||||
import { DynamicLookupNameModel } from './models/lookup/dynamic-lookup-name.model';
|
||||
|
||||
describe('DsDynamicFormControlComponent test suite', () => {
|
||||
|
||||
const authorityOptions: AuthorityOptions = {
|
||||
closed: false,
|
||||
metadata: 'list',
|
||||
name: 'type_programme',
|
||||
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
|
||||
};
|
||||
const formModel = [
|
||||
new DynamicCheckboxModel({id: 'checkbox'}),
|
||||
new DynamicCheckboxGroupModel({id: 'checkboxGroup', group: []}),
|
||||
new DynamicColorPickerModel({id: 'colorpicker'}),
|
||||
new DynamicDatePickerModel({id: 'datepicker'}),
|
||||
new DynamicEditorModel({id: 'editor'}),
|
||||
new DynamicFileUploadModel({id: 'upload', url: ''}),
|
||||
new DynamicFormArrayModel({id: 'formArray', groupFactory: () => []}),
|
||||
new DynamicFormGroupModel({id: 'formGroup', group: []}),
|
||||
new DynamicInputModel({id: 'input', maxLength: 51}),
|
||||
new DynamicRadioGroupModel({id: 'radioGroup'}),
|
||||
new DynamicRatingModel({id: 'rating'}),
|
||||
new DynamicSelectModel({id: 'select', options: [{value: 'One'}, {value: 'Two'}], value: 'One'}),
|
||||
new DynamicSliderModel({id: 'slider'}),
|
||||
new DynamicSwitchModel({id: 'switch'}),
|
||||
new DynamicTextAreaModel({id: 'textarea'}),
|
||||
new DynamicTimePickerModel({id: 'timepicker'}),
|
||||
new DynamicTypeaheadModel({id: 'typeahead'}),
|
||||
new DynamicScrollableDropdownModel({id: 'scrollableDropdown', authorityOptions: authorityOptions}),
|
||||
new DynamicTagModel({id: 'tag'}),
|
||||
new DynamicListCheckboxGroupModel({id: 'checkboxList', authorityOptions: authorityOptions, repeatable: true}),
|
||||
new DynamicListRadioGroupModel({id: 'radioList', authorityOptions: authorityOptions, repeatable: false}),
|
||||
new DynamicGroupModel({
|
||||
id: 'relationGroup',
|
||||
formConfiguration: [],
|
||||
mandatoryField: '',
|
||||
name: 'relationGroup',
|
||||
relationFields: [],
|
||||
scopeUUID: '',
|
||||
submissionScope: ''
|
||||
}),
|
||||
new DynamicDsDatePickerModel({id: 'datepicker'}),
|
||||
new DynamicLookupModel({id: 'lookup'}),
|
||||
new DynamicLookupNameModel({id: 'lookupName'}),
|
||||
new DynamicQualdropModel({id: 'combobox', readOnly: false})
|
||||
];
|
||||
const testModel = formModel[8];
|
||||
let formGroup: FormGroup;
|
||||
let fixture: ComponentFixture<DsDynamicFormControlComponent>;
|
||||
let component: DsDynamicFormControlComponent;
|
||||
let debugElement: DebugElement;
|
||||
let testElement: DebugElement;
|
||||
|
||||
beforeEach(async(() => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbModule.forRoot(),
|
||||
DynamicFormsCoreModule.forRoot(),
|
||||
SharedModule,
|
||||
TranslateModule.forRoot(),
|
||||
TextMaskModule,
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
}).compileComponents().then(() => {
|
||||
|
||||
fixture = TestBed.createComponent(DsDynamicFormControlComponent);
|
||||
|
||||
component = fixture.componentInstance;
|
||||
debugElement = fixture.debugElement;
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(inject([DynamicFormService], (service: DynamicFormService) => {
|
||||
|
||||
formGroup = service.createFormGroup(formModel);
|
||||
|
||||
component.group = formGroup;
|
||||
component.model = testModel;
|
||||
|
||||
component.ngOnChanges({
|
||||
|
||||
group: new SimpleChange(null, component.group, true),
|
||||
model: new SimpleChange(null, component.model, true)
|
||||
});
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
testElement = debugElement.query(By.css(`input[id='${testModel.id}']`));
|
||||
}));
|
||||
|
||||
it('should initialize correctly', () => {
|
||||
|
||||
expect(component.context).toBeNull();
|
||||
expect(component.control instanceof FormControl).toBe(true);
|
||||
expect(component.group instanceof FormGroup).toBe(true);
|
||||
expect(component.model instanceof DynamicFormControlModel).toBe(true);
|
||||
expect(component.hasErrorMessaging).toBe(false);
|
||||
expect(component.asBootstrapFormGroup).toBe(true);
|
||||
|
||||
expect(component.onControlValueChanges).toBeDefined();
|
||||
expect(component.onModelDisabledUpdates).toBeDefined();
|
||||
expect(component.onModelValueUpdates).toBeDefined();
|
||||
|
||||
expect(component.blur).toBeDefined();
|
||||
expect(component.change).toBeDefined();
|
||||
expect(component.focus).toBeDefined();
|
||||
|
||||
expect(component.onValueChange).toBeDefined();
|
||||
expect(component.onBlur).toBeDefined();
|
||||
expect(component.onFocus).toBeDefined();
|
||||
|
||||
expect(component.isValid).toBe(true);
|
||||
expect(component.isInvalid).toBe(false);
|
||||
expect(component.showErrorMessages).toBe(false);
|
||||
|
||||
expect(component.type).toBe(NGBootstrapFormControlType.Input);
|
||||
});
|
||||
|
||||
it('should have an input element', () => {
|
||||
|
||||
expect(testElement instanceof DebugElement).toBe(true);
|
||||
});
|
||||
|
||||
it('should listen to native blur events', () => {
|
||||
|
||||
spyOn(component, 'onBlur');
|
||||
|
||||
testElement.triggerEventHandler('blur', null);
|
||||
|
||||
expect(component.onBlur).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should listen to native focus events', () => {
|
||||
|
||||
spyOn(component, 'onFocus');
|
||||
|
||||
testElement.triggerEventHandler('focus', null);
|
||||
|
||||
expect(component.onFocus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should listen to native change event', () => {
|
||||
|
||||
spyOn(component, 'onValueChange');
|
||||
|
||||
testElement.triggerEventHandler('change', null);
|
||||
|
||||
expect(component.onValueChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update model value when control value changes', () => {
|
||||
|
||||
spyOn(component, 'onControlValueChanges');
|
||||
|
||||
component.control.setValue('test');
|
||||
|
||||
expect(component.onControlValueChanges).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update control value when model value changes', () => {
|
||||
|
||||
spyOn(component, 'onModelValueUpdates');
|
||||
|
||||
(testModel as DynamicInputModel).valueUpdates.next('test');
|
||||
|
||||
expect(component.onModelValueUpdates).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update control activation when model disabled property changes', () => {
|
||||
|
||||
spyOn(component, 'onModelDisabledUpdates');
|
||||
|
||||
testModel.disabledUpdates.next(true);
|
||||
|
||||
expect(component.onModelDisabledUpdates).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should determine correct form control type', () => {
|
||||
|
||||
const testFn = DsDynamicFormControlComponent.getFormControlType;
|
||||
|
||||
expect(testFn(formModel[0])).toEqual(NGBootstrapFormControlType.Checkbox);
|
||||
|
||||
expect(testFn(formModel[1])).toEqual(NGBootstrapFormControlType.CheckboxGroup);
|
||||
|
||||
expect(testFn(formModel[2])).toBeNull();
|
||||
|
||||
expect(testFn(formModel[3])).toEqual(NGBootstrapFormControlType.DatePicker);
|
||||
|
||||
(formModel[3] as DynamicDatePickerModel).inline = true;
|
||||
expect(testFn(formModel[3])).toEqual(NGBootstrapFormControlType.Calendar);
|
||||
|
||||
expect(testFn(formModel[4])).toBeNull();
|
||||
|
||||
expect(testFn(formModel[5])).toBeNull();
|
||||
|
||||
expect(testFn(formModel[6])).toEqual(NGBootstrapFormControlType.Array);
|
||||
|
||||
expect(testFn(formModel[7])).toEqual(NGBootstrapFormControlType.Group);
|
||||
|
||||
expect(testFn(formModel[8])).toEqual(NGBootstrapFormControlType.Input);
|
||||
|
||||
expect(testFn(formModel[9])).toEqual(NGBootstrapFormControlType.RadioGroup);
|
||||
|
||||
expect(testFn(formModel[10])).toBeNull();
|
||||
|
||||
expect(testFn(formModel[11])).toEqual(NGBootstrapFormControlType.Select);
|
||||
|
||||
expect(testFn(formModel[12])).toBeNull();
|
||||
|
||||
expect(testFn(formModel[13])).toBeNull();
|
||||
|
||||
expect(testFn(formModel[14])).toEqual(NGBootstrapFormControlType.TextArea);
|
||||
|
||||
expect(testFn(formModel[15])).toEqual(NGBootstrapFormControlType.TimePicker);
|
||||
|
||||
expect(testFn(formModel[16])).toEqual(NGBootstrapFormControlType.TypeAhead);
|
||||
|
||||
expect(testFn(formModel[17])).toEqual(NGBootstrapFormControlType.ScrollableDropdown);
|
||||
|
||||
expect(testFn(formModel[18])).toEqual(NGBootstrapFormControlType.Tag);
|
||||
|
||||
expect(testFn(formModel[19])).toEqual(NGBootstrapFormControlType.List);
|
||||
|
||||
expect(testFn(formModel[20])).toEqual(NGBootstrapFormControlType.List);
|
||||
|
||||
expect(testFn(formModel[21])).toEqual(NGBootstrapFormControlType.Relation);
|
||||
|
||||
expect(testFn(formModel[22])).toEqual(NGBootstrapFormControlType.Date);
|
||||
|
||||
expect(testFn(formModel[23])).toEqual(NGBootstrapFormControlType.Lookup);
|
||||
|
||||
expect(testFn(formModel[24])).toEqual(NGBootstrapFormControlType.LookupName);
|
||||
|
||||
expect(testFn(formModel[25])).toEqual(NGBootstrapFormControlType.Group);
|
||||
});
|
||||
});
|
@@ -0,0 +1,178 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ContentChildren,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
Output,
|
||||
QueryList,
|
||||
SimpleChanges
|
||||
} from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import {
|
||||
DynamicDatePickerModel,
|
||||
DynamicFormControlComponent,
|
||||
DynamicFormControlEvent,
|
||||
DynamicFormControlModel,
|
||||
DynamicFormLayout,
|
||||
DynamicFormLayoutService,
|
||||
DynamicFormValidationService,
|
||||
DynamicTemplateDirective,
|
||||
DYNAMIC_FORM_CONTROL_TYPE_ARRAY,
|
||||
DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX,
|
||||
DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX_GROUP,
|
||||
DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER,
|
||||
DYNAMIC_FORM_CONTROL_TYPE_GROUP,
|
||||
DYNAMIC_FORM_CONTROL_TYPE_INPUT,
|
||||
DYNAMIC_FORM_CONTROL_TYPE_RADIO_GROUP,
|
||||
DYNAMIC_FORM_CONTROL_TYPE_SELECT,
|
||||
DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA,
|
||||
DYNAMIC_FORM_CONTROL_TYPE_TIMEPICKER,
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD } from './models/typeahead/dynamic-typeahead.model';
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_TAG } from './models/tag/dynamic-tag.model';
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './models/dynamic-group/dynamic-group.model';
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER } from './models/date-picker/date-picker.model';
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_LOOKUP } from './models/lookup/dynamic-lookup.model';
|
||||
import { DynamicListCheckboxGroupModel } from './models/list/dynamic-list-checkbox-group.model';
|
||||
import { DynamicListRadioGroupModel } from './models/list/dynamic-list-radio-group.model';
|
||||
import { isNotEmpty } from '../../../empty.util';
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME } from './models/lookup/dynamic-lookup-name.model';
|
||||
|
||||
export const enum NGBootstrapFormControlType {
|
||||
|
||||
Array = 1, // 'ARRAY',
|
||||
Calendar = 2, // 'CALENDAR',
|
||||
Checkbox = 3, // 'CHECKBOX',
|
||||
CheckboxGroup = 4, // 'CHECKBOX_GROUP',
|
||||
DatePicker = 5, // 'DATEPICKER',
|
||||
Group = 6, // 'GROUP',
|
||||
Input = 7, // 'INPUT',
|
||||
RadioGroup = 8, // 'RADIO_GROUP',
|
||||
Select = 9, // 'SELECT',
|
||||
TextArea = 10, // 'TEXTAREA',
|
||||
TimePicker = 11, // 'TIMEPICKER'
|
||||
TypeAhead = 12, // 'TYPEAHEAD'
|
||||
ScrollableDropdown = 13, // 'SCROLLABLE_DROPDOWN'
|
||||
Tag = 14, // 'TAG'
|
||||
List = 15, // 'TYPELIST'
|
||||
Relation = 16, // 'RELATION'
|
||||
Date = 17, // 'DATE'
|
||||
Lookup = 18, // LOOKUP
|
||||
LookupName = 19, // LOOKUP_NAME
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dynamic-form-control',
|
||||
styleUrls: ['../../form.component.scss', './ds-dynamic-form.component.scss'],
|
||||
templateUrl: './ds-dynamic-form-control.component.html'
|
||||
})
|
||||
export class DsDynamicFormControlComponent extends DynamicFormControlComponent implements OnChanges {
|
||||
|
||||
@ContentChildren(DynamicTemplateDirective) contentTemplateList: QueryList<DynamicTemplateDirective>;
|
||||
// tslint:disable-next-line:no-input-rename
|
||||
@Input('templates') inputTemplateList: QueryList<DynamicTemplateDirective>;
|
||||
|
||||
@Input() formId: string;
|
||||
@Input() asBootstrapFormGroup = true;
|
||||
@Input() bindId = true;
|
||||
@Input() context: any | null = null;
|
||||
@Input() group: FormGroup;
|
||||
@Input() hasErrorMessaging = false;
|
||||
@Input() layout: DynamicFormLayout;
|
||||
@Input() model: any;
|
||||
|
||||
/* tslint:disable:no-output-rename */
|
||||
@Output('dfBlur') blur: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||
@Output('dfChange') change: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||
@Output('dfFocus') focus: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||
/* tslint:enable:no-output-rename */
|
||||
|
||||
type: NGBootstrapFormControlType | null;
|
||||
|
||||
static getFormControlType(model: DynamicFormControlModel): NGBootstrapFormControlType | null {
|
||||
|
||||
switch (model.type) {
|
||||
|
||||
case DYNAMIC_FORM_CONTROL_TYPE_ARRAY:
|
||||
return NGBootstrapFormControlType.Array;
|
||||
|
||||
case DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX:
|
||||
return NGBootstrapFormControlType.Checkbox;
|
||||
|
||||
case DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX_GROUP:
|
||||
return (model instanceof DynamicListCheckboxGroupModel) ? NGBootstrapFormControlType.List : NGBootstrapFormControlType.CheckboxGroup;
|
||||
|
||||
case DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER:
|
||||
const datepickerModel = model as DynamicDatePickerModel;
|
||||
|
||||
return datepickerModel.inline ? NGBootstrapFormControlType.Calendar : NGBootstrapFormControlType.DatePicker;
|
||||
|
||||
case DYNAMIC_FORM_CONTROL_TYPE_GROUP:
|
||||
return NGBootstrapFormControlType.Group;
|
||||
|
||||
case DYNAMIC_FORM_CONTROL_TYPE_INPUT:
|
||||
return NGBootstrapFormControlType.Input;
|
||||
|
||||
case DYNAMIC_FORM_CONTROL_TYPE_RADIO_GROUP:
|
||||
return (model instanceof DynamicListRadioGroupModel) ? NGBootstrapFormControlType.List : NGBootstrapFormControlType.RadioGroup;
|
||||
|
||||
case DYNAMIC_FORM_CONTROL_TYPE_SELECT:
|
||||
return NGBootstrapFormControlType.Select;
|
||||
|
||||
case DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA:
|
||||
return NGBootstrapFormControlType.TextArea;
|
||||
|
||||
case DYNAMIC_FORM_CONTROL_TYPE_TIMEPICKER:
|
||||
return NGBootstrapFormControlType.TimePicker;
|
||||
|
||||
case DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD:
|
||||
return NGBootstrapFormControlType.TypeAhead;
|
||||
|
||||
case DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN:
|
||||
return NGBootstrapFormControlType.ScrollableDropdown;
|
||||
|
||||
case DYNAMIC_FORM_CONTROL_TYPE_TAG:
|
||||
return NGBootstrapFormControlType.Tag;
|
||||
|
||||
case DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP:
|
||||
return NGBootstrapFormControlType.Relation;
|
||||
|
||||
case DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER:
|
||||
return NGBootstrapFormControlType.Date;
|
||||
|
||||
case DYNAMIC_FORM_CONTROL_TYPE_LOOKUP:
|
||||
return NGBootstrapFormControlType.Lookup;
|
||||
|
||||
case DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME:
|
||||
return NGBootstrapFormControlType.LookupName;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(protected changeDetectorRef: ChangeDetectorRef, protected layoutService: DynamicFormLayoutService,
|
||||
protected validationService: DynamicFormValidationService) {
|
||||
|
||||
super(changeDetectorRef, layoutService, validationService);
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes) {
|
||||
super.ngOnChanges(changes);
|
||||
}
|
||||
|
||||
if (changes.model) {
|
||||
this.type = DsDynamicFormControlComponent.getFormControlType(this.model);
|
||||
}
|
||||
}
|
||||
|
||||
onChangeLanguage(event) {
|
||||
if (isNotEmpty((this.model as any).value)) {
|
||||
this.onValueChange(event);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
<ds-dynamic-form-control *ngFor="let model of formModel; trackBy: trackByFn"
|
||||
[formId]="formId"
|
||||
[group]="formGroup"
|
||||
[hasErrorMessaging]="model.hasErrorMessages"
|
||||
[hidden]="model.hidden"
|
||||
[layout]="formLayout"
|
||||
[model]="model"
|
||||
[ngClass]="[getClass(model, 'element', 'host'), getClass(model, 'grid', 'host')]"
|
||||
[templates]="templates"
|
||||
(dfBlur)="onEvent($event, 'blur')"
|
||||
(dfChange)="onEvent($event, 'change')"
|
||||
(dfFocus)="onEvent($event, 'focus')"></ds-dynamic-form-control>
|
@@ -0,0 +1,5 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
Component,
|
||||
ContentChildren,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
QueryList,
|
||||
ViewChildren
|
||||
} from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import {
|
||||
DynamicFormComponent,
|
||||
DynamicFormControlEvent,
|
||||
DynamicFormControlModel,
|
||||
DynamicFormLayout,
|
||||
DynamicFormLayoutService,
|
||||
DynamicFormService,
|
||||
DynamicTemplateDirective,
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { DsDynamicFormControlComponent } from './ds-dynamic-form-control.component';
|
||||
import { FormBuilderService } from '../form-builder.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dynamic-form',
|
||||
templateUrl: './ds-dynamic-form.component.html'
|
||||
})
|
||||
export class DsDynamicFormComponent extends DynamicFormComponent {
|
||||
|
||||
@Input() formId: string;
|
||||
@Input() formGroup: FormGroup;
|
||||
@Input() formModel: DynamicFormControlModel[];
|
||||
@Input() formLayout: DynamicFormLayout = null;
|
||||
|
||||
/* tslint:disable:no-output-rename */
|
||||
@Output('dfBlur') blur: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||
@Output('dfChange') change: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||
@Output('dfFocus') focus: EventEmitter<DynamicFormControlEvent> = new EventEmitter<DynamicFormControlEvent>();
|
||||
/* tslint:enable:no-output-rename */
|
||||
|
||||
@ContentChildren(DynamicTemplateDirective) templates: QueryList<DynamicTemplateDirective>;
|
||||
|
||||
@ViewChildren(DsDynamicFormControlComponent) components: QueryList<DsDynamicFormControlComponent>;
|
||||
|
||||
constructor(protected formService: FormBuilderService, protected layoutService: DynamicFormLayoutService) {
|
||||
super(formService, layoutService);
|
||||
}
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
<div class="d-flex">
|
||||
<ds-number-picker
|
||||
tabindex="1"
|
||||
[disabled]="model.disabled"
|
||||
[min]="minYear"
|
||||
[max]="maxYear"
|
||||
[name]="'year'"
|
||||
[size]="4"
|
||||
[(ngModel)]="initialYear"
|
||||
[value]="year"
|
||||
[invalid]="showErrorMessages"
|
||||
[placeholder]='yearPlaceholder'
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onChange($event)"
|
||||
(focus)="onFocus($event)"
|
||||
></ds-number-picker>
|
||||
|
||||
<ds-number-picker
|
||||
tabindex="2"
|
||||
[min]="minMonth"
|
||||
[max]="maxMonth"
|
||||
[name]="'month'"
|
||||
[size]="6"
|
||||
[(ngModel)]="initialMonth"
|
||||
[value]="month"
|
||||
[placeholder]="monthPlaceholder"
|
||||
[disabled]="!year || model.disabled"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onChange($event)"
|
||||
(focus)="onFocus($event)"
|
||||
></ds-number-picker>
|
||||
|
||||
<ds-number-picker
|
||||
tabindex="3"
|
||||
[min]="minDay"
|
||||
[max]="maxDay"
|
||||
[name]="'day'"
|
||||
[size]="2"
|
||||
[(ngModel)]="initialDay"
|
||||
[value]="day"
|
||||
[placeholder]="dayPlaceholder"
|
||||
[disabled]="!month || model.disabled"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onChange($event)"
|
||||
(focus)="onFocus($event)"
|
||||
></ds-number-picker>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
@@ -0,0 +1,3 @@
|
||||
.col-lg-1 {
|
||||
width: auto;
|
||||
}
|
@@ -0,0 +1,255 @@
|
||||
// Load the implementations that should be tested
|
||||
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, inject, TestBed, } from '@angular/core/testing';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
||||
|
||||
import { DsDatePickerComponent } from './date-picker.component';
|
||||
import { DynamicDsDatePickerModel } from './date-picker.model';
|
||||
import { FormBuilderService } from '../../../form-builder.service';
|
||||
|
||||
import { FormComponent } from '../../../../form.component';
|
||||
import { FormService } from '../../../../form.service';
|
||||
import { createTestComponent } from '../../../../../testing/utils';
|
||||
|
||||
export const DATE_TEST_GROUP = new FormGroup({
|
||||
date: new FormControl()
|
||||
});
|
||||
|
||||
export const DATE_TEST_MODEL_CONFIG = {
|
||||
disabled: false,
|
||||
errorMessages: {required: 'You must enter at least the year.'},
|
||||
id: 'date',
|
||||
label: 'Date',
|
||||
name: 'date',
|
||||
placeholder: 'Date',
|
||||
readOnly: false,
|
||||
required: true,
|
||||
toggleIcon: 'fa fa-calendar'
|
||||
};
|
||||
|
||||
describe('DsDatePickerComponent test suite', () => {
|
||||
|
||||
let testComp: TestComponent;
|
||||
let dateComp: DsDatePickerComponent;
|
||||
let testFixture: ComponentFixture<TestComponent>;
|
||||
let dateFixture: ComponentFixture<DsDatePickerComponent>;
|
||||
let html;
|
||||
|
||||
// async beforeEach
|
||||
beforeEach(async(() => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
NgbModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
DsDatePickerComponent,
|
||||
TestComponent,
|
||||
], // declare the test component
|
||||
providers: [
|
||||
ChangeDetectorRef,
|
||||
DsDatePickerComponent,
|
||||
DynamicFormValidationService,
|
||||
FormBuilderService,
|
||||
FormComponent,
|
||||
FormService
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
describe('', () => {
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
html = `
|
||||
<ds-date-picker
|
||||
[bindId]='bindId'
|
||||
[group]='group'
|
||||
[model]='model'
|
||||
[showErrorMessages]='showErrorMessages'
|
||||
(blur)='onBlur($event)'
|
||||
(change)='onValueChange($event)'
|
||||
(focus)='onFocus($event)'></ds-date-picker>`;
|
||||
|
||||
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||
testComp = testFixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create DsDatePickerComponent', inject([DsDatePickerComponent], (app: DsDatePickerComponent) => {
|
||||
|
||||
expect(app).toBeDefined();
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe('', () => {
|
||||
describe('when init model value is empty', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
dateFixture = TestBed.createComponent(DsDatePickerComponent);
|
||||
dateComp = dateFixture.componentInstance; // FormComponent test instance
|
||||
dateComp.group = DATE_TEST_GROUP;
|
||||
dateComp.model = new DynamicDsDatePickerModel(DATE_TEST_MODEL_CONFIG);
|
||||
dateFixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should init component properly', () => {
|
||||
expect(dateComp.initialYear).toBeDefined();
|
||||
expect(dateComp.initialMonth).toBeDefined();
|
||||
expect(dateComp.initialDay).toBeDefined();
|
||||
expect(dateComp.maxYear).toBeDefined();
|
||||
expect(dateComp.disabledMonth).toBeTruthy();
|
||||
expect(dateComp.disabledDay).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set year and enable month field when year field is entered', () => {
|
||||
const event = {
|
||||
field: 'year',
|
||||
value: '1983'
|
||||
};
|
||||
dateComp.onChange(event);
|
||||
|
||||
expect(dateComp.year).toEqual('1983');
|
||||
expect(dateComp.disabledMonth).toBeFalsy();
|
||||
expect(dateComp.disabledDay).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set month and enable day field when month field is entered', () => {
|
||||
const event = {
|
||||
field: 'month',
|
||||
value: '11'
|
||||
};
|
||||
|
||||
dateComp.year = '1983';
|
||||
dateComp.disabledMonth = false;
|
||||
dateFixture.detectChanges();
|
||||
|
||||
dateComp.onChange(event);
|
||||
|
||||
expect(dateComp.year).toEqual('1983');
|
||||
expect(dateComp.month).toEqual('11');
|
||||
expect(dateComp.disabledMonth).toBeFalsy();
|
||||
expect(dateComp.disabledDay).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should set day when day field is entered', () => {
|
||||
const event = {
|
||||
field: 'day',
|
||||
value: '18'
|
||||
};
|
||||
|
||||
dateComp.year = '1983';
|
||||
dateComp.month = '11';
|
||||
dateComp.disabledMonth = false;
|
||||
dateComp.disabledDay = false;
|
||||
dateFixture.detectChanges();
|
||||
|
||||
dateComp.onChange(event);
|
||||
|
||||
expect(dateComp.year).toEqual('1983');
|
||||
expect(dateComp.month).toEqual('11');
|
||||
expect(dateComp.day).toEqual('18');
|
||||
expect(dateComp.disabledMonth).toBeFalsy();
|
||||
expect(dateComp.disabledDay).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should emit blur Event onBlur', () => {
|
||||
spyOn(dateComp.blur, 'emit');
|
||||
dateComp.onBlur(new Event('blur'));
|
||||
expect(dateComp.blur.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit focus Event onFocus', () => {
|
||||
spyOn(dateComp.focus, 'emit');
|
||||
dateComp.onFocus(new Event('focus'));
|
||||
expect(dateComp.focus.emit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when init model value is not empty', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
dateFixture = TestBed.createComponent(DsDatePickerComponent);
|
||||
dateComp = dateFixture.componentInstance; // FormComponent test instance
|
||||
dateComp.group = DATE_TEST_GROUP;
|
||||
dateComp.model = new DynamicDsDatePickerModel(DATE_TEST_MODEL_CONFIG);
|
||||
dateComp.model.value = '1983-11-18';
|
||||
dateFixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should init component properly', () => {
|
||||
expect(dateComp.initialYear).toBeDefined();
|
||||
expect(dateComp.initialMonth).toBeDefined();
|
||||
expect(dateComp.initialDay).toBeDefined();
|
||||
expect(dateComp.maxYear).toBeDefined();
|
||||
expect(dateComp.year).toBe(1983);
|
||||
expect(dateComp.month).toBe(11);
|
||||
expect(dateComp.day).toBe(18);
|
||||
expect(dateComp.disabledMonth).toBeFalsy();
|
||||
expect(dateComp.disabledDay).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should disable month and day fields when year field is canceled', () => {
|
||||
const event = {
|
||||
field: 'year',
|
||||
value: null
|
||||
};
|
||||
dateComp.onChange(event);
|
||||
|
||||
expect(dateComp.year).not.toBeDefined();
|
||||
expect(dateComp.month).not.toBeDefined();
|
||||
expect(dateComp.day).not.toBeDefined();
|
||||
expect(dateComp.disabledMonth).toBeTruthy();
|
||||
expect(dateComp.disabledDay).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should disable day field when month field is canceled', () => {
|
||||
const event = {
|
||||
field: 'month',
|
||||
value: null
|
||||
};
|
||||
dateComp.onChange(event);
|
||||
|
||||
expect(dateComp.year).toBe(1983);
|
||||
expect(dateComp.month).not.toBeDefined();
|
||||
expect(dateComp.day).not.toBeDefined();
|
||||
expect(dateComp.disabledMonth).toBeFalsy();
|
||||
expect(dateComp.disabledDay).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not disable day field when day field is canceled', () => {
|
||||
const event = {
|
||||
field: 'day',
|
||||
value: null
|
||||
};
|
||||
dateComp.onChange(event);
|
||||
|
||||
expect(dateComp.year).toBe(1983);
|
||||
expect(dateComp.month).toBe(11);
|
||||
expect(dateComp.day).not.toBeDefined();
|
||||
expect(dateComp.disabledMonth).toBeFalsy();
|
||||
expect(dateComp.disabledDay).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// declare a test component
|
||||
@Component({
|
||||
selector: 'ds-test-cmp',
|
||||
template: ``
|
||||
})
|
||||
class TestComponent {
|
||||
|
||||
group = DATE_TEST_GROUP;
|
||||
|
||||
model = new DynamicDsDatePickerModel(DATE_TEST_MODEL_CONFIG);
|
||||
|
||||
showErrorMessages = false;
|
||||
|
||||
}
|
@@ -0,0 +1,172 @@
|
||||
import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { DynamicDsDatePickerModel } from './date-picker.model';
|
||||
import { hasNoValue, hasValue, isNotEmpty } from '../../../../../empty.util';
|
||||
|
||||
export const DS_DATE_PICKER_SEPARATOR = '-';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-date-picker',
|
||||
styleUrls: ['./date-picker.component.scss'],
|
||||
templateUrl: './date-picker.component.html',
|
||||
})
|
||||
|
||||
export class DsDatePickerComponent implements OnInit {
|
||||
@Input() bindId = true;
|
||||
@Input() group: FormGroup;
|
||||
@Input() model: DynamicDsDatePickerModel;
|
||||
@Input() showErrorMessages = false;
|
||||
// @Input()
|
||||
// minDate;
|
||||
// @Input()
|
||||
// maxDate;
|
||||
|
||||
@Output() selected = new EventEmitter<number>();
|
||||
@Output() remove = new EventEmitter<number>();
|
||||
@Output() blur = new EventEmitter<any>();
|
||||
@Output() change = new EventEmitter<any>();
|
||||
@Output() focus = new EventEmitter<any>();
|
||||
|
||||
initialYear: number;
|
||||
initialMonth: number;
|
||||
initialDay: number;
|
||||
|
||||
year: any;
|
||||
month: any;
|
||||
day: any;
|
||||
|
||||
minYear: 0;
|
||||
maxYear: number;
|
||||
minMonth = 1;
|
||||
maxMonth = 12;
|
||||
minDay = 1;
|
||||
maxDay = 31;
|
||||
|
||||
yearPlaceholder = 'year';
|
||||
monthPlaceholder = 'month';
|
||||
dayPlaceholder = 'day';
|
||||
|
||||
disabledMonth = true;
|
||||
disabledDay = true;
|
||||
|
||||
ngOnInit() {
|
||||
const now = new Date();
|
||||
this.initialYear = now.getFullYear();
|
||||
this.initialMonth = now.getMonth() + 1;
|
||||
this.initialDay = now.getDate();
|
||||
|
||||
if (this.model.value && this.model.value !== null) {
|
||||
const values = this.model.value.toString().split(DS_DATE_PICKER_SEPARATOR);
|
||||
if (values.length > 0) {
|
||||
this.initialYear = parseInt(values[0], 10);
|
||||
this.year = this.initialYear;
|
||||
this.disabledMonth = false;
|
||||
}
|
||||
if (values.length > 1) {
|
||||
this.initialMonth = parseInt(values[1], 10);
|
||||
this.month = this.initialMonth;
|
||||
this.disabledDay = false;
|
||||
}
|
||||
if (values.length > 2) {
|
||||
this.initialDay = parseInt(values[2], 10);
|
||||
this.day = this.initialDay;
|
||||
}
|
||||
}
|
||||
|
||||
this.maxYear = this.initialYear + 100;
|
||||
|
||||
}
|
||||
|
||||
onBlur(event) {
|
||||
this.blur.emit();
|
||||
}
|
||||
|
||||
onChange(event) {
|
||||
// update year-month-day
|
||||
switch (event.field) {
|
||||
case 'year': {
|
||||
if (event.value !== null) {
|
||||
this.year = event.value;
|
||||
} else {
|
||||
this.year = undefined;
|
||||
this.month = undefined;
|
||||
this.day = undefined;
|
||||
this.disabledMonth = true;
|
||||
this.disabledDay = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'month': {
|
||||
if (event.value !== null) {
|
||||
this.month = event.value;
|
||||
} else {
|
||||
this.month = undefined;
|
||||
this.day = undefined;
|
||||
this.disabledDay = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'day': {
|
||||
if (event.value !== null) {
|
||||
this.day = event.value;
|
||||
} else {
|
||||
this.day = undefined;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// set max for days by month/year
|
||||
if (!this.disabledDay) {
|
||||
const month = this.month ? this.month - 1 : 0;
|
||||
const date = new Date(this.year, month, 1);
|
||||
this.maxDay = this.getLastDay(date);
|
||||
if (this.day > this.maxDay) {
|
||||
this.day = this.maxDay;
|
||||
}
|
||||
}
|
||||
|
||||
// Manage disable
|
||||
if (hasValue(this.year) && event.field === 'year') {
|
||||
this.disabledMonth = false;
|
||||
} else if (hasValue(this.month) && event.field === 'month') {
|
||||
this.disabledDay = false;
|
||||
}
|
||||
|
||||
// update value
|
||||
let value = null;
|
||||
if (hasValue(this.year)) {
|
||||
let yyyy = this.year.toString();
|
||||
while (yyyy.length < 4) {
|
||||
yyyy = '0' + yyyy;
|
||||
}
|
||||
value = yyyy;
|
||||
}
|
||||
if (hasValue(this.month)) {
|
||||
const mm = this.month.toString().length === 1
|
||||
? '0' + this.month.toString()
|
||||
: this.month.toString();
|
||||
value += DS_DATE_PICKER_SEPARATOR + mm;
|
||||
}
|
||||
if (hasValue(this.day)) {
|
||||
const dd = this.day.toString().length === 1
|
||||
? '0' + this.day.toString()
|
||||
: this.day.toString();
|
||||
value += DS_DATE_PICKER_SEPARATOR + dd;
|
||||
}
|
||||
|
||||
this.model.valueUpdates.next(value);
|
||||
this.change.emit(value);
|
||||
}
|
||||
|
||||
onFocus(event) {
|
||||
this.focus.emit(event);
|
||||
}
|
||||
|
||||
getLastDay(date: Date) {
|
||||
// Last Day of the same month (+1 month, -1 day)
|
||||
date.setMonth(date.getMonth() + 1, 0);
|
||||
return date.getDate();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
import { DynamicDateControlModel, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
|
||||
import { DynamicDateControlModelConfig } from '@ng-dynamic-forms/core/src/model/dynamic-date-control.model';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
|
||||
export const DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER = 'DATE';
|
||||
|
||||
/**
|
||||
* Dynamic Date Picker Model class
|
||||
*/
|
||||
export class DynamicDsDatePickerModel extends DynamicDateControlModel {
|
||||
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER;
|
||||
valueUpdates: Subject<any>;
|
||||
malformedDate: boolean;
|
||||
hasLanguages = false;
|
||||
|
||||
constructor(config: DynamicDateControlModelConfig, layout?: DynamicFormControlLayout) {
|
||||
super(config, layout);
|
||||
this.malformedDate = false;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicFormGroupModelConfig, serializable } from '@ng-dynamic-forms/core';
|
||||
import { isNotEmpty } from '../../../../empty.util';
|
||||
import { DsDynamicInputModel } from './ds-dynamic-input.model';
|
||||
import { AuthorityValueModel } from '../../../../../core/integration/models/authority-value.model';
|
||||
import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model';
|
||||
|
||||
export const CONCAT_GROUP_SUFFIX = '_CONCAT_GROUP';
|
||||
export const CONCAT_FIRST_INPUT_SUFFIX = '_CONCAT_FIRST_INPUT';
|
||||
export const CONCAT_SECOND_INPUT_SUFFIX = '_CONCAT_SECOND_INPUT';
|
||||
|
||||
export interface DynamicConcatModelConfig extends DynamicFormGroupModelConfig {
|
||||
separator: string;
|
||||
}
|
||||
|
||||
export class DynamicConcatModel extends DynamicFormGroupModel {
|
||||
|
||||
@serializable() separator: string;
|
||||
@serializable() hasLanguages = false;
|
||||
isCustomGroup = true;
|
||||
|
||||
constructor(config: DynamicConcatModelConfig, layout?: DynamicFormControlLayout) {
|
||||
|
||||
super(config, layout);
|
||||
|
||||
this.separator = config.separator + ' ';
|
||||
}
|
||||
|
||||
get value() {
|
||||
const firstValue = (this.get(0) as DsDynamicInputModel).value;
|
||||
const secondValue = (this.get(1) as DsDynamicInputModel).value;
|
||||
if (isNotEmpty(firstValue) && isNotEmpty(secondValue)) {
|
||||
return new FormFieldMetadataValueObject(firstValue + this.separator + secondValue);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
set value(value: string | FormFieldMetadataValueObject) {
|
||||
let values;
|
||||
if (typeof value === 'string') {
|
||||
values = value ? value.split(this.separator) : [null, null];
|
||||
} else {
|
||||
values = value ? value.value.split(this.separator) : [null, null];
|
||||
}
|
||||
|
||||
if (values.length > 1) {
|
||||
(this.get(0) as DsDynamicInputModel).valueUpdates.next(values[0]);
|
||||
(this.get(1) as DsDynamicInputModel).valueUpdates.next(values[1]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
DynamicFormControlLayout,
|
||||
DynamicInputModel,
|
||||
DynamicInputModelConfig,
|
||||
serializable
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
|
||||
import { LanguageCode } from '../../models/form-field-language-value.model';
|
||||
import { AuthorityOptions } from '../../../../../core/integration/models/authority-options.model';
|
||||
import { hasValue } from '../../../../empty.util';
|
||||
import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model';
|
||||
|
||||
export interface DsDynamicInputModelConfig extends DynamicInputModelConfig {
|
||||
authorityOptions?: AuthorityOptions;
|
||||
languageCodes?: LanguageCode[];
|
||||
language?: string;
|
||||
value?: any;
|
||||
}
|
||||
|
||||
export class DsDynamicInputModel extends DynamicInputModel {
|
||||
|
||||
@serializable() authorityOptions: AuthorityOptions;
|
||||
@serializable() private _languageCodes: LanguageCode[];
|
||||
@serializable() private _language: string;
|
||||
@serializable() languageUpdates: Subject<string>;
|
||||
|
||||
constructor(config: DsDynamicInputModelConfig, layout?: DynamicFormControlLayout) {
|
||||
super(config, layout);
|
||||
|
||||
this.readOnly = config.readOnly;
|
||||
this.value = config.value;
|
||||
this.language = config.language;
|
||||
if (!this.language) {
|
||||
// TypeAhead
|
||||
if (config.value instanceof FormFieldMetadataValueObject) {
|
||||
this.language = config.value.language;
|
||||
} else if (Array.isArray(config.value)) {
|
||||
// Tag of Authority
|
||||
if (config.value[0].language) {
|
||||
this.language = config.value[0].language;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.languageCodes = config.languageCodes;
|
||||
|
||||
this.languageUpdates = new Subject<string>();
|
||||
this.languageUpdates.subscribe((lang: string) => {
|
||||
this.language = lang;
|
||||
});
|
||||
|
||||
this.authorityOptions = config.authorityOptions;
|
||||
}
|
||||
|
||||
get hasAuthority(): boolean {
|
||||
return this.authorityOptions && hasValue(this.authorityOptions.name);
|
||||
}
|
||||
|
||||
get hasLanguages(): boolean {
|
||||
if (this.languageCodes && this.languageCodes.length > 1) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
get language(): string {
|
||||
return this._language;
|
||||
}
|
||||
|
||||
set language(language: string) {
|
||||
this._language = language;
|
||||
}
|
||||
|
||||
get languageCodes(): LanguageCode[] {
|
||||
return this._languageCodes;
|
||||
}
|
||||
|
||||
set languageCodes(languageCodes: LanguageCode[]) {
|
||||
this._languageCodes = languageCodes;
|
||||
if (!this.language || this.language === null || this.language === '') {
|
||||
this.language = this.languageCodes ? this.languageCodes[0].code : null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicInputModelConfig, serializable } from '@ng-dynamic-forms/core';
|
||||
import { DsDynamicInputModel, DsDynamicInputModelConfig } from './ds-dynamic-input.model';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
import { DynamicFormGroupModelConfig } from '@ng-dynamic-forms/core/src/model/form-group/dynamic-form-group.model';
|
||||
import { LanguageCode } from '../../models/form-field-language-value.model';
|
||||
|
||||
export const QUALDROP_GROUP_SUFFIX = '_QUALDROP_GROUP';
|
||||
export const QUALDROP_METADATA_SUFFIX = '_QUALDROP_METADATA';
|
||||
export const QUALDROP_VALUE_SUFFIX = '_QUALDROP_VALUE';
|
||||
|
||||
export interface DsDynamicQualdropModelConfig extends DynamicFormGroupModelConfig {
|
||||
languageCodes?: LanguageCode[];
|
||||
language?: string;
|
||||
readOnly: boolean;
|
||||
}
|
||||
|
||||
export class DynamicQualdropModel extends DynamicFormGroupModel {
|
||||
@serializable() private _language: string;
|
||||
@serializable() private _languageCodes: LanguageCode[];
|
||||
@serializable() languageUpdates: Subject<string>;
|
||||
@serializable() hasLanguages = false;
|
||||
@serializable() readOnly: boolean;
|
||||
isCustomGroup = true;
|
||||
|
||||
constructor(config: DsDynamicQualdropModelConfig, layout?: DynamicFormControlLayout) {
|
||||
super(config, layout);
|
||||
|
||||
this.readOnly = config.readOnly;
|
||||
this.language = config.language;
|
||||
this.languageCodes = config.languageCodes;
|
||||
|
||||
this.languageUpdates = new Subject<string>();
|
||||
this.languageUpdates.subscribe((lang: string) => {
|
||||
this.language = lang;
|
||||
});
|
||||
}
|
||||
|
||||
get value() {
|
||||
return (this.get(1) as DsDynamicInputModel).value;
|
||||
}
|
||||
|
||||
get qualdropId(): string {
|
||||
return (this.get(0) as DsDynamicInputModel).value.toString();
|
||||
}
|
||||
|
||||
get language(): string {
|
||||
return this._language;
|
||||
}
|
||||
|
||||
set language(language: string) {
|
||||
this._language = language;
|
||||
}
|
||||
|
||||
get languageCodes(): LanguageCode[] {
|
||||
return this._languageCodes;
|
||||
}
|
||||
|
||||
set languageCodes(languageCodes: LanguageCode[]) {
|
||||
this._languageCodes = languageCodes;
|
||||
if (!this.language || this.language === null || this.language === '') {
|
||||
this.language = this.languageCodes ? this.languageCodes[0].code : null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
DYNAMIC_FORM_CONTROL_TYPE_ARRAY,
|
||||
DynamicFormArrayModel, DynamicFormArrayModelConfig, DynamicFormControlLayout,
|
||||
serializable
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_TAG } from './tag/dynamic-tag.model';
|
||||
|
||||
export interface DynamicRowArrayModelConfig extends DynamicFormArrayModelConfig {
|
||||
notRepeteable: boolean;
|
||||
}
|
||||
|
||||
export class DynamicRowArrayModel extends DynamicFormArrayModel {
|
||||
@serializable() notRepeteable = false;
|
||||
isRowArray = true;
|
||||
|
||||
constructor(config: DynamicRowArrayModelConfig, layout?: DynamicFormControlLayout) {
|
||||
super(config, layout);
|
||||
this.notRepeteable = config.notRepeteable;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
import { DynamicFormGroupModel } from '@ng-dynamic-forms/core';
|
||||
|
||||
export class DynamicRowGroupModel extends DynamicFormGroupModel {
|
||||
isRowGroup = true;
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
|
||||
import { DsDynamicInputModel, DsDynamicInputModelConfig } from './ds-dynamic-input.model';
|
||||
|
||||
export interface DsDynamicTextAreaModelConfig extends DsDynamicInputModelConfig {
|
||||
cols?: number;
|
||||
rows?: number;
|
||||
wrap?: string;
|
||||
}
|
||||
|
||||
export class DsDynamicTextAreaModel extends DsDynamicInputModel {
|
||||
@serializable() cols: number;
|
||||
@serializable() rows: number;
|
||||
@serializable() wrap: string;
|
||||
@serializable() type = DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA;
|
||||
|
||||
constructor(config: DsDynamicTextAreaModelConfig, layout?: DynamicFormControlLayout) {
|
||||
super(config, layout);
|
||||
|
||||
this.cols = config.cols;
|
||||
this.rows = config.rows;
|
||||
this.wrap = config.wrap;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,72 @@
|
||||
<a *ngIf="!(formCollapsed | async)"
|
||||
class="close position-relative"
|
||||
ngbTooltip="{{'form.group-collapse-help' | translate}}"
|
||||
placement="left">
|
||||
<span class="fa fa-angle-up fa-fw fa-2x"
|
||||
aria-hidden="true"
|
||||
(click)="collapseForm()"></span>
|
||||
</a>
|
||||
<a *ngIf="(formCollapsed | async)"
|
||||
class="close position-relative"
|
||||
ngbTooltip="{{'form.group-expand-help' | translate}}"
|
||||
placement="left">
|
||||
<span class="fa fa-angle-down fa-fw fa-2x"
|
||||
aria-hidden="true"
|
||||
(click)="expandForm()"></span>
|
||||
</a>
|
||||
|
||||
<div class="pt-2" [ngClass]="{'border-top': !showErrorMessages, 'border border-danger': showErrorMessages}">
|
||||
<div *ngIf="!(formCollapsed | async)" class="pl-2 row" @shrinkInOut>
|
||||
<ds-form #formRef="formComponent"
|
||||
class="col-sm-12 col-md-8 col-lg-9 col-xl-10 pl-0"
|
||||
[formId]="formId"
|
||||
[formModel]="formModel"
|
||||
[displaySubmit]="false"
|
||||
[emitChange]="false"
|
||||
(dfBlur)="onBlur($event)"
|
||||
(dfFocus)="onFocus($event)"></ds-form>
|
||||
|
||||
|
||||
<div *ngIf="!(formCollapsed | async)" class="col p-0 m-0 d-flex justify-content-center align-items-center">
|
||||
|
||||
<button type="button"
|
||||
class="btn btn-link"
|
||||
[disabled]="isMandatoryFieldEmpty()"
|
||||
(click)="save()">
|
||||
<i class="fa fa-save text-primary fa-2x"
|
||||
aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-link"
|
||||
[disabled]="!editMode"
|
||||
(click)="delete()">
|
||||
<i class="fa fa-trash text-danger fa-2x"
|
||||
aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-link"
|
||||
[disabled]="isMandatoryFieldEmpty()"
|
||||
(click)="clear()">
|
||||
<i class="fa fa-undo fa-2x"
|
||||
aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex">
|
||||
<div *ngIf="!chips.hasItems()">
|
||||
<input type="text"
|
||||
class="border-0 form-control-plaintext tag-input mt-1 mb-1 pl-2 text-muted"
|
||||
readonly
|
||||
tabindex="-1"
|
||||
value="{{'form.no-value' | translate}}">
|
||||
</div>
|
||||
<ds-chips
|
||||
*ngIf="chips.hasItems()"
|
||||
[chips]="chips"
|
||||
[editable]="true"
|
||||
(selected)="onChipSelected($event)"></ds-chips>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,3 @@
|
||||
.close {
|
||||
top: -2.5rem;
|
||||
}
|
@@ -0,0 +1,315 @@
|
||||
// Load the implementations that should be tested
|
||||
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, inject, TestBed, } from '@angular/core/testing';
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
import { DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/observable/of';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { DsDynamicGroupComponent } from './dynamic-group.components';
|
||||
import { DynamicGroupModel, DynamicGroupModelConfig } from './dynamic-group.model';
|
||||
import { FormRowModel, SubmissionFormsModel } from '../../../../../../core/shared/config/config-submission-forms.model';
|
||||
import { FormFieldModel } from '../../../models/form-field.model';
|
||||
import { FormBuilderService } from '../../../form-builder.service';
|
||||
import { FormService } from '../../../../form.service';
|
||||
import { GLOBAL_CONFIG } from '../../../../../../../config';
|
||||
import { FormComponent } from '../../../../form.component';
|
||||
import { AppState } from '../../../../../../app.reducer';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { Chips } from '../../../../../chips/models/chips.model';
|
||||
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
|
||||
import { DsDynamicInputModel } from '../ds-dynamic-input.model';
|
||||
import { createTestComponent } from '../../../../../testing/utils';
|
||||
|
||||
export const FORM_GROUP_TEST_MODEL_CONFIG = {
|
||||
disabled: false,
|
||||
errorMessages: {required: 'You must specify at least one author.'},
|
||||
formConfiguration: [{
|
||||
fields: [{
|
||||
hints: 'Enter the name of the author.',
|
||||
input: {type: 'onebox'},
|
||||
label: 'Author',
|
||||
languageCodes: [],
|
||||
mandatory: 'true',
|
||||
mandatoryMessage: 'Required field!',
|
||||
repeatable: false,
|
||||
selectableMetadata: [{
|
||||
authority: 'RPAuthority',
|
||||
closed: false,
|
||||
metadata: 'dc.contributor.author'
|
||||
}],
|
||||
} as FormFieldModel]
|
||||
} as FormRowModel, {
|
||||
fields: [{
|
||||
hints: 'Enter the affiliation of the author.',
|
||||
input: {type: 'onebox'},
|
||||
label: 'Affiliation',
|
||||
languageCodes: [],
|
||||
mandatory: 'false',
|
||||
repeatable: false,
|
||||
selectableMetadata: [{
|
||||
authority: 'OUAuthority',
|
||||
closed: false,
|
||||
metadata: 'local.contributor.affiliation'
|
||||
}]
|
||||
} as FormFieldModel]
|
||||
} as FormRowModel],
|
||||
id: 'dc_contributor_author',
|
||||
label: 'Authors',
|
||||
mandatoryField: 'dc.contributor.author',
|
||||
name: 'dc.contributor.author',
|
||||
placeholder: 'Authors',
|
||||
readOnly: false,
|
||||
relationFields: ['local.contributor.affiliation'],
|
||||
required: true,
|
||||
scopeUUID: '43fe1f8c-09a6-4fcf-9c78-5d4fed8f2c8f',
|
||||
submissionScope: undefined,
|
||||
validators: {required: null}
|
||||
} as DynamicGroupModelConfig;
|
||||
|
||||
export const FORM_GROUP_TEST_GROUP = new FormGroup({
|
||||
dc_contributor_author: new FormControl(),
|
||||
});
|
||||
|
||||
describe('DsDynamicGroupComponent test suite', () => {
|
||||
const config = {
|
||||
form: {
|
||||
validatorMap: {
|
||||
required: 'required',
|
||||
regex: 'pattern'
|
||||
}
|
||||
}
|
||||
} as any;
|
||||
let testComp: TestComponent;
|
||||
let groupComp: DsDynamicGroupComponent;
|
||||
let testFixture: ComponentFixture<TestComponent>;
|
||||
let groupFixture: ComponentFixture<DsDynamicGroupComponent>;
|
||||
let modelValue: any;
|
||||
let html;
|
||||
let control1: FormControl;
|
||||
let model1: DsDynamicInputModel;
|
||||
let control2: FormControl;
|
||||
let model2: DsDynamicInputModel;
|
||||
|
||||
const store: Store<AppState> = jasmine.createSpyObj('store', {
|
||||
dispatch: {},
|
||||
select: Observable.of(true)
|
||||
});
|
||||
|
||||
// async beforeEach
|
||||
beforeEach(async(() => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
BrowserAnimationsModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbModule.forRoot(),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
FormComponent,
|
||||
DsDynamicGroupComponent,
|
||||
TestComponent,
|
||||
], // declare the test component
|
||||
providers: [
|
||||
ChangeDetectorRef,
|
||||
DsDynamicGroupComponent,
|
||||
DynamicFormValidationService,
|
||||
FormBuilderService,
|
||||
FormComponent,
|
||||
FormService,
|
||||
{provide: GLOBAL_CONFIG, useValue: config},
|
||||
{provide: Store, useValue: store},
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
describe('', () => {
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
html = `
|
||||
<ds-dynamic-group [model]="model"
|
||||
[formId]="formId"
|
||||
[group]="group"
|
||||
[showErrorMessages]="showErrorMessages"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onValueChange($event)"
|
||||
(focus)="onFocus($event)"></ds-dynamic-group>`;
|
||||
|
||||
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||
testComp = testFixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create DsDynamicGroupComponent', inject([DsDynamicGroupComponent], (app: DsDynamicGroupComponent) => {
|
||||
|
||||
expect(app).toBeDefined();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when init model value is empty', () => {
|
||||
beforeEach(inject([FormBuilderService], (service: FormBuilderService) => {
|
||||
|
||||
groupFixture = TestBed.createComponent(DsDynamicGroupComponent);
|
||||
groupComp = groupFixture.componentInstance; // FormComponent test instance
|
||||
groupComp.formId = 'testForm';
|
||||
groupComp.group = FORM_GROUP_TEST_GROUP;
|
||||
groupComp.model = new DynamicGroupModel(FORM_GROUP_TEST_MODEL_CONFIG);
|
||||
groupComp.showErrorMessages = false;
|
||||
groupFixture.detectChanges();
|
||||
|
||||
control1 = service.getFormControlById('dc_contributor_author', (groupComp as any).formRef.formGroup, groupComp.formModel) as FormControl;
|
||||
model1 = service.findById('dc_contributor_author', groupComp.formModel) as DsDynamicInputModel;
|
||||
control2 = service.getFormControlById('local_contributor_affiliation', (groupComp as any).formRef.formGroup, groupComp.formModel) as FormControl;
|
||||
model2 = service.findById('local_contributor_affiliation', groupComp.formModel) as DsDynamicInputModel;
|
||||
|
||||
// spyOn(store, 'dispatch');
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
groupFixture.destroy();
|
||||
groupComp = null;
|
||||
});
|
||||
|
||||
it('should init component properly', inject([FormBuilderService], (service: FormBuilderService) => {
|
||||
const formConfig = {rows: groupComp.model.formConfiguration} as SubmissionFormsModel;
|
||||
const formModel = service.modelFromConfiguration(formConfig, groupComp.model.scopeUUID, {}, groupComp.model.submissionScope, groupComp.model.readOnly);
|
||||
const chips = new Chips([], 'value', 'dc.contributor.author');
|
||||
|
||||
expect(groupComp.formCollapsed).toEqual(Observable.of(false));
|
||||
expect(groupComp.formModel.length).toEqual(formModel.length);
|
||||
expect(groupComp.chips.getChipsItems()).toEqual(chips.getChipsItems());
|
||||
}));
|
||||
|
||||
it('should save a new chips item', () => {
|
||||
control1.setValue('test author');
|
||||
(model1 as any).value = new FormFieldMetadataValueObject('test author');
|
||||
control2.setValue('test affiliation');
|
||||
(model2 as any).value = new FormFieldMetadataValueObject('test affiliation');
|
||||
modelValue = [{
|
||||
'dc.contributor.author': new FormFieldMetadataValueObject('test author'),
|
||||
'local.contributor.affiliation': new FormFieldMetadataValueObject('test affiliation')
|
||||
}];
|
||||
groupFixture.detectChanges();
|
||||
|
||||
const buttons = groupFixture.debugElement.nativeElement.querySelectorAll('button');
|
||||
const btnEl = buttons[0];
|
||||
btnEl.click();
|
||||
|
||||
expect(groupComp.chips.getChipsItems()).toEqual(modelValue);
|
||||
expect(groupComp.formCollapsed).toEqual(Observable.of(true));
|
||||
});
|
||||
|
||||
it('should clear form inputs', () => {
|
||||
control1.setValue('test author');
|
||||
(model1 as any).value = new FormFieldMetadataValueObject('test author');
|
||||
control2.setValue('test affiliation');
|
||||
(model2 as any).value = new FormFieldMetadataValueObject('test affiliation');
|
||||
|
||||
groupFixture.detectChanges();
|
||||
|
||||
const buttons = groupFixture.debugElement.nativeElement.querySelectorAll('button');
|
||||
const btnEl = buttons[2];
|
||||
btnEl.click();
|
||||
|
||||
expect(control1.value).toBeNull();
|
||||
expect(control2.value).toBeNull();
|
||||
expect(groupComp.formCollapsed).toEqual(Observable.of(false));
|
||||
});
|
||||
});
|
||||
|
||||
describe('when init model value is not empty', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
groupFixture = TestBed.createComponent(DsDynamicGroupComponent);
|
||||
groupComp = groupFixture.componentInstance; // FormComponent test instance
|
||||
groupComp.formId = 'testForm';
|
||||
groupComp.group = FORM_GROUP_TEST_GROUP;
|
||||
groupComp.model = new DynamicGroupModel(FORM_GROUP_TEST_MODEL_CONFIG);
|
||||
modelValue = [{
|
||||
'dc.contributor.author': new FormFieldMetadataValueObject('test author'),
|
||||
'local.contributor.affiliation': new FormFieldMetadataValueObject('test affiliation')
|
||||
}];
|
||||
groupComp.model.value = modelValue;
|
||||
groupComp.showErrorMessages = false;
|
||||
groupFixture.detectChanges();
|
||||
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
groupFixture.destroy();
|
||||
groupComp = null;
|
||||
});
|
||||
|
||||
it('should init component properly', inject([FormBuilderService], (service: FormBuilderService) => {
|
||||
const formConfig = {rows: groupComp.model.formConfiguration} as SubmissionFormsModel;
|
||||
const formModel = service.modelFromConfiguration(formConfig, groupComp.model.scopeUUID, {}, groupComp.model.submissionScope, groupComp.model.readOnly);
|
||||
const chips = new Chips(modelValue, 'value', 'dc.contributor.author');
|
||||
|
||||
expect(groupComp.formCollapsed).toEqual(Observable.of(true));
|
||||
expect(groupComp.formModel.length).toEqual(formModel.length);
|
||||
expect(groupComp.chips.getChipsItems()).toEqual(chips.getChipsItems());
|
||||
}));
|
||||
|
||||
it('should modify existing chips item', inject([FormBuilderService], (service: FormBuilderService) => {
|
||||
groupComp.onChipSelected(0);
|
||||
groupFixture.detectChanges();
|
||||
|
||||
control1 = service.getFormControlById('dc_contributor_author', (groupComp as any).formRef.formGroup, groupComp.formModel) as FormControl;
|
||||
model1 = service.findById('dc_contributor_author', groupComp.formModel) as DsDynamicInputModel;
|
||||
|
||||
control1.setValue('test author modify');
|
||||
(model1 as any).value = new FormFieldMetadataValueObject('test author modify');
|
||||
|
||||
modelValue = [{
|
||||
'dc.contributor.author': new FormFieldMetadataValueObject('test author modify'),
|
||||
'local.contributor.affiliation': new FormFieldMetadataValueObject('test affiliation')
|
||||
}];
|
||||
groupFixture.detectChanges();
|
||||
|
||||
const buttons = groupFixture.debugElement.nativeElement.querySelectorAll('button');
|
||||
const btnEl = buttons[0];
|
||||
btnEl.click();
|
||||
|
||||
groupFixture.detectChanges();
|
||||
|
||||
expect(groupComp.chips.getChipsItems()).toEqual(modelValue);
|
||||
expect(groupComp.formCollapsed).toEqual(Observable.of(true));
|
||||
}));
|
||||
|
||||
it('should delete existing chips item', () => {
|
||||
groupComp.onChipSelected(0);
|
||||
groupFixture.detectChanges();
|
||||
|
||||
const buttons = groupFixture.debugElement.nativeElement.querySelectorAll('button');
|
||||
const btnEl = buttons[1];
|
||||
btnEl.click();
|
||||
|
||||
expect(groupComp.chips.getChipsItems()).toEqual([]);
|
||||
expect(groupComp.formCollapsed).toEqual(Observable.of(false));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// declare a test component
|
||||
@Component({
|
||||
selector: 'ds-test-cmp',
|
||||
template: ``
|
||||
})
|
||||
class TestComponent {
|
||||
|
||||
group = FORM_GROUP_TEST_GROUP;
|
||||
|
||||
groupModelConfig = FORM_GROUP_TEST_MODEL_CONFIG;
|
||||
|
||||
model = new DynamicGroupModel(this.groupModelConfig);
|
||||
|
||||
showErrorMessages = false;
|
||||
|
||||
}
|
@@ -0,0 +1,240 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Inject,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { DynamicFormControlModel, DynamicFormGroupModel, DynamicInputModel } from '@ng-dynamic-forms/core';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { DynamicGroupModel, PLACEHOLDER_PARENT_METADATA } from './dynamic-group.model';
|
||||
import { FormBuilderService } from '../../../form-builder.service';
|
||||
import { SubmissionFormsModel } from '../../../../../../core/shared/config/config-submission-forms.model';
|
||||
import { FormService } from '../../../../form.service';
|
||||
import { FormComponent } from '../../../../form.component';
|
||||
import { Chips } from '../../../../../chips/models/chips.model';
|
||||
import { hasValue, isEmpty, isNotEmpty } from '../../../../../empty.util';
|
||||
import { shrinkInOut } from '../../../../../animations/shrink';
|
||||
import { ChipsItem } from '../../../../../chips/models/chips-item.model';
|
||||
import { GlobalConfig } from '../../../../../../../config/global-config.interface';
|
||||
import { GLOBAL_CONFIG } from '../../../../../../../config';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { hasOnlyEmptyProperties } from '../../../../../object.util';
|
||||
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
|
||||
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dynamic-group',
|
||||
styleUrls: ['./dynamic-group.component.scss'],
|
||||
templateUrl: './dynamic-group.component.html',
|
||||
animations: [shrinkInOut]
|
||||
})
|
||||
export class DsDynamicGroupComponent implements OnDestroy, OnInit {
|
||||
|
||||
@Input() formId: string;
|
||||
@Input() group: FormGroup;
|
||||
@Input() model: DynamicGroupModel;
|
||||
@Input() showErrorMessages = false;
|
||||
|
||||
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() change: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
public chips: Chips;
|
||||
public formCollapsed = Observable.of(false);
|
||||
public formModel: DynamicFormControlModel[];
|
||||
public editMode = false;
|
||||
|
||||
private selectedChipItem: ChipsItem;
|
||||
private subs: Subscription[] = [];
|
||||
|
||||
@ViewChild('formRef') private formRef: FormComponent;
|
||||
|
||||
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||
private formBuilderService: FormBuilderService,
|
||||
private formService: FormService,
|
||||
private cdr: ChangeDetectorRef) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
const config = {rows: this.model.formConfiguration} as SubmissionFormsModel;
|
||||
if (!this.model.isEmpty()) {
|
||||
this.formCollapsed = Observable.of(true);
|
||||
}
|
||||
this.model.valueUpdates.subscribe((value: any[]) => {
|
||||
if ((isNotEmpty(value) && !(value.length === 1 && hasOnlyEmptyProperties(value[0])))) {
|
||||
this.collapseForm();
|
||||
} else {
|
||||
this.expandForm();
|
||||
}
|
||||
// this.formCollapsed = (isNotEmpty(value) && !(value.length === 1 && hasOnlyEmptyProperties(value[0]))) ? Observable.of(true) : Observable.of(false);
|
||||
});
|
||||
|
||||
this.formId = this.formService.getUniqueId(this.model.id);
|
||||
this.formModel = this.formBuilderService.modelFromConfiguration(
|
||||
config,
|
||||
this.model.scopeUUID,
|
||||
{},
|
||||
this.model.submissionScope,
|
||||
this.model.readOnly);
|
||||
const initChipsValue = this.model.isEmpty() ? [] : this.model.value;
|
||||
this.chips = new Chips(
|
||||
initChipsValue,
|
||||
'value',
|
||||
this.model.mandatoryField);
|
||||
this.subs.push(
|
||||
this.chips.chipsItems
|
||||
.subscribe((subItems: any[]) => {
|
||||
const items = this.chips.getChipsItems();
|
||||
// Does not emit change if model value is equal to the current value
|
||||
if (!isEqual(items, this.model.value)) {
|
||||
// if ((isNotEmpty(items) && !this.model.isEmpty()) || (isEmpty(items) && !this.model.isEmpty())) {
|
||||
if (!(isEmpty(items) && this.model.isEmpty())) {
|
||||
this.model.valueUpdates.next(items);
|
||||
this.change.emit();
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
isMandatoryFieldEmpty() {
|
||||
// formModel[0].group[0].value == null
|
||||
let res = true;
|
||||
this.formModel.forEach((row) => {
|
||||
const modelRow = row as DynamicFormGroupModel;
|
||||
modelRow.group.forEach((model: DynamicInputModel) => {
|
||||
if (model.name === this.model.mandatoryField) {
|
||||
res = model.value == null;
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
onBlur(event) {
|
||||
this.blur.emit();
|
||||
}
|
||||
|
||||
onChipSelected(event) {
|
||||
this.expandForm();
|
||||
this.selectedChipItem = this.chips.getChipByIndex(event);
|
||||
this.formModel.forEach((row) => {
|
||||
const modelRow = row as DynamicFormGroupModel;
|
||||
modelRow.group.forEach((model: DynamicInputModel) => {
|
||||
const value = (this.selectedChipItem.item[model.name] === PLACEHOLDER_PARENT_METADATA
|
||||
|| this.selectedChipItem.item[model.name].value === PLACEHOLDER_PARENT_METADATA)
|
||||
? null
|
||||
: this.selectedChipItem.item[model.name];
|
||||
// if (value instanceof FormFieldMetadataValueObject || value instanceof AuthorityValueModel) {
|
||||
// model.valueUpdates.next(value.display);
|
||||
// } else {
|
||||
// model.valueUpdates.next(value);
|
||||
// }
|
||||
model.valueUpdates.next(value);
|
||||
});
|
||||
});
|
||||
|
||||
this.editMode = true;
|
||||
}
|
||||
|
||||
onFocus(event) {
|
||||
this.focus.emit(event);
|
||||
}
|
||||
|
||||
collapseForm() {
|
||||
this.formCollapsed = Observable.of(true);
|
||||
this.clear();
|
||||
}
|
||||
|
||||
expandForm() {
|
||||
this.formCollapsed = Observable.of(false);
|
||||
}
|
||||
|
||||
clear() {
|
||||
if (this.editMode) {
|
||||
this.selectedChipItem.editMode = false;
|
||||
this.selectedChipItem = null;
|
||||
this.editMode = false;
|
||||
}
|
||||
this.resetForm();
|
||||
if (!this.model.isEmpty()) {
|
||||
this.formCollapsed = Observable.of(true);
|
||||
}
|
||||
}
|
||||
|
||||
save() {
|
||||
if (this.editMode) {
|
||||
this.modifyChip();
|
||||
} else {
|
||||
this.addToChips();
|
||||
}
|
||||
}
|
||||
|
||||
delete() {
|
||||
this.chips.remove(this.selectedChipItem);
|
||||
this.clear();
|
||||
}
|
||||
|
||||
private addToChips() {
|
||||
if (!this.formRef.formGroup.valid) {
|
||||
this.formService.validateAllFormFields(this.formRef.formGroup);
|
||||
return;
|
||||
}
|
||||
|
||||
// Item to add
|
||||
if (!this.isMandatoryFieldEmpty()) {
|
||||
const item = this.buildChipItem();
|
||||
this.chips.add(item);
|
||||
|
||||
this.resetForm();
|
||||
}
|
||||
}
|
||||
|
||||
private modifyChip() {
|
||||
if (!this.formRef.formGroup.valid) {
|
||||
this.formService.validateAllFormFields(this.formRef.formGroup);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isMandatoryFieldEmpty()) {
|
||||
const item = this.buildChipItem();
|
||||
this.chips.update(this.selectedChipItem.id, item);
|
||||
this.resetForm();
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
private buildChipItem() {
|
||||
const item = Object.create({});
|
||||
this.formModel.forEach((row) => {
|
||||
const modelRow = row as DynamicFormGroupModel;
|
||||
modelRow.group.forEach((control: DynamicInputModel) => {
|
||||
item[control.name] = control.value || PLACEHOLDER_PARENT_METADATA;
|
||||
});
|
||||
});
|
||||
return item;
|
||||
}
|
||||
|
||||
private resetForm() {
|
||||
if (this.formRef) {
|
||||
this.formService.resetForm(this.formRef.formGroup, this.formModel, this.formId);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs
|
||||
.filter((sub) => hasValue(sub))
|
||||
.forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,73 @@
|
||||
import { DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
|
||||
import { FormRowModel } from '../../../../../../core/shared/config/config-submission-forms.model';
|
||||
import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model';
|
||||
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
|
||||
import { isEmpty, isNull } from '../../../../../empty.util';
|
||||
|
||||
export const DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP = 'RELATION';
|
||||
export const PLACEHOLDER_PARENT_METADATA = '#PLACEHOLDER_PARENT_METADATA_VALUE#';
|
||||
|
||||
/**
|
||||
* Dynamic Group Model configuration interface
|
||||
*/
|
||||
export interface DynamicGroupModelConfig extends DsDynamicInputModelConfig {
|
||||
formConfiguration: FormRowModel[],
|
||||
mandatoryField: string,
|
||||
relationFields: string[],
|
||||
scopeUUID: string,
|
||||
submissionScope: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamic Group Model class
|
||||
*/
|
||||
export class DynamicGroupModel extends DsDynamicInputModel {
|
||||
@serializable() formConfiguration: FormRowModel[];
|
||||
@serializable() mandatoryField: string;
|
||||
@serializable() relationFields: string[];
|
||||
@serializable() scopeUUID: string;
|
||||
@serializable() submissionScope: string;
|
||||
@serializable() _value: any[];
|
||||
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP;
|
||||
|
||||
constructor(config: DynamicGroupModelConfig, layout?: DynamicFormControlLayout) {
|
||||
super(config, layout);
|
||||
|
||||
this.formConfiguration = config.formConfiguration;
|
||||
this.mandatoryField = config.mandatoryField;
|
||||
this.relationFields = config.relationFields;
|
||||
this.scopeUUID = config.scopeUUID;
|
||||
this.submissionScope = config.submissionScope;
|
||||
const value = config.value || [];
|
||||
this.valueUpdates.next(value);
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this._value
|
||||
}
|
||||
|
||||
set value(value) {
|
||||
this._value = (isEmpty(value)) ? null : value;
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
const value = this.getGroupValue();
|
||||
return (value.length === 1 && isNull(value[0][this.mandatoryField]));
|
||||
}
|
||||
|
||||
getGroupValue(): any[] {
|
||||
if (isEmpty(this._value)) {
|
||||
// If items is empty, last element has been removed
|
||||
// so emit an empty value that allows to dispatch
|
||||
// a remove JSON PATCH operation
|
||||
const emptyItem = Object.create({});
|
||||
emptyItem[this.mandatoryField] = null;
|
||||
this.relationFields
|
||||
.forEach((field) => {
|
||||
emptyItem[field] = null;
|
||||
});
|
||||
return [emptyItem];
|
||||
}
|
||||
return this._value
|
||||
}
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
|
||||
import {
|
||||
DynamicCheckboxGroupModel, DynamicFormControlLayout,
|
||||
DynamicFormGroupModelConfig,
|
||||
serializable
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
|
||||
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
|
||||
import { hasValue } from '../../../../../empty.util';
|
||||
|
||||
export interface DynamicListCheckboxGroupModelConfig extends DynamicFormGroupModelConfig {
|
||||
authorityOptions: AuthorityOptions;
|
||||
groupLength?: number;
|
||||
repeatable: boolean;
|
||||
value?: any;
|
||||
}
|
||||
|
||||
export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel {
|
||||
|
||||
@serializable() authorityOptions: AuthorityOptions;
|
||||
@serializable() repeatable: boolean;
|
||||
@serializable() groupLength: number;
|
||||
@serializable() _value: AuthorityValueModel[];
|
||||
isListGroup = true;
|
||||
valueUpdates: Subject<any>;
|
||||
|
||||
constructor(config: DynamicListCheckboxGroupModelConfig, layout?: DynamicFormControlLayout) {
|
||||
super(config, layout);
|
||||
|
||||
this.authorityOptions = config.authorityOptions;
|
||||
this.groupLength = config.groupLength || 5;
|
||||
this._value = [];
|
||||
this.repeatable = config.repeatable;
|
||||
|
||||
this.valueUpdates = new Subject<any>();
|
||||
this.valueUpdates.subscribe((value: AuthorityValueModel | AuthorityValueModel[]) => this.value = value);
|
||||
this.valueUpdates.next(config.value);
|
||||
}
|
||||
|
||||
get hasAuthority(): boolean {
|
||||
return this.authorityOptions && hasValue(this.authorityOptions.name);
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
set value(value: AuthorityValueModel | AuthorityValueModel[]) {
|
||||
if (value) {
|
||||
if (Array.isArray(value)) {
|
||||
this._value = value;
|
||||
} else {
|
||||
// _value is non extendible so assign it a new array
|
||||
const newValue = (this.value as AuthorityValueModel[]).concat([value]);
|
||||
this._value = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
DynamicFormControlLayout,
|
||||
DynamicRadioGroupModel,
|
||||
DynamicRadioGroupModelConfig,
|
||||
serializable
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
|
||||
import { hasValue } from '../../../../../empty.util';
|
||||
|
||||
export interface DynamicListModelConfig extends DynamicRadioGroupModelConfig<any> {
|
||||
authorityOptions: AuthorityOptions;
|
||||
groupLength?: number;
|
||||
repeatable: boolean;
|
||||
value?: any;
|
||||
}
|
||||
|
||||
export class DynamicListRadioGroupModel extends DynamicRadioGroupModel<any> {
|
||||
|
||||
@serializable() authorityOptions: AuthorityOptions;
|
||||
@serializable() repeatable: boolean;
|
||||
@serializable() groupLength: number;
|
||||
isListGroup = true;
|
||||
|
||||
constructor(config: DynamicListModelConfig, layout?: DynamicFormControlLayout) {
|
||||
super(config, layout);
|
||||
|
||||
this.authorityOptions = config.authorityOptions;
|
||||
this.groupLength = config.groupLength || 5;
|
||||
this.repeatable = config.repeatable;
|
||||
this.valueUpdates.next(config.value);
|
||||
}
|
||||
|
||||
get hasAuthority(): boolean {
|
||||
return this.authorityOptions && hasValue(this.authorityOptions.name);
|
||||
}
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
<div [formGroup]="group">
|
||||
<div *ngIf="model.repeatable"
|
||||
class="form-row"
|
||||
[attr.tabindex]="model.tabIndex"
|
||||
[dynamicId]="bindId && model.id"
|
||||
[formGroupName]="model.id"
|
||||
[ngClass]="model.layout.element?.control">
|
||||
|
||||
<div *ngFor="let columnItems of items" class="col-sm ml-3">
|
||||
|
||||
<div *ngFor="let item of columnItems" class="custom-control custom-checkbox">
|
||||
|
||||
<input type="checkbox" class="custom-control-input"
|
||||
[attr.tabindex]="item.index"
|
||||
[checked]="item.value"
|
||||
[id]="item.id"
|
||||
[dynamicId]="item.id"
|
||||
[formControlName]="item.id"
|
||||
[name]="model.name"
|
||||
[required]="model.required"
|
||||
[value]="item.value"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onChange($event)"
|
||||
(focus)="onFocus($event)"/>
|
||||
<label class="custom-control-label"
|
||||
[class.disabled]="model.disabled"
|
||||
[ngClass]="model.layout.element?.control"
|
||||
[for]="item.id">
|
||||
<span [ngClass]="model.layout.element?.label" [innerHTML]="item.label"></span>
|
||||
</label>
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div *ngIf="!model.repeatable"
|
||||
class="form-row"
|
||||
ngbRadioGroup
|
||||
[attr.tabindex]="model.tabIndex"
|
||||
[dynamicId]="bindId && model.id"
|
||||
[ngClass]="model.layout.element?.control"
|
||||
(change)="onChange($event)">
|
||||
|
||||
<div *ngFor="let columnItems of items" class="col-sm ml-3">
|
||||
|
||||
<div *ngFor="let item of columnItems" class="custom-control custom-radio">
|
||||
<label class="custom-control-label"
|
||||
[class.disabled]="model.disabled"
|
||||
[ngClass]="model.layout.element?.control">
|
||||
<input type="radio" class="custom-control-input"
|
||||
[checked]="item.value"
|
||||
[dynamicId]="item.id"
|
||||
[name]="model.id"
|
||||
[required]="model.required"
|
||||
[value]="item.index"
|
||||
(blur)="onBlur($event)"
|
||||
(focus)="onFocus($event)"/>
|
||||
<span [ngClass]="model.layout.element?.label" [innerHTML]="item.label"></span>
|
||||
</label>
|
||||
</div>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1 @@
|
||||
|
@@ -0,0 +1,299 @@
|
||||
// Load the implementations that should be tested
|
||||
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { async, ComponentFixture, inject, TestBed, } from '@angular/core/testing';
|
||||
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { DsDynamicListComponent } from './dynamic-list.component';
|
||||
import { DynamicListCheckboxGroupModel } from './dynamic-list-checkbox-group.model';
|
||||
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
|
||||
import { FormBuilderService } from '../../../form-builder.service';
|
||||
import { DynamicFormControlLayout, DynamicFormsCoreModule, DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
||||
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
|
||||
import { AuthorityService } from '../../../../../../core/integration/authority.service';
|
||||
import { AuthorityServiceStub } from '../../../../../testing/authority-service-stub';
|
||||
import { DynamicListRadioGroupModel } from './dynamic-list-radio-group.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
|
||||
import { createTestComponent } from '../../../../../testing/utils';
|
||||
|
||||
export const LAYOUT_TEST = {
|
||||
element: {
|
||||
group: ''
|
||||
}
|
||||
} as DynamicFormControlLayout;
|
||||
|
||||
export const LIST_TEST_GROUP = new FormGroup({
|
||||
listCheckbox: new FormGroup({}),
|
||||
listRadio: new FormGroup({})
|
||||
});
|
||||
|
||||
export const LIST_CHECKBOX_TEST_MODEL_CONFIG = {
|
||||
authorityOptions: {
|
||||
closed: false,
|
||||
metadata: 'listCheckbox',
|
||||
name: 'type_programme',
|
||||
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
|
||||
} as AuthorityOptions,
|
||||
disabled: false,
|
||||
id: 'listCheckbox',
|
||||
label: 'Programme',
|
||||
name: 'listCheckbox',
|
||||
placeholder: 'Programme',
|
||||
readOnly: false,
|
||||
required: false,
|
||||
repeatable: true
|
||||
};
|
||||
|
||||
export const LIST_RADIO_TEST_MODEL_CONFIG = {
|
||||
authorityOptions: {
|
||||
closed: false,
|
||||
metadata: 'listRadio',
|
||||
name: 'type_programme',
|
||||
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
|
||||
} as AuthorityOptions,
|
||||
disabled: false,
|
||||
id: 'listRadio',
|
||||
label: 'Programme',
|
||||
name: 'listRadio',
|
||||
placeholder: 'Programme',
|
||||
readOnly: false,
|
||||
required: false,
|
||||
repeatable: false
|
||||
};
|
||||
|
||||
describe('DsDynamicListComponent test suite', () => {
|
||||
|
||||
let testComp: TestComponent;
|
||||
let listComp: DsDynamicListComponent;
|
||||
let testFixture: ComponentFixture<TestComponent>;
|
||||
let listFixture: ComponentFixture<DsDynamicListComponent>;
|
||||
let html;
|
||||
let modelValue;
|
||||
|
||||
const authorityServiceStub = new AuthorityServiceStub();
|
||||
|
||||
// async beforeEach
|
||||
beforeEach(async(() => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
DynamicFormsCoreModule,
|
||||
DynamicFormsNGBootstrapUIModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
DsDynamicListComponent,
|
||||
TestComponent,
|
||||
], // declare the test component
|
||||
providers: [
|
||||
AuthorityService,
|
||||
ChangeDetectorRef,
|
||||
DsDynamicListComponent,
|
||||
DynamicFormValidationService,
|
||||
FormBuilderService,
|
||||
{provide: AuthorityService, useValue: authorityServiceStub},
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
describe('', () => {
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
html = `
|
||||
<ds-dynamic-list
|
||||
[bindId]="bindId"
|
||||
[group]="group"
|
||||
[model]="model"
|
||||
[showErrorMessages]="showErrorMessages"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onValueChange($event)"
|
||||
(focus)="onFocus($event)"></ds-dynamic-list>`;
|
||||
|
||||
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||
testComp = testFixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create DsDynamicListComponent', inject([DsDynamicListComponent], (app: DsDynamicListComponent) => {
|
||||
|
||||
expect(app).toBeDefined();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when model is a DynamicListCheckboxGroupModel', () => {
|
||||
describe('and init model value is empty', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
listFixture = TestBed.createComponent(DsDynamicListComponent);
|
||||
listComp = listFixture.componentInstance; // FormComponent test instance
|
||||
listComp.group = LIST_TEST_GROUP;
|
||||
listComp.model = new DynamicListCheckboxGroupModel(LIST_CHECKBOX_TEST_MODEL_CONFIG, LAYOUT_TEST);
|
||||
listFixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
listFixture.destroy();
|
||||
listComp = null;
|
||||
});
|
||||
|
||||
it('should init component properly', () => {
|
||||
const results$ = authorityServiceStub.getEntriesByName({} as any);
|
||||
|
||||
results$.subscribe((results) => {
|
||||
expect((listComp as any).optionsList).toEqual(results.payload);
|
||||
expect(listComp.items.length).toBe(1);
|
||||
expect(listComp.items[0].length).toBe(2);
|
||||
})
|
||||
});
|
||||
|
||||
it('should set model value properly when a checkbox option is selected', () => {
|
||||
const de = listFixture.debugElement.queryAll(By.css('div.custom-checkbox'));
|
||||
const items = de[0].queryAll(By.css('input.custom-control-input'));
|
||||
const item = items[0];
|
||||
modelValue = [Object.assign(new AuthorityValueModel(), {id: 1, display: 'one', value: 1})];
|
||||
|
||||
item.nativeElement.click();
|
||||
|
||||
expect(listComp.model.value).toEqual(modelValue)
|
||||
});
|
||||
|
||||
it('should emit blur Event onBlur', () => {
|
||||
spyOn(listComp.blur, 'emit');
|
||||
listComp.onBlur(new Event('blur'));
|
||||
expect(listComp.blur.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit focus Event onFocus', () => {
|
||||
spyOn(listComp.focus, 'emit');
|
||||
listComp.onFocus(new Event('focus'));
|
||||
expect(listComp.focus.emit).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('and init model value is not empty', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
listFixture = TestBed.createComponent(DsDynamicListComponent);
|
||||
listComp = listFixture.componentInstance; // FormComponent test instance
|
||||
listComp.group = LIST_TEST_GROUP;
|
||||
listComp.model = new DynamicListCheckboxGroupModel(LIST_CHECKBOX_TEST_MODEL_CONFIG, LAYOUT_TEST);
|
||||
modelValue = [Object.assign(new AuthorityValueModel(), {id: 1, display: 'one', value: 1})];
|
||||
listComp.model.value = modelValue;
|
||||
listFixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
listFixture.destroy();
|
||||
listComp = null;
|
||||
});
|
||||
|
||||
it('should init component properly', () => {
|
||||
const results$ = authorityServiceStub.getEntriesByName({} as any);
|
||||
|
||||
results$.subscribe((results) => {
|
||||
expect((listComp as any).optionsList).toEqual(results.payload);
|
||||
expect(listComp.model.value).toEqual(modelValue);
|
||||
expect((listComp.model as DynamicListCheckboxGroupModel).group[0].value).toBeTruthy();
|
||||
})
|
||||
});
|
||||
|
||||
it('should set model value properly when a checkbox option is deselected', () => {
|
||||
const de = listFixture.debugElement.queryAll(By.css('div.custom-checkbox'));
|
||||
const items = de[0].queryAll(By.css('input.custom-control-input'));
|
||||
const item = items[0];
|
||||
modelValue = [];
|
||||
|
||||
item.nativeElement.click();
|
||||
|
||||
expect(listComp.model.value).toEqual(modelValue)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when model is a DynamicListRadioGroupModel', () => {
|
||||
describe('and init model value is empty', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
listFixture = TestBed.createComponent(DsDynamicListComponent);
|
||||
listComp = listFixture.componentInstance; // FormComponent test instance
|
||||
listComp.group = LIST_TEST_GROUP;
|
||||
listComp.model = new DynamicListRadioGroupModel(LIST_RADIO_TEST_MODEL_CONFIG, LAYOUT_TEST);
|
||||
listFixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
listFixture.destroy();
|
||||
listComp = null;
|
||||
});
|
||||
|
||||
it('should init component properly', () => {
|
||||
const results$ = authorityServiceStub.getEntriesByName({} as any);
|
||||
|
||||
results$.subscribe((results) => {
|
||||
expect((listComp as any).optionsList).toEqual(results.payload);
|
||||
expect(listComp.items.length).toBe(1);
|
||||
expect(listComp.items[0].length).toBe(2);
|
||||
})
|
||||
});
|
||||
|
||||
it('should set model value when a radio option is selected', () => {
|
||||
const de = listFixture.debugElement.queryAll(By.css('div.custom-radio'));
|
||||
const items = de[0].queryAll(By.css('input.custom-control-input'));
|
||||
const item = items[0];
|
||||
modelValue = Object.assign(new AuthorityValueModel(), {id: 1, display: 'one', value: 1});
|
||||
|
||||
item.nativeElement.click();
|
||||
|
||||
expect(listComp.model.value).toEqual(modelValue)
|
||||
});
|
||||
});
|
||||
|
||||
describe('and init model value is not empty', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
listFixture = TestBed.createComponent(DsDynamicListComponent);
|
||||
listComp = listFixture.componentInstance; // FormComponent test instance
|
||||
listComp.group = LIST_TEST_GROUP;
|
||||
listComp.model = new DynamicListRadioGroupModel(LIST_RADIO_TEST_MODEL_CONFIG, LAYOUT_TEST);
|
||||
modelValue = Object.assign(new AuthorityValueModel(), {id: 1, display: 'one', value: 1});
|
||||
listComp.model.value = modelValue;
|
||||
listFixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
listFixture.destroy();
|
||||
listComp = null;
|
||||
});
|
||||
|
||||
it('should init component properly', () => {
|
||||
const results$ = authorityServiceStub.getEntriesByName({} as any);
|
||||
|
||||
results$.subscribe((results) => {
|
||||
expect((listComp as any).optionsList).toEqual(results.payload);
|
||||
expect(listComp.model.value).toEqual(modelValue);
|
||||
expect((listComp.model as DynamicListRadioGroupModel).options[0].value).toBeTruthy();
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// declare a test component
|
||||
@Component({
|
||||
selector: 'ds-test-cmp',
|
||||
template: ``
|
||||
})
|
||||
class TestComponent {
|
||||
|
||||
group: FormGroup = LIST_TEST_GROUP;
|
||||
|
||||
model = new DynamicListCheckboxGroupModel(LIST_CHECKBOX_TEST_MODEL_CONFIG, LAYOUT_TEST);
|
||||
|
||||
showErrorMessages = false;
|
||||
|
||||
}
|
@@ -0,0 +1,135 @@
|
||||
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { findKey } from 'lodash';
|
||||
|
||||
import { AuthorityService } from '../../../../../../core/integration/authority.service';
|
||||
import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model';
|
||||
import { hasValue, isNotEmpty } from '../../../../../empty.util';
|
||||
import { DynamicListCheckboxGroupModel } from './dynamic-list-checkbox-group.model';
|
||||
import { FormBuilderService } from '../../../form-builder.service';
|
||||
import { DynamicCheckboxModel } from '@ng-dynamic-forms/core';
|
||||
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
|
||||
import { DynamicListRadioGroupModel } from './dynamic-list-radio-group.model';
|
||||
import { IntegrationData } from '../../../../../../core/integration/integration-data';
|
||||
|
||||
export interface ListItem {
|
||||
id: string,
|
||||
label: string,
|
||||
value: boolean,
|
||||
index: number
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dynamic-list',
|
||||
styleUrls: ['./dynamic-list.component.scss'],
|
||||
templateUrl: './dynamic-list.component.html'
|
||||
})
|
||||
|
||||
export class DsDynamicListComponent implements OnInit {
|
||||
@Input() bindId = true;
|
||||
@Input() group: FormGroup;
|
||||
@Input() model: DynamicListCheckboxGroupModel | DynamicListRadioGroupModel;
|
||||
@Input() showErrorMessages = false;
|
||||
|
||||
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() change: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
public items: ListItem[][] = [];
|
||||
protected optionsList: AuthorityValueModel[];
|
||||
protected searchOptions: IntegrationSearchOptions;
|
||||
|
||||
constructor(private authorityService: AuthorityService,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private formBuilderService: FormBuilderService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.hasAuthorityOptions()) {
|
||||
// TODO Replace max elements 1000 with a paginated request when pagination bug is resolved
|
||||
this.searchOptions = new IntegrationSearchOptions(
|
||||
this.model.authorityOptions.scope,
|
||||
this.model.authorityOptions.name,
|
||||
this.model.authorityOptions.metadata,
|
||||
'',
|
||||
1000, // Max elements
|
||||
1);// Current Page
|
||||
this.setOptionsFromAuthority();
|
||||
}
|
||||
}
|
||||
|
||||
onBlur(event: Event) {
|
||||
this.blur.emit(event);
|
||||
}
|
||||
|
||||
onFocus(event: Event) {
|
||||
this.focus.emit(event);
|
||||
}
|
||||
|
||||
onChange(event: Event) {
|
||||
const target = event.target as any;
|
||||
if (this.model.repeatable) {
|
||||
// Target tabindex coincide with the array index of the value into the authority list
|
||||
const authorityValue: AuthorityValueModel = this.optionsList[target.tabIndex];
|
||||
if (target.checked) {
|
||||
this.model.valueUpdates.next(authorityValue);
|
||||
} else {
|
||||
const newValue = [];
|
||||
this.model.value
|
||||
.filter((item) => item.value !== authorityValue.value)
|
||||
.forEach((item) => newValue.push(item));
|
||||
this.model.valueUpdates.next(newValue);
|
||||
}
|
||||
} else {
|
||||
(this.model as DynamicListRadioGroupModel).valueUpdates.next(this.optionsList[target.value]);
|
||||
}
|
||||
this.change.emit(event);
|
||||
}
|
||||
|
||||
protected setOptionsFromAuthority() {
|
||||
if (this.model.authorityOptions.name && this.model.authorityOptions.name.length > 0) {
|
||||
const listGroup = this.group.controls[this.model.id] as FormGroup;
|
||||
this.authorityService.getEntriesByName(this.searchOptions).subscribe((authorities: IntegrationData) => {
|
||||
let groupCounter = 0;
|
||||
let itemsPerGroup = 0;
|
||||
let tempList: ListItem[] = [];
|
||||
this.optionsList = authorities.payload as AuthorityValueModel[];
|
||||
// Make a list of available options (checkbox/radio) and split in groups of 'model.groupLength'
|
||||
(authorities.payload as AuthorityValueModel[]).forEach((option, key) => {
|
||||
const value = option.id || option.value;
|
||||
const checked: boolean = isNotEmpty(findKey(
|
||||
this.model.value,
|
||||
{value: option.value}));
|
||||
|
||||
const item: ListItem = {
|
||||
id: value,
|
||||
label: option.display,
|
||||
value: checked,
|
||||
index: key
|
||||
};
|
||||
if (this.model.repeatable) {
|
||||
this.formBuilderService.addFormGroupControl(listGroup, (this.model as DynamicListCheckboxGroupModel), new DynamicCheckboxModel(item));
|
||||
} else {
|
||||
(this.model as DynamicListRadioGroupModel).options.push({label: item.label, value: option});
|
||||
}
|
||||
tempList.push(item);
|
||||
itemsPerGroup++;
|
||||
this.items[groupCounter] = tempList;
|
||||
if (itemsPerGroup === this.model.groupLength) {
|
||||
groupCounter++;
|
||||
itemsPerGroup = 0;
|
||||
tempList = [];
|
||||
}
|
||||
});
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
protected hasAuthorityOptions() {
|
||||
return (hasValue(this.model.authorityOptions.scope)
|
||||
&& hasValue(this.model.authorityOptions.name)
|
||||
&& hasValue(this.model.authorityOptions.metadata));
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
import { DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
|
||||
import { DynamicLookupModel, DynamicLookupModelConfig } from './dynamic-lookup.model';
|
||||
|
||||
export const DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME = 'LOOKUP_NAME';
|
||||
|
||||
export interface DynamicLookupNameModelConfig extends DynamicLookupModelConfig {
|
||||
separator?: string;
|
||||
firstPlaceholder?: string;
|
||||
secondPlaceholder?: string;
|
||||
}
|
||||
|
||||
export class DynamicLookupNameModel extends DynamicLookupModel {
|
||||
|
||||
@serializable() separator: string;
|
||||
@serializable() secondPlaceholder: string;
|
||||
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME;
|
||||
|
||||
constructor(config: DynamicLookupNameModelConfig, layout?: DynamicFormControlLayout) {
|
||||
|
||||
super(config, layout);
|
||||
|
||||
this.separator = config.separator || ',';
|
||||
this.placeholder = config.firstPlaceholder || 'form.last-name';
|
||||
this.secondPlaceholder = config.secondPlaceholder || 'form.first-name';
|
||||
}
|
||||
}
|
@@ -0,0 +1,127 @@
|
||||
<div ngbDropdown #sdRef="ngbDropdown"
|
||||
(click)="$event.stopPropagation();"
|
||||
(openChange)="openChange($event);">
|
||||
|
||||
<!--Simple lookup, only 1 field -->
|
||||
<div class="form-row" *ngIf="!isLookupName()">
|
||||
<div class="col-xs-12 col-sm-8 col-md-9 col-lg-10">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input class="form-control"
|
||||
[attr.autoComplete]="model.autoComplete"
|
||||
[class.is-invalid]="showErrorMessages"
|
||||
[dynamicId]="bindId && model.id"
|
||||
[name]="model.name"
|
||||
[type]="model.inputType"
|
||||
[(ngModel)]="firstInputValue"
|
||||
[disabled]="isInputDisabled()"
|
||||
[placeholder]="model.placeholder"
|
||||
[readonly]="model.readOnly"
|
||||
(change)="$event.preventDefault()"
|
||||
(blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();"
|
||||
(focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();"
|
||||
(click)="$event.stopPropagation(); $event.stopPropagation(); sdRef.close();"
|
||||
(input)="onInput($event)">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-4 col-md-2 col-lg-1 text-center">
|
||||
<button ngbDropdownAnchor
|
||||
*ngIf="!isInputDisabled()" class="btn btn-secondary"
|
||||
type="button"
|
||||
[disabled]="model.readOnly || isSearchDisabled()"
|
||||
(click)="sdRef.open(); search(); $event.stopPropagation();">{{'form.search' | translate}}
|
||||
</button>
|
||||
<button *ngIf="isInputDisabled()" class="btn btn-secondary"
|
||||
type="button"
|
||||
[disabled]="model.readOnly"
|
||||
(click)="remove($event)">{{'form.remove' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--Lookup-name, 2 fields-->
|
||||
<div class="form-row" *ngIf="isLookupName()">
|
||||
<div class="col-xs-12 col-md-8 col-lg-9">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-6">
|
||||
<input class="form-control"
|
||||
[attr.autoComplete]="model.autoComplete"
|
||||
[class.is-invalid]="showErrorMessages"
|
||||
[dynamicId]="bindId && model.id"
|
||||
[name]="model.name"
|
||||
[type]="model.inputType"
|
||||
[(ngModel)]="firstInputValue"
|
||||
[disabled]="isInputDisabled()"
|
||||
[placeholder]="model.placeholder | translate"
|
||||
[readonly]="model.readOnly"
|
||||
(change)="$event.preventDefault()"
|
||||
(blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();"
|
||||
(focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();"
|
||||
(click)="$event.stopPropagation(); $event.stopPropagation(); sdRef.close();"
|
||||
(input)="onInput($event)">
|
||||
</div>
|
||||
|
||||
<div *ngIf="isLookupName()" class="col-xs-12 col-md-6 pl-md-0" >
|
||||
<input class="form-control"
|
||||
[ngClass]="{}"
|
||||
[attr.autoComplete]="model.autoComplete"
|
||||
[class.is-invalid]="showErrorMessages"
|
||||
[dynamicId]="bindId && model.id"
|
||||
[name]="model.name + '_2'"
|
||||
[type]="model.inputType"
|
||||
[(ngModel)]="secondInputValue"
|
||||
[disabled]="firstInputValue.length === 0 || isInputDisabled()"
|
||||
[placeholder]="model.secondPlaceholder | translate"
|
||||
[readonly]="model.readOnly"
|
||||
(change)="$event.preventDefault()"
|
||||
(blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();"
|
||||
(focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();"
|
||||
(click)="$event.stopPropagation(); sdRef.close();"
|
||||
(input)="onInput($event)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-3 col-lg-2 text-center">
|
||||
<button ngbDropdownAnchor
|
||||
*ngIf="!isInputDisabled()" class="btn btn-secondary"
|
||||
type="button"
|
||||
[disabled]="isSearchDisabled()"
|
||||
(click)="sdRef.open(); search(); $event.stopPropagation();">{{'form.search' | translate}}
|
||||
</button>
|
||||
<button *ngIf="isInputDisabled()" class="btn btn-secondary"
|
||||
type="button"
|
||||
(click)="remove($event)">{{'form.remove' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ngbDropdownMenu
|
||||
class="mt-0 dropdown-menu scrollable-dropdown-menu w-100"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
aria-labelledby="scrollableDropdownMenuButton">
|
||||
<div class="scrollable-menu"
|
||||
aria-labelledby="scrollableDropdownMenuButton"
|
||||
infiniteScroll
|
||||
[infiniteScrollDistance]="2"
|
||||
[infiniteScrollThrottle]="50"
|
||||
(scrolled)="onScroll()"
|
||||
[scrollWindow]="false">
|
||||
|
||||
<button class="dropdown-item disabled"
|
||||
*ngIf="optionsList && optionsList.length == 0"
|
||||
(click)="$event.stopPropagation(); clearFields(); sdRef.close();">{{'form.no-results' | translate}}
|
||||
</button>
|
||||
<button class="dropdown-item collection-item"
|
||||
*ngFor="let listEntry of optionsList"
|
||||
(click)="$event.stopPropagation(); onSelect(listEntry); sdRef.close();"
|
||||
title="{{ listEntry.display }}">
|
||||
{{listEntry.value}}
|
||||
</button>
|
||||
<div class="scrollable-dropdown-loading text-center" *ngIf="loading"><p>{{'form.loading' | translate}}</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
@@ -0,0 +1,65 @@
|
||||
@import "../../../../../../../styles/variables";
|
||||
|
||||
.dropdown-toggle::after {
|
||||
display:none
|
||||
}
|
||||
|
||||
/* enable absolute positioning */
|
||||
.spinner-addon {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* style fa-spin */
|
||||
.spinner-addon .fa-spin {
|
||||
color: map-get($theme-colors, primary);
|
||||
position: absolute;
|
||||
margin-top: 3px;
|
||||
padding: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* align fa-spin */
|
||||
.left-addon .fa-spin {
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.right-addon .fa-spin {
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
/* add padding */
|
||||
.left-addon input {
|
||||
padding-left: $spacer * 2;
|
||||
}
|
||||
|
||||
.right-addon input {
|
||||
padding-right: $spacer * 2;
|
||||
}
|
||||
|
||||
:host /deep/ .dropdown-menu {
|
||||
width: 100% !important;
|
||||
max-height: $dropdown-menu-max-height;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
:host /deep/ .dropdown-item.active,
|
||||
:host /deep/ .dropdown-item:active,
|
||||
:host /deep/ .dropdown-item:focus,
|
||||
:host /deep/ .dropdown-item:hover {
|
||||
color: $dropdown-link-hover-color !important;
|
||||
background-color: $dropdown-link-hover-bg !important;
|
||||
}
|
||||
//
|
||||
//.dropdown-menu {
|
||||
// margin-top: -($spacer * 0.625);
|
||||
//}
|
||||
|
||||
div {
|
||||
overflow: visible;
|
||||
//padding-right: 0 !important;
|
||||
}
|
||||
//
|
||||
//button {
|
||||
// margin-right: $spacer * 0.625;
|
||||
//}
|
@@ -0,0 +1,343 @@
|
||||
// Load the implementations that should be tested
|
||||
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick, } from '@angular/core/testing';
|
||||
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
|
||||
import { DynamicFormsCoreModule, DynamicFormValidationService } from '@ng-dynamic-forms/core';
|
||||
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
|
||||
import { AuthorityService } from '../../../../../../core/integration/authority.service';
|
||||
import { AuthorityServiceStub } from '../../../../../testing/authority-service-stub';
|
||||
import { DsDynamicLookupComponent } from './dynamic-lookup.component';
|
||||
import { DynamicLookupModel } from './dynamic-lookup.model';
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { FormBuilderService } from '../../../form-builder.service';
|
||||
import { FormService } from '../../../../form.service';
|
||||
import { FormComponent } from '../../../../form.component';
|
||||
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
|
||||
import { DynamicLookupNameModel } from './dynamic-lookup-name.model';
|
||||
import { createTestComponent } from '../../../../../testing/utils';
|
||||
|
||||
export const LOOKUP_TEST_MODEL_CONFIG = {
|
||||
authorityOptions: {
|
||||
closed: false,
|
||||
metadata: 'lookup',
|
||||
name: 'RPAuthority',
|
||||
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
|
||||
} as AuthorityOptions,
|
||||
disabled: false,
|
||||
errorMessages: {required: 'Required field.'},
|
||||
id: 'lookup',
|
||||
label: 'Author',
|
||||
maxOptions: 10,
|
||||
name: 'lookup',
|
||||
placeholder: 'Author',
|
||||
readOnly: false,
|
||||
required: true,
|
||||
repeatable: true,
|
||||
separator: ',',
|
||||
validators: {required: null},
|
||||
value: undefined
|
||||
};
|
||||
|
||||
export const LOOKUP_NAME_TEST_MODEL_CONFIG = {
|
||||
authorityOptions: {
|
||||
closed: false,
|
||||
metadata: 'lookup-name',
|
||||
name: 'RPAuthority',
|
||||
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
|
||||
} as AuthorityOptions,
|
||||
disabled: false,
|
||||
errorMessages: {required: 'Required field.'},
|
||||
id: 'lookupName',
|
||||
label: 'Author',
|
||||
maxOptions: 10,
|
||||
name: 'lookupName',
|
||||
placeholder: 'Author',
|
||||
readOnly: false,
|
||||
required: true,
|
||||
repeatable: true,
|
||||
separator: ',',
|
||||
validators: {required: null},
|
||||
value: undefined
|
||||
};
|
||||
|
||||
export const LOOKUP_TEST_GROUP = new FormGroup({
|
||||
lookup: new FormControl(),
|
||||
lookupName: new FormControl()
|
||||
});
|
||||
|
||||
describe('Dynamic Lookup component', () => {
|
||||
|
||||
let testComp: TestComponent;
|
||||
let lookupComp: DsDynamicLookupComponent;
|
||||
let testFixture: ComponentFixture<TestComponent>;
|
||||
let lookupFixture: ComponentFixture<DsDynamicLookupComponent>;
|
||||
let html;
|
||||
|
||||
const authorityServiceStub = new AuthorityServiceStub();
|
||||
|
||||
// async beforeEach
|
||||
beforeEach(async(() => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
DynamicFormsCoreModule,
|
||||
DynamicFormsNGBootstrapUIModule,
|
||||
FormsModule,
|
||||
InfiniteScrollModule,
|
||||
ReactiveFormsModule,
|
||||
NgbModule.forRoot(),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
DsDynamicLookupComponent,
|
||||
TestComponent,
|
||||
], // declare the test component
|
||||
providers: [
|
||||
ChangeDetectorRef,
|
||||
DsDynamicLookupComponent,
|
||||
DynamicFormValidationService,
|
||||
FormBuilderService,
|
||||
FormComponent,
|
||||
FormService,
|
||||
{provide: AuthorityService, useValue: authorityServiceStub},
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
describe('', () => {
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
html = `
|
||||
<ds-dynamic-lookup
|
||||
[bindId]="bindId"
|
||||
[group]="group"
|
||||
[model]="model"
|
||||
[showErrorMessages]="showErrorMessages"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onValueChange($event)"
|
||||
(focus)="onFocus($event)"></ds-dynamic-lookup>`;
|
||||
|
||||
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||
testComp = testFixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create DsDynamicLookupComponent', inject([DsDynamicLookupComponent], (app: DsDynamicLookupComponent) => {
|
||||
expect(app).toBeDefined();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when model is DynamicLookupModel', () => {
|
||||
|
||||
describe('', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
lookupFixture = TestBed.createComponent(DsDynamicLookupComponent);
|
||||
lookupComp = lookupFixture.componentInstance; // FormComponent test instance
|
||||
lookupComp.group = LOOKUP_TEST_GROUP;
|
||||
lookupComp.model = new DynamicLookupModel(LOOKUP_TEST_MODEL_CONFIG);
|
||||
lookupFixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should render only an input element', () => {
|
||||
const de = lookupFixture.debugElement.queryAll(By.css('input.form-control'));
|
||||
expect(de.length).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('and init model value is empty', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
lookupFixture = TestBed.createComponent(DsDynamicLookupComponent);
|
||||
lookupComp = lookupFixture.componentInstance; // FormComponent test instance
|
||||
lookupComp.group = LOOKUP_TEST_GROUP;
|
||||
lookupComp.model = new DynamicLookupModel(LOOKUP_TEST_MODEL_CONFIG);
|
||||
lookupFixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should init component properly', () => {
|
||||
expect(lookupComp.firstInputValue).toBe('');
|
||||
});
|
||||
|
||||
it('should return search results', fakeAsync(() => {
|
||||
const de = lookupFixture.debugElement.queryAll(By.css('button'));
|
||||
const btnEl = de[0].nativeElement;
|
||||
const results$ = authorityServiceStub.getEntriesByName({} as any);
|
||||
|
||||
lookupComp.firstInputValue = 'test';
|
||||
lookupFixture.detectChanges();
|
||||
|
||||
btnEl.click();
|
||||
tick();
|
||||
lookupFixture.detectChanges();
|
||||
results$.subscribe((results) => {
|
||||
expect(lookupComp.optionsList).toEqual(results.payload);
|
||||
})
|
||||
|
||||
}));
|
||||
|
||||
it('should select a results entry properly', fakeAsync(() => {
|
||||
let de = lookupFixture.debugElement.queryAll(By.css('button'));
|
||||
const btnEl = de[0].nativeElement;
|
||||
const selectedValue = Object.assign(new AuthorityValueModel(), {id: 1, display: 'one', value: 1});
|
||||
spyOn(lookupComp.change, 'emit');
|
||||
|
||||
lookupComp.firstInputValue = 'test';
|
||||
lookupFixture.detectChanges();
|
||||
btnEl.click();
|
||||
tick();
|
||||
lookupFixture.detectChanges();
|
||||
de = lookupFixture.debugElement.queryAll(By.css('button.dropdown-item'));
|
||||
const entryEl = de[0].nativeElement;
|
||||
entryEl.click();
|
||||
|
||||
expect(lookupComp.firstInputValue).toEqual('one');
|
||||
expect(lookupComp.model.value).toEqual(selectedValue);
|
||||
expect(lookupComp.change.emit).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should set model.value on input type when AuthorityOptions.closed is false', fakeAsync(() => {
|
||||
lookupComp.firstInputValue = 'test';
|
||||
lookupFixture.detectChanges();
|
||||
|
||||
lookupComp.onInput(new Event('input'));
|
||||
expect(lookupComp.model.value).toEqual(new FormFieldMetadataValueObject('test'))
|
||||
|
||||
}));
|
||||
|
||||
it('should not set model.value on input type when AuthorityOptions.closed is true', () => {
|
||||
lookupComp.model.authorityOptions.closed = true;
|
||||
lookupComp.firstInputValue = 'test';
|
||||
lookupFixture.detectChanges();
|
||||
|
||||
lookupComp.onInput(new Event('input'));
|
||||
expect(lookupComp.model.value).not.toBeDefined();
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('and init model value is not empty', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
lookupFixture = TestBed.createComponent(DsDynamicLookupComponent);
|
||||
lookupComp = lookupFixture.componentInstance; // FormComponent test instance
|
||||
lookupComp.group = LOOKUP_TEST_GROUP;
|
||||
lookupComp.model = new DynamicLookupModel(LOOKUP_TEST_MODEL_CONFIG);
|
||||
lookupComp.model.value = new FormFieldMetadataValueObject('test', null, 'test001');
|
||||
lookupFixture.detectChanges();
|
||||
|
||||
// spyOn(store, 'dispatch');
|
||||
});
|
||||
|
||||
it('should init component properly', () => {
|
||||
expect(lookupComp.firstInputValue).toBe('test')
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when model is DynamicLookupNameModel', () => {
|
||||
|
||||
describe('', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
lookupFixture = TestBed.createComponent(DsDynamicLookupComponent);
|
||||
lookupComp = lookupFixture.componentInstance; // FormComponent test instance
|
||||
lookupComp.group = LOOKUP_TEST_GROUP;
|
||||
lookupComp.model = new DynamicLookupNameModel(LOOKUP_NAME_TEST_MODEL_CONFIG);
|
||||
lookupFixture.detectChanges();
|
||||
|
||||
// spyOn(store, 'dispatch');
|
||||
});
|
||||
|
||||
it('should render two input element', () => {
|
||||
const de = lookupFixture.debugElement.queryAll(By.css('input.form-control'));
|
||||
expect(de.length).toBe(2);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('and init model value is empty', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
lookupFixture = TestBed.createComponent(DsDynamicLookupComponent);
|
||||
lookupComp = lookupFixture.componentInstance; // FormComponent test instance
|
||||
lookupComp.group = LOOKUP_TEST_GROUP;
|
||||
lookupComp.model = new DynamicLookupNameModel(LOOKUP_NAME_TEST_MODEL_CONFIG);
|
||||
lookupFixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should select a results entry properly', fakeAsync(() => {
|
||||
const payload = [
|
||||
Object.assign(new AuthorityValueModel(), {id: 1, display: 'Name, Lastname', value: 1}),
|
||||
Object.assign(new AuthorityValueModel(), {id: 2, display: 'NameTwo, LastnameTwo', value: 2}),
|
||||
];
|
||||
let de = lookupFixture.debugElement.queryAll(By.css('button'));
|
||||
const btnEl = de[0].nativeElement;
|
||||
const selectedValue = Object.assign(new AuthorityValueModel(), {id: 1, display: 'Name, Lastname', value: 1});
|
||||
|
||||
spyOn(lookupComp.change, 'emit');
|
||||
authorityServiceStub.setNewPayload(payload);
|
||||
lookupComp.firstInputValue = 'test';
|
||||
lookupFixture.detectChanges();
|
||||
btnEl.click();
|
||||
tick();
|
||||
lookupFixture.detectChanges();
|
||||
de = lookupFixture.debugElement.queryAll(By.css('button.dropdown-item'));
|
||||
const entryEl = de[0].nativeElement;
|
||||
entryEl.click();
|
||||
|
||||
expect(lookupComp.firstInputValue).toEqual('Name');
|
||||
expect(lookupComp.secondInputValue).toEqual('Lastname');
|
||||
expect(lookupComp.model.value).toEqual(selectedValue);
|
||||
expect(lookupComp.change.emit).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe('and init model value is not empty', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
lookupFixture = TestBed.createComponent(DsDynamicLookupComponent);
|
||||
lookupComp = lookupFixture.componentInstance; // FormComponent test instance
|
||||
lookupComp.group = LOOKUP_TEST_GROUP;
|
||||
lookupComp.model = new DynamicLookupNameModel(LOOKUP_NAME_TEST_MODEL_CONFIG);
|
||||
lookupComp.model.value = new FormFieldMetadataValueObject('Name, Lastname', null, 'test001');
|
||||
lookupFixture.detectChanges();
|
||||
|
||||
});
|
||||
|
||||
it('should init component properly', () => {
|
||||
expect(lookupComp.firstInputValue).toBe('Name');
|
||||
expect(lookupComp.secondInputValue).toBe('Lastname');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
// declare a test component
|
||||
@Component({
|
||||
selector: 'ds-test-cmp',
|
||||
template: ``
|
||||
})
|
||||
class TestComponent {
|
||||
|
||||
group: FormGroup = LOOKUP_TEST_GROUP;
|
||||
|
||||
inputLookupModelConfig = LOOKUP_TEST_MODEL_CONFIG;
|
||||
|
||||
model = new DynamicLookupModel(this.inputLookupModelConfig);
|
||||
|
||||
showErrorMessages = false;
|
||||
|
||||
}
|
@@ -0,0 +1,217 @@
|
||||
import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
import { AuthorityService } from '../../../../../../core/integration/authority.service';
|
||||
import { DynamicLookupModel } from './dynamic-lookup.model';
|
||||
import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model';
|
||||
import { hasValue, isEmpty, isNotEmpty, isNull, isUndefined } from '../../../../../empty.util';
|
||||
import { IntegrationData } from '../../../../../../core/integration/integration-data';
|
||||
import { PageInfo } from '../../../../../../core/shared/page-info.model';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
|
||||
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
|
||||
import { DynamicLookupNameModel } from './dynamic-lookup-name.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dynamic-lookup',
|
||||
styleUrls: ['./dynamic-lookup.component.scss'],
|
||||
templateUrl: './dynamic-lookup.component.html'
|
||||
})
|
||||
export class DsDynamicLookupComponent implements OnDestroy, OnInit {
|
||||
@Input() bindId = true;
|
||||
@Input() group: FormGroup;
|
||||
@Input() model: DynamicLookupModel | DynamicLookupNameModel;
|
||||
@Input() showErrorMessages = false;
|
||||
|
||||
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() change: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
public firstInputValue = '';
|
||||
public secondInputValue = '';
|
||||
public loading = false;
|
||||
public pageInfo: PageInfo;
|
||||
public optionsList: any;
|
||||
|
||||
protected searchOptions: IntegrationSearchOptions;
|
||||
protected sub: Subscription;
|
||||
|
||||
constructor(private authorityService: AuthorityService,
|
||||
private cdr: ChangeDetectorRef) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.searchOptions = new IntegrationSearchOptions(
|
||||
this.model.authorityOptions.scope,
|
||||
this.model.authorityOptions.name,
|
||||
this.model.authorityOptions.metadata,
|
||||
'',
|
||||
this.model.maxOptions,
|
||||
1);
|
||||
|
||||
this.setInputsValue(this.model.value);
|
||||
|
||||
this.model.valueUpdates
|
||||
.subscribe((value) => {
|
||||
if (isEmpty(value)) {
|
||||
this.resetFields();
|
||||
} else {
|
||||
this.setInputsValue(this.model.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public formatItemForInput(item: any, field: number): string {
|
||||
if (isUndefined(item) || isNull(item)) {
|
||||
return '';
|
||||
}
|
||||
return (typeof item === 'string') ? item : this.inputFormatter(item, field);
|
||||
}
|
||||
|
||||
// inputFormatter = (x: { display: string }) => x.display;
|
||||
inputFormatter = (x: { display: string }, y: number) => {
|
||||
// this.splitValues();
|
||||
return y === 1 ? this.firstInputValue : this.secondInputValue;
|
||||
};
|
||||
|
||||
onInput(event) {
|
||||
if (!this.model.authorityOptions.closed) {
|
||||
if (isNotEmpty(this.getCurrentValue())) {
|
||||
const currentValue = new FormFieldMetadataValueObject(this.getCurrentValue());
|
||||
this.onSelect(currentValue);
|
||||
} else {
|
||||
this.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onScroll() {
|
||||
if (!this.loading && this.pageInfo.currentPage <= this.pageInfo.totalPages) {
|
||||
this.searchOptions.currentPage++;
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
|
||||
protected setInputsValue(value) {
|
||||
if (hasValue(value)) {
|
||||
let displayValue = value;
|
||||
if (value instanceof FormFieldMetadataValueObject || value instanceof AuthorityValueModel) {
|
||||
displayValue = value.display;
|
||||
}
|
||||
|
||||
if (hasValue(displayValue)) {
|
||||
if (this.isLookupName()) {
|
||||
const values = displayValue.split((this.model as DynamicLookupNameModel).separator);
|
||||
|
||||
this.firstInputValue = (values[0] || '').trim();
|
||||
this.secondInputValue = (values[1] || '').trim();
|
||||
} else {
|
||||
this.firstInputValue = displayValue || '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected getCurrentValue(): string {
|
||||
let result = '';
|
||||
if (!this.isLookupName()) {
|
||||
result = this.firstInputValue;
|
||||
} else {
|
||||
if (isNotEmpty(this.firstInputValue)) {
|
||||
result = this.firstInputValue;
|
||||
}
|
||||
if (isNotEmpty(this.secondInputValue)) {
|
||||
result = isEmpty(result)
|
||||
? this.secondInputValue
|
||||
: this.firstInputValue + (this.model as DynamicLookupNameModel).separator + ' ' + this.secondInputValue;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
search() {
|
||||
this.optionsList = null;
|
||||
this.pageInfo = null;
|
||||
|
||||
// Query
|
||||
this.searchOptions.query = this.getCurrentValue();
|
||||
|
||||
this.loading = true;
|
||||
this.authorityService.getEntriesByName(this.searchOptions)
|
||||
.distinctUntilChanged()
|
||||
.subscribe((object: IntegrationData) => {
|
||||
this.optionsList = object.payload;
|
||||
this.pageInfo = object.pageInfo;
|
||||
this.loading = false;
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
clearFields() {
|
||||
// Clear inputs whether there is no results and authority is closed
|
||||
if (this.model.authorityOptions.closed) {
|
||||
this.resetFields();
|
||||
}
|
||||
}
|
||||
|
||||
protected resetFields() {
|
||||
this.firstInputValue = '';
|
||||
if (this.isLookupName()) {
|
||||
this.secondInputValue = '';
|
||||
}
|
||||
}
|
||||
|
||||
onSelect(event) {
|
||||
this.group.markAsDirty();
|
||||
this.model.valueUpdates.next(event);
|
||||
this.setInputsValue(event);
|
||||
this.change.emit(event);
|
||||
this.optionsList = null;
|
||||
this.pageInfo = null;
|
||||
}
|
||||
|
||||
isInputDisabled() {
|
||||
return this.model.authorityOptions.closed && hasValue(this.model.value);
|
||||
}
|
||||
|
||||
isLookupName() {
|
||||
return (this.model instanceof DynamicLookupNameModel);
|
||||
}
|
||||
|
||||
isSearchDisabled() {
|
||||
// if (this.firstInputValue === ''
|
||||
// && (this.isLookupName ? this.secondInputValue === '' : true)) {
|
||||
// return true;
|
||||
// }
|
||||
// return false;
|
||||
return isEmpty(this.firstInputValue);
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.group.markAsPristine();
|
||||
this.model.valueUpdates.next(null);
|
||||
this.change.emit(null);
|
||||
}
|
||||
|
||||
openChange(isOpened: boolean) {
|
||||
if (!isOpened) {
|
||||
if (this.model.authorityOptions.closed) {
|
||||
this.setInputsValue('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onBlurEvent(event: Event) {
|
||||
this.blur.emit(event);
|
||||
}
|
||||
|
||||
onFocusEvent(event) {
|
||||
this.focus.emit(event);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (hasValue(this.sub)) {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
import { AUTOCOMPLETE_OFF, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
|
||||
import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model';
|
||||
|
||||
export const DYNAMIC_FORM_CONTROL_TYPE_LOOKUP = 'LOOKUP';
|
||||
|
||||
export interface DynamicLookupModelConfig extends DsDynamicInputModelConfig {
|
||||
maxOptions?: number;
|
||||
value?: any;
|
||||
}
|
||||
|
||||
export class DynamicLookupModel extends DsDynamicInputModel {
|
||||
|
||||
@serializable() maxOptions: number;
|
||||
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_LOOKUP;
|
||||
@serializable() value: any;
|
||||
|
||||
constructor(config: DynamicLookupModelConfig, layout?: DynamicFormControlLayout) {
|
||||
|
||||
super(config, layout);
|
||||
|
||||
this.autoComplete = AUTOCOMPLETE_OFF;
|
||||
this.maxOptions = config.maxOptions || 10;
|
||||
|
||||
this.valueUpdates.next(config.value);
|
||||
}
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
<div #sdRef="ngbDropdown" ngbDropdown class="input-group w-100">
|
||||
<input class="form-control"
|
||||
[attr.autoComplete]="model.autoComplete"
|
||||
[class.is-invalid]="showErrorMessages"
|
||||
[dynamicId]="bindId && model.id"
|
||||
[name]="model.name"
|
||||
[readonly]="model.readOnly"
|
||||
[type]="model.inputType"
|
||||
[value]="formatItemForInput(model.value)"
|
||||
(blur)="onBlur($event)"
|
||||
(click)="$event.stopPropagation(); openDropdown(sdRef);"
|
||||
(focus)="onFocus($event)"
|
||||
(keypress)="$event.preventDefault()">
|
||||
<button aria-describedby="collectionControlsMenuLabel"
|
||||
class="ds-form-input-btn btn btn-outline-primary"
|
||||
id="scrollableDropdownMenuButton_{{model.id}}"
|
||||
ngbDropdownToggle
|
||||
[disabled]="model.readOnly"></button>
|
||||
|
||||
<div ngbDropdownMenu
|
||||
class="dropdown-menu scrollable-dropdown-menu w-100"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
aria-labelledby="scrollableDropdownMenuButton">
|
||||
<div class="scrollable-menu"
|
||||
aria-labelledby="scrollableDropdownMenuButton"
|
||||
infiniteScroll
|
||||
[infiniteScrollDistance]="2"
|
||||
[infiniteScrollThrottle]="50"
|
||||
(scrolled)="onScroll()"
|
||||
[scrollWindow]="false">
|
||||
|
||||
<button class="dropdown-item disabled" *ngIf="optionsList && optionsList.length == 0">{{'form.no-results' | translate}}</button>
|
||||
<button class="dropdown-item collection-item" *ngFor="let listEntry of optionsList" (click)="onSelect(listEntry)" title="{{ listEntry.display }}">
|
||||
{{inputFormatter(listEntry)}}
|
||||
</button>
|
||||
<div class="scrollable-dropdown-loading text-center" *ngIf="loading"><p>{{'form.loading' | translate}}</p></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@@ -0,0 +1,26 @@
|
||||
@import '../../../../form.component';
|
||||
|
||||
.scrollable-menu {
|
||||
height: auto;
|
||||
max-height: $dropdown-menu-max-height;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.collection-item {
|
||||
border-bottom: $dropdown-border-width solid $dropdown-border-color;
|
||||
}
|
||||
|
||||
.scrollable-dropdown-loading {
|
||||
background-color: map-get($theme-colors, primary);
|
||||
color: white;
|
||||
height: $spacer * 2 !important;
|
||||
line-height: $spacer * 2;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.scrollable-dropdown-menu {
|
||||
left: 0 !important;
|
||||
margin-bottom: $spacer;
|
||||
z-index: 1000;
|
||||
}
|
@@ -0,0 +1,235 @@
|
||||
// Load the implementations that should be tested
|
||||
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick, } from '@angular/core/testing';
|
||||
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
|
||||
import { DynamicFormsCoreModule } from '@ng-dynamic-forms/core';
|
||||
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
|
||||
import { AuthorityService } from '../../../../../../core/integration/authority.service';
|
||||
import { AuthorityServiceStub } from '../../../../../testing/authority-service-stub';
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { DsDynamicScrollableDropdownComponent } from './dynamic-scrollable-dropdown.component';
|
||||
import { DynamicScrollableDropdownModel } from './dynamic-scrollable-dropdown.model';
|
||||
import { DsDynamicTypeaheadComponent } from '../typeahead/dynamic-typeahead.component';
|
||||
import { DynamicTypeaheadModel } from '../typeahead/dynamic-typeahead.model';
|
||||
import { TYPEAHEAD_TEST_GROUP, TYPEAHEAD_TEST_MODEL_CONFIG } from '../typeahead/dynamic-typeahead.component.spec';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
|
||||
import { hasClass, createTestComponent } from '../../../../../testing/utils';
|
||||
|
||||
export const SD_TEST_GROUP = new FormGroup({
|
||||
dropdown: new FormControl(),
|
||||
});
|
||||
|
||||
export const SD_TEST_MODEL_CONFIG = {
|
||||
authorityOptions: {
|
||||
closed: false,
|
||||
metadata: 'dropdown',
|
||||
name: 'common_iso_languages',
|
||||
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
|
||||
} as AuthorityOptions,
|
||||
disabled: false,
|
||||
errorMessages: {required: 'Required field.'},
|
||||
id: 'dropdown',
|
||||
label: 'Language',
|
||||
maxOptions: 10,
|
||||
name: 'dropdown',
|
||||
placeholder: 'Language',
|
||||
readOnly: false,
|
||||
required: false,
|
||||
repeatable: false,
|
||||
value: undefined
|
||||
};
|
||||
|
||||
describe('Dynamic Dynamic Scrollable Dropdown component', () => {
|
||||
|
||||
let testComp: TestComponent;
|
||||
let scrollableDropdownComp: DsDynamicScrollableDropdownComponent;
|
||||
let testFixture: ComponentFixture<TestComponent>;
|
||||
let scrollableDropdownFixture: ComponentFixture<DsDynamicScrollableDropdownComponent>;
|
||||
let html;
|
||||
let modelValue;
|
||||
|
||||
const authorityServiceStub = new AuthorityServiceStub();
|
||||
|
||||
// async beforeEach
|
||||
beforeEach(async(() => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
DynamicFormsCoreModule,
|
||||
DynamicFormsNGBootstrapUIModule,
|
||||
FormsModule,
|
||||
InfiniteScrollModule,
|
||||
ReactiveFormsModule,
|
||||
NgbModule.forRoot(),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [
|
||||
DsDynamicScrollableDropdownComponent,
|
||||
TestComponent,
|
||||
], // declare the test component
|
||||
providers: [
|
||||
ChangeDetectorRef,
|
||||
DsDynamicScrollableDropdownComponent,
|
||||
{provide: AuthorityService, useValue: authorityServiceStub},
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
describe('', () => {
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
html = `
|
||||
<ds-dynamic-scrollable-dropdown [bindId]="bindId"
|
||||
[group]="group"
|
||||
[model]="model"
|
||||
[showErrorMessages]="showErrorMessages"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onValueChange($event)"
|
||||
(focus)="onFocus($event)"></ds-dynamic-scrollable-dropdown>`;
|
||||
|
||||
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||
testComp = testFixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create DsDynamicScrollableDropdownComponent', inject([DsDynamicScrollableDropdownComponent], (app: DsDynamicScrollableDropdownComponent) => {
|
||||
|
||||
expect(app).toBeDefined();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('', () => {
|
||||
describe('when init model value is empty', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
scrollableDropdownFixture = TestBed.createComponent(DsDynamicScrollableDropdownComponent);
|
||||
scrollableDropdownComp = scrollableDropdownFixture.componentInstance; // FormComponent test instance
|
||||
scrollableDropdownComp.group = SD_TEST_GROUP;
|
||||
scrollableDropdownComp.model = new DynamicScrollableDropdownModel(SD_TEST_MODEL_CONFIG);
|
||||
scrollableDropdownFixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
scrollableDropdownFixture.destroy();
|
||||
scrollableDropdownComp = null;
|
||||
});
|
||||
|
||||
it('should init component properly', () => {
|
||||
const results$ = authorityServiceStub.getEntriesByName({} as any);
|
||||
expect(scrollableDropdownComp.optionsList).toBeDefined();
|
||||
results$.subscribe((results) => {
|
||||
expect(scrollableDropdownComp.optionsList).toEqual(results.payload);
|
||||
})
|
||||
});
|
||||
|
||||
it('should display dropdown menu entries', () => {
|
||||
const de = scrollableDropdownFixture.debugElement.query(By.css('button.ds-form-input-btn'));
|
||||
const btnEl = de.nativeElement;
|
||||
|
||||
const deMenu = scrollableDropdownFixture.debugElement.query(By.css('div.scrollable-dropdown-menu'));
|
||||
const menuEl = deMenu.nativeElement;
|
||||
|
||||
btnEl.click();
|
||||
scrollableDropdownFixture.detectChanges();
|
||||
|
||||
expect(hasClass(menuEl, 'show')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should fetch the next set of results when the user scroll to the end of the list', fakeAsync(() => {
|
||||
scrollableDropdownComp.pageInfo.currentPage = 1;
|
||||
scrollableDropdownComp.pageInfo.totalPages = 2;
|
||||
|
||||
scrollableDropdownFixture.detectChanges();
|
||||
|
||||
scrollableDropdownComp.onScroll();
|
||||
tick();
|
||||
|
||||
expect(scrollableDropdownComp.optionsList.length).toBe(4);
|
||||
}));
|
||||
|
||||
it('should select a results entry properly', fakeAsync(() => {
|
||||
const selectedValue = Object.assign(new AuthorityValueModel(), {id: 1, display: 'one', value: 1});
|
||||
|
||||
let de: any = scrollableDropdownFixture.debugElement.query(By.css('button.ds-form-input-btn'));
|
||||
let btnEl = de.nativeElement;
|
||||
|
||||
de = scrollableDropdownFixture.debugElement.query(By.css('div.scrollable-dropdown-menu'));
|
||||
const menuEl = de.nativeElement;
|
||||
|
||||
btnEl.click();
|
||||
scrollableDropdownFixture.detectChanges();
|
||||
|
||||
de = scrollableDropdownFixture.debugElement.queryAll(By.css('button.dropdown-item'));
|
||||
btnEl = de[0].nativeElement;
|
||||
|
||||
btnEl.click();
|
||||
|
||||
scrollableDropdownFixture.detectChanges();
|
||||
|
||||
expect((scrollableDropdownComp.model as any).value).toEqual(selectedValue);
|
||||
}));
|
||||
|
||||
it('should emit blur Event onBlur', () => {
|
||||
spyOn(scrollableDropdownComp.blur, 'emit');
|
||||
scrollableDropdownComp.onBlur(new Event('blur'));
|
||||
expect(scrollableDropdownComp.blur.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit focus Event onFocus', () => {
|
||||
spyOn(scrollableDropdownComp.focus, 'emit');
|
||||
scrollableDropdownComp.onFocus(new Event('focus'));
|
||||
expect(scrollableDropdownComp.focus.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when init model value is not empty', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
scrollableDropdownFixture = TestBed.createComponent(DsDynamicScrollableDropdownComponent);
|
||||
scrollableDropdownComp = scrollableDropdownFixture.componentInstance; // FormComponent test instance
|
||||
scrollableDropdownComp.group = SD_TEST_GROUP;
|
||||
modelValue = Object.assign(new AuthorityValueModel(), {id: 1, display: 'one', value: 1});
|
||||
scrollableDropdownComp.model = new DynamicScrollableDropdownModel(SD_TEST_MODEL_CONFIG);
|
||||
scrollableDropdownComp.model.value = modelValue;
|
||||
scrollableDropdownFixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
scrollableDropdownFixture.destroy();
|
||||
scrollableDropdownComp = null;
|
||||
});
|
||||
|
||||
it('should init component properly', () => {
|
||||
const results$ = authorityServiceStub.getEntriesByName({} as any);
|
||||
expect(scrollableDropdownComp.optionsList).toBeDefined();
|
||||
results$.subscribe((results) => {
|
||||
expect(scrollableDropdownComp.optionsList).toEqual(results.payload);
|
||||
expect(scrollableDropdownComp.model.value).toEqual(modelValue);
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// declare a test component
|
||||
@Component({
|
||||
selector: 'ds-test-cmp',
|
||||
template: ``
|
||||
})
|
||||
class TestComponent {
|
||||
|
||||
group: FormGroup = SD_TEST_GROUP;
|
||||
|
||||
model = new DynamicScrollableDropdownModel(SD_TEST_MODEL_CONFIG);
|
||||
|
||||
showErrorMessages = false;
|
||||
|
||||
}
|
@@ -0,0 +1,92 @@
|
||||
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
import { DynamicScrollableDropdownModel } from './dynamic-scrollable-dropdown.model';
|
||||
import { PageInfo } from '../../../../../../core/shared/page-info.model';
|
||||
import { isNull, isUndefined } from '../../../../../empty.util';
|
||||
import { AuthorityService } from '../../../../../../core/integration/authority.service';
|
||||
import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model';
|
||||
import { IntegrationData } from '../../../../../../core/integration/integration-data';
|
||||
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
|
||||
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dynamic-scrollable-dropdown',
|
||||
styleUrls: ['./dynamic-scrollable-dropdown.component.scss'],
|
||||
templateUrl: './dynamic-scrollable-dropdown.component.html'
|
||||
})
|
||||
export class DsDynamicScrollableDropdownComponent implements OnInit {
|
||||
@Input() bindId = true;
|
||||
@Input() group: FormGroup;
|
||||
@Input() model: DynamicScrollableDropdownModel;
|
||||
@Input() showErrorMessages = false;
|
||||
|
||||
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() change: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
public loading = false;
|
||||
public pageInfo: PageInfo;
|
||||
public optionsList: any;
|
||||
|
||||
protected searchOptions: IntegrationSearchOptions;
|
||||
|
||||
constructor(private authorityService: AuthorityService, private cdr: ChangeDetectorRef) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.searchOptions = new IntegrationSearchOptions(
|
||||
this.model.authorityOptions.scope,
|
||||
this.model.authorityOptions.name,
|
||||
this.model.authorityOptions.metadata,
|
||||
'',
|
||||
this.model.maxOptions,
|
||||
1);
|
||||
this.authorityService.getEntriesByName(this.searchOptions)
|
||||
.subscribe((object: IntegrationData) => {
|
||||
this.optionsList = object.payload;
|
||||
this.pageInfo = object.pageInfo;
|
||||
this.cdr.detectChanges();
|
||||
})
|
||||
}
|
||||
|
||||
public formatItemForInput(item: any): string {
|
||||
if (isUndefined(item) || isNull(item)) { return '' }
|
||||
return (typeof item === 'string') ? item : this.inputFormatter(item);
|
||||
}
|
||||
|
||||
inputFormatter = (x: AuthorityValueModel) => x.display || x.value;
|
||||
|
||||
openDropdown(sdRef: NgbDropdown) {
|
||||
if (!this.model.readOnly) {
|
||||
sdRef.open();
|
||||
}
|
||||
}
|
||||
|
||||
onScroll() {
|
||||
if (!this.loading && this.pageInfo.currentPage <= this.pageInfo.totalPages) {
|
||||
this.loading = true;
|
||||
this.searchOptions.currentPage++;
|
||||
this.authorityService.getEntriesByName(this.searchOptions)
|
||||
.do(() => this.loading = false)
|
||||
.subscribe((object: IntegrationData) => {
|
||||
this.optionsList = this.optionsList.concat(object.payload);
|
||||
this.pageInfo = object.pageInfo;
|
||||
this.cdr.detectChanges();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onBlur(event: Event) {
|
||||
this.blur.emit(event);
|
||||
}
|
||||
|
||||
onFocus(event) {
|
||||
this.focus.emit(event);
|
||||
}
|
||||
|
||||
onSelect(event) {
|
||||
this.group.markAsDirty();
|
||||
this.model.valueUpdates.next(event);
|
||||
this.change.emit(event);
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
import { AUTOCOMPLETE_OFF, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
|
||||
import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model';
|
||||
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
|
||||
|
||||
export const DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN = 'SCROLLABLE_DROPDOWN';
|
||||
|
||||
export interface DynamicScrollableDropdownModelConfig extends DsDynamicInputModelConfig {
|
||||
authorityOptions: AuthorityOptions;
|
||||
maxOptions?: number;
|
||||
value?: any;
|
||||
}
|
||||
|
||||
export class DynamicScrollableDropdownModel extends DsDynamicInputModel {
|
||||
|
||||
@serializable() maxOptions: number;
|
||||
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN;
|
||||
|
||||
constructor(config: DynamicScrollableDropdownModelConfig, layout?: DynamicFormControlLayout) {
|
||||
|
||||
super(config, layout);
|
||||
|
||||
this.autoComplete = AUTOCOMPLETE_OFF;
|
||||
this.authorityOptions = config.authorityOptions;
|
||||
this.maxOptions = config.maxOptions || 10;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
<ng-template #rt let-r="result" let-t="term">
|
||||
{{ r.display }}
|
||||
</ng-template>
|
||||
|
||||
|
||||
<ds-chips [chips]="chips" [wrapperClass]="'border-bottom border-light'">
|
||||
|
||||
<input *ngIf="!searchOptions"
|
||||
class="border-0 form-control-plaintext tag-input flex-grow-1 mt-1 mb-1 chips-sort-ignore"
|
||||
type="text"
|
||||
[class.pl-3]="chips.hasItems()"
|
||||
[placeholder]="model.placeholder"
|
||||
[readonly]="model.readOnly"
|
||||
[(ngModel)]="currentValue"
|
||||
(blur)="onBlur($event)"
|
||||
(keypress)="preventEventsPropagation($event)"
|
||||
(keydown)="preventEventsPropagation($event)"
|
||||
(keyup)="onKeyUp($event)" />
|
||||
|
||||
|
||||
<input *ngIf="searchOptions"
|
||||
class="border-0 form-control-plaintext tag-input flex-grow-1 mt-1 mb-1 chips-sort-ignore"
|
||||
type="text"
|
||||
[(ngModel)]="currentValue"
|
||||
[attr.autoComplete]="model.autoComplete"
|
||||
[class.is-invalid]="showErrorMessages"
|
||||
[class.pl-3]="chips.hasItems()"
|
||||
[dynamicId]="bindId && model.id"
|
||||
[inputFormatter]="formatter"
|
||||
[name]="model.name"
|
||||
[ngbTypeahead]="search"
|
||||
[placeholder]="model.placeholder"
|
||||
[readonly]="model.readOnly"
|
||||
[resultTemplate]="rt"
|
||||
[type]="model.inputType"
|
||||
(blur)="onBlur($event)"
|
||||
(focus)="onFocus($event)"
|
||||
(change)="$event.stopPropagation()"
|
||||
(input)="onInput($event)"
|
||||
(selectItem)="onSelectItem($event)"
|
||||
(keypress)="preventEventsPropagation($event)"
|
||||
(keydown)="preventEventsPropagation($event)"
|
||||
(keyup)="onKeyUp($event)"/>
|
||||
<i *ngIf="searching" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw text-primary position-absolute mt-1 p-0" aria-hidden="true"></i>
|
||||
</ds-chips>
|
||||
|
@@ -0,0 +1,33 @@
|
||||
@import "../../../../../../../styles/variables";
|
||||
|
||||
/* style fa-spin */
|
||||
.fa-spin {
|
||||
pointer-events: none;
|
||||
right: 0;
|
||||
}
|
||||
.chips-left {
|
||||
left: 0;
|
||||
padding-right: 100%;
|
||||
}
|
||||
|
||||
:host /deep/ .dropdown-menu {
|
||||
width: 100% !important;
|
||||
max-height: $dropdown-menu-max-height;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
left: 0 !important;
|
||||
margin-top: $spacer !important;
|
||||
}
|
||||
|
||||
:host /deep/ .dropdown-item.active,
|
||||
:host /deep/ .dropdown-item:active,
|
||||
:host /deep/ .dropdown-item:focus,
|
||||
:host /deep/ .dropdown-item:hover {
|
||||
color: $dropdown-link-hover-color !important;
|
||||
background-color: $dropdown-link-hover-bg !important;
|
||||
}
|
||||
|
||||
.tag-input {
|
||||
outline: none;
|
||||
width: auto !important;
|
||||
}
|
@@ -0,0 +1,293 @@
|
||||
// Load the implementations that should be tested
|
||||
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { async, ComponentFixture, fakeAsync, flush, inject, TestBed, } from '@angular/core/testing';
|
||||
|
||||
import { DynamicFormsCoreModule } from '@ng-dynamic-forms/core';
|
||||
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
|
||||
import { NgbModule, NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/observable/of'
|
||||
|
||||
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
|
||||
import { AuthorityService } from '../../../../../../core/integration/authority.service';
|
||||
import { AuthorityServiceStub } from '../../../../../testing/authority-service-stub';
|
||||
import { DsDynamicTagComponent } from './dynamic-tag.component';
|
||||
import { DynamicTagModel } from './dynamic-tag.model';
|
||||
import { GlobalConfig } from '../../../../../../../config/global-config.interface';
|
||||
import { GLOBAL_CONFIG } from '../../../../../../../config';
|
||||
import { Chips } from '../../../../../chips/models/chips.model';
|
||||
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
|
||||
import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model';
|
||||
import { createTestComponent } from '../../../../../testing/utils';
|
||||
|
||||
function createKeyUpEvent(key: number) {
|
||||
/* tslint:disable:no-empty */
|
||||
const event = {
|
||||
keyCode: key, preventDefault: () => {
|
||||
}, stopPropagation: () => {
|
||||
}
|
||||
};
|
||||
/* tslint:enable:no-empty */
|
||||
spyOn(event, 'preventDefault');
|
||||
spyOn(event, 'stopPropagation');
|
||||
return event;
|
||||
}
|
||||
|
||||
export const TAG_TEST_GROUP = new FormGroup({
|
||||
tag: new FormControl(),
|
||||
});
|
||||
|
||||
export const TAG_TEST_MODEL_CONFIG = {
|
||||
authorityOptions: {
|
||||
closed: false,
|
||||
metadata: 'tag',
|
||||
name: 'common_iso_languages',
|
||||
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
|
||||
} as AuthorityOptions,
|
||||
disabled: false,
|
||||
id: 'tag',
|
||||
label: 'Keywords',
|
||||
minChars: 3,
|
||||
name: 'tag',
|
||||
placeholder: 'Keywords',
|
||||
readOnly: false,
|
||||
required: false,
|
||||
repeatable: false
|
||||
};
|
||||
|
||||
describe('DsDynamicTagComponent test suite', () => {
|
||||
|
||||
let testComp: TestComponent;
|
||||
let tagComp: DsDynamicTagComponent;
|
||||
let testFixture: ComponentFixture<TestComponent>;
|
||||
let tagFixture: ComponentFixture<DsDynamicTagComponent>;
|
||||
let html;
|
||||
let chips: Chips;
|
||||
let modelValue: any;
|
||||
|
||||
// async beforeEach
|
||||
beforeEach(async(() => {
|
||||
const authorityServiceStub = new AuthorityServiceStub();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
DynamicFormsCoreModule,
|
||||
DynamicFormsNGBootstrapUIModule,
|
||||
FormsModule,
|
||||
NgbModule.forRoot(),
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
declarations: [
|
||||
DsDynamicTagComponent,
|
||||
TestComponent,
|
||||
], // declare the test component
|
||||
providers: [
|
||||
ChangeDetectorRef,
|
||||
DsDynamicTagComponent,
|
||||
{provide: AuthorityService, useValue: authorityServiceStub},
|
||||
{provide: GLOBAL_CONFIG, useValue: {} as GlobalConfig},
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
describe('', () => {
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
html = `
|
||||
<ds-dynamic-tag [bindId]="bindId"
|
||||
[group]="group"
|
||||
[model]="model"
|
||||
[showErrorMessages]="showErrorMessages"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onValueChange($event)"
|
||||
(focus)="onFocus($event)"></ds-dynamic-tag>`;
|
||||
|
||||
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||
testComp = testFixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create DsDynamicTagComponent', inject([DsDynamicTagComponent], (app: DsDynamicTagComponent) => {
|
||||
|
||||
expect(app).toBeDefined();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when authorityOptions are setted', () => {
|
||||
describe('and init model value is empty', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
tagFixture = TestBed.createComponent(DsDynamicTagComponent);
|
||||
tagComp = tagFixture.componentInstance; // FormComponent test instance
|
||||
tagComp.group = TAG_TEST_GROUP;
|
||||
tagComp.model = new DynamicTagModel(TAG_TEST_MODEL_CONFIG);
|
||||
tagFixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tagFixture.destroy();
|
||||
tagComp = null;
|
||||
});
|
||||
|
||||
it('should init component properly', () => {
|
||||
chips = new Chips([], 'display');
|
||||
expect(tagComp.chips.getChipsItems()).toEqual(chips.getChipsItems());
|
||||
expect(tagComp.searchOptions).toBeDefined();
|
||||
});
|
||||
|
||||
it('should search when 3+ characters typed', fakeAsync(() => {
|
||||
spyOn((tagComp as any).authorityService, 'getEntriesByName').and.callThrough();
|
||||
|
||||
tagComp.search(Observable.of('test')).subscribe(() => {
|
||||
expect((tagComp as any).authorityService.getEntriesByName).toHaveBeenCalled();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should select a results entry properly', fakeAsync(() => {
|
||||
modelValue = [
|
||||
Object.assign(new AuthorityValueModel(), {id: 1, display: 'Name, Lastname', value: 1})
|
||||
];
|
||||
const event: NgbTypeaheadSelectItemEvent = {
|
||||
item: Object.assign(new AuthorityValueModel(), {id: 1, display: 'Name, Lastname', value: 1}),
|
||||
preventDefault: () => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
spyOn(tagComp.change, 'emit');
|
||||
|
||||
tagComp.onSelectItem(event);
|
||||
|
||||
tagFixture.detectChanges();
|
||||
flush();
|
||||
|
||||
expect(tagComp.chips.getChipsItems()).toEqual(modelValue);
|
||||
expect(tagComp.model.value).toEqual(modelValue);
|
||||
expect(tagComp.currentValue).toBeNull();
|
||||
expect(tagComp.change.emit).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should emit blur Event onBlur', () => {
|
||||
spyOn(tagComp.blur, 'emit');
|
||||
tagComp.onBlur(new Event('blur'));
|
||||
expect(tagComp.blur.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit focus Event onFocus', () => {
|
||||
spyOn(tagComp.focus, 'emit');
|
||||
tagComp.onFocus(new Event('focus'));
|
||||
expect(tagComp.focus.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit change Event onBlur when currentValue is not empty', fakeAsync(() => {
|
||||
tagComp.currentValue = 'test value';
|
||||
tagFixture.detectChanges();
|
||||
spyOn(tagComp.blur, 'emit');
|
||||
spyOn(tagComp.change, 'emit');
|
||||
tagComp.onBlur(new Event('blur'));
|
||||
|
||||
tagFixture.detectChanges();
|
||||
flush();
|
||||
|
||||
expect(tagComp.change.emit).toHaveBeenCalled();
|
||||
expect(tagComp.blur.emit).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('and init model value is not empty', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
tagFixture = TestBed.createComponent(DsDynamicTagComponent);
|
||||
tagComp = tagFixture.componentInstance; // FormComponent test instance
|
||||
tagComp.group = TAG_TEST_GROUP;
|
||||
tagComp.model = new DynamicTagModel(TAG_TEST_MODEL_CONFIG);
|
||||
modelValue = [
|
||||
new FormFieldMetadataValueObject('a', null, 'test001'),
|
||||
new FormFieldMetadataValueObject('b', null, 'test002'),
|
||||
new FormFieldMetadataValueObject('c', null, 'test003'),
|
||||
];
|
||||
tagComp.model.value = modelValue;
|
||||
tagFixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tagFixture.destroy();
|
||||
tagComp = null;
|
||||
});
|
||||
|
||||
it('should init component properly', () => {
|
||||
chips = new Chips(modelValue, 'display');
|
||||
expect(tagComp.chips.getChipsItems()).toEqual(chips.getChipsItems());
|
||||
expect(tagComp.searchOptions).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when authorityOptions are not setted', () => {
|
||||
describe('and init model value is empty', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
tagFixture = TestBed.createComponent(DsDynamicTagComponent);
|
||||
tagComp = tagFixture.componentInstance; // FormComponent test instance
|
||||
tagComp.group = TAG_TEST_GROUP;
|
||||
const config = TAG_TEST_MODEL_CONFIG;
|
||||
config.authorityOptions = null;
|
||||
tagComp.model = new DynamicTagModel(config);
|
||||
tagFixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tagFixture.destroy();
|
||||
tagComp = null;
|
||||
});
|
||||
|
||||
it('should init component properly', () => {
|
||||
chips = new Chips([], 'display');
|
||||
expect(tagComp.chips.getChipsItems()).toEqual(chips.getChipsItems());
|
||||
expect(tagComp.searchOptions).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('should add an item on ENTER or key press is \',\' or \';\'', fakeAsync(() => {
|
||||
let event = createKeyUpEvent(13);
|
||||
tagComp.currentValue = 'test value';
|
||||
|
||||
tagFixture.detectChanges();
|
||||
tagComp.onKeyUp(event);
|
||||
|
||||
flush();
|
||||
|
||||
expect(tagComp.model.value).toEqual(['test value']);
|
||||
expect(tagComp.currentValue).toBeNull();
|
||||
|
||||
event = createKeyUpEvent(188);
|
||||
tagComp.currentValue = 'test value';
|
||||
|
||||
tagFixture.detectChanges();
|
||||
tagComp.onKeyUp(event);
|
||||
|
||||
flush();
|
||||
|
||||
expect(tagComp.model.value).toEqual(['test value']);
|
||||
expect(tagComp.currentValue).toBeNull();
|
||||
}));
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// declare a test component
|
||||
@Component({
|
||||
selector: 'ds-test-cmp',
|
||||
template: ``
|
||||
})
|
||||
class TestComponent {
|
||||
|
||||
group: FormGroup = TAG_TEST_GROUP;
|
||||
|
||||
model = new DynamicTagModel(TAG_TEST_MODEL_CONFIG);
|
||||
|
||||
showErrorMessages = false;
|
||||
|
||||
}
|
@@ -0,0 +1,182 @@
|
||||
import { ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { AuthorityService } from '../../../../../../core/integration/authority.service';
|
||||
import { DynamicTagModel } from './dynamic-tag.model';
|
||||
import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model';
|
||||
import { Chips } from '../../../../../chips/models/chips.model';
|
||||
import { hasValue, isNotEmpty } from '../../../../../empty.util';
|
||||
import { isEqual } from 'lodash';
|
||||
import { GlobalConfig } from '../../../../../../../config/global-config.interface';
|
||||
import { GLOBAL_CONFIG } from '../../../../../../../config';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dynamic-tag',
|
||||
styleUrls: ['./dynamic-tag.component.scss'],
|
||||
templateUrl: './dynamic-tag.component.html'
|
||||
})
|
||||
export class DsDynamicTagComponent implements OnInit {
|
||||
@Input() bindId = true;
|
||||
@Input() group: FormGroup;
|
||||
@Input() model: DynamicTagModel;
|
||||
@Input() showErrorMessages = false;
|
||||
|
||||
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() change: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
chips: Chips;
|
||||
hasAuthority: boolean;
|
||||
|
||||
searching = false;
|
||||
searchOptions: IntegrationSearchOptions;
|
||||
searchFailed = false;
|
||||
hideSearchingWhenUnsubscribed = new Observable(() => () => this.changeSearchingStatus(false));
|
||||
currentValue: any;
|
||||
|
||||
formatter = (x: { display: string }) => x.display;
|
||||
|
||||
search = (text$: Observable<string>) =>
|
||||
text$
|
||||
.debounceTime(300)
|
||||
.distinctUntilChanged()
|
||||
.do(() => this.changeSearchingStatus(true))
|
||||
.switchMap((term) => {
|
||||
if (term === '' || term.length < this.model.minChars) {
|
||||
return Observable.of({list: []});
|
||||
} else {
|
||||
this.searchOptions.query = term;
|
||||
return this.authorityService.getEntriesByName(this.searchOptions)
|
||||
.map((authorities) => {
|
||||
// @TODO Pagination for authority is not working, to refactor when it will be fixed
|
||||
return {
|
||||
list: authorities.payload,
|
||||
pageInfo: authorities.pageInfo
|
||||
};
|
||||
})
|
||||
.do(() => this.searchFailed = false)
|
||||
.catch(() => {
|
||||
this.searchFailed = true;
|
||||
return Observable.of({list: []});
|
||||
});
|
||||
}
|
||||
})
|
||||
.map((results) => results.list)
|
||||
.do(() => this.changeSearchingStatus(false))
|
||||
.merge(this.hideSearchingWhenUnsubscribed);
|
||||
|
||||
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||
private authorityService: AuthorityService,
|
||||
private cdr: ChangeDetectorRef) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.hasAuthority = this.model.authorityOptions && hasValue(this.model.authorityOptions.name);
|
||||
if (this.hasAuthority) {
|
||||
this.searchOptions = new IntegrationSearchOptions(
|
||||
this.model.authorityOptions.scope,
|
||||
this.model.authorityOptions.name,
|
||||
this.model.authorityOptions.metadata);
|
||||
}
|
||||
|
||||
this.chips = new Chips(this.model.value, 'display');
|
||||
|
||||
this.chips.chipsItems
|
||||
.subscribe((subItems: any[]) => {
|
||||
const items = this.chips.getChipsItems();
|
||||
// Does not emit change if model value is equal to the current value
|
||||
if (!isEqual(items, this.model.value)) {
|
||||
this.model.valueUpdates.next(items);
|
||||
this.change.emit(event);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
changeSearchingStatus(status: boolean) {
|
||||
this.searching = status;
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
|
||||
onInput(event) {
|
||||
if (event.data) {
|
||||
this.group.markAsDirty();
|
||||
}
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
|
||||
onBlur(event: Event) {
|
||||
if (isNotEmpty(this.currentValue)) {
|
||||
this.addTagsToChips();
|
||||
}
|
||||
this.blur.emit(event);
|
||||
}
|
||||
|
||||
onFocus(event) {
|
||||
this.focus.emit(event);
|
||||
}
|
||||
|
||||
onSelectItem(event: NgbTypeaheadSelectItemEvent) {
|
||||
this.chips.add(event.item);
|
||||
// this.group.controls[this.model.id].setValue(this.model.value);
|
||||
this.updateModel(event);
|
||||
|
||||
setTimeout(() => {
|
||||
// Reset the input text after x ms, mandatory or the formatter overwrite it
|
||||
this.currentValue = null;
|
||||
this.cdr.detectChanges();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
updateModel(event) {
|
||||
this.model.valueUpdates.next(this.chips.getChipsItems());
|
||||
this.change.emit(event);
|
||||
}
|
||||
|
||||
onKeyUp(event) {
|
||||
if (event.keyCode === 13 || event.keyCode === 188) {
|
||||
event.preventDefault();
|
||||
// Key: Enter or ',' or ';'
|
||||
this.addTagsToChips();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
preventEventsPropagation(event) {
|
||||
event.stopPropagation();
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private addTagsToChips() {
|
||||
if (!this.hasAuthority || !this.model.authorityOptions.closed) {
|
||||
let res: string[] = [];
|
||||
res = this.currentValue.split(',');
|
||||
|
||||
const res1 = [];
|
||||
res.forEach((item) => {
|
||||
item.split(';').forEach((i) => {
|
||||
res1.push(i);
|
||||
});
|
||||
});
|
||||
|
||||
res1.forEach((c) => {
|
||||
c = c.trim();
|
||||
if (c.length > 0) {
|
||||
this.chips.add(c);
|
||||
}
|
||||
});
|
||||
|
||||
// this.currentValue = '';
|
||||
setTimeout(() => {
|
||||
// Reset the input text after x ms, mandatory or the formatter overwrite it
|
||||
this.currentValue = null;
|
||||
this.cdr.detectChanges();
|
||||
}, 50);
|
||||
this.updateModel(event);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
import { AUTOCOMPLETE_OFF, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
|
||||
import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model';
|
||||
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
|
||||
|
||||
export const DYNAMIC_FORM_CONTROL_TYPE_TAG = 'TAG';
|
||||
|
||||
export interface DynamicTagModelConfig extends DsDynamicInputModelConfig {
|
||||
minChars?: number;
|
||||
value?: any;
|
||||
}
|
||||
|
||||
export class DynamicTagModel extends DsDynamicInputModel {
|
||||
|
||||
@serializable() minChars: number;
|
||||
@serializable() value: any[];
|
||||
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_TAG;
|
||||
|
||||
constructor(config: DynamicTagModelConfig, layout?: DynamicFormControlLayout) {
|
||||
|
||||
super(config, layout);
|
||||
|
||||
this.autoComplete = AUTOCOMPLETE_OFF;
|
||||
this.minChars = config.minChars || 3;
|
||||
const value = config.value || [];
|
||||
this.valueUpdates.next(value)
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
<ng-template #rt let-r="result" let-t="term">
|
||||
{{ r.display}}
|
||||
</ng-template>
|
||||
<div class="position-relative right-addon">
|
||||
<i *ngIf="searching" class="fa fa-circle-o-notch fa-spin fa-2x fa-fw text-primary position-absolute mt-1 p-0" aria-hidden="true"></i>
|
||||
<input class="form-control"
|
||||
[attr.autoComplete]="model.autoComplete"
|
||||
[class.is-invalid]="showErrorMessages"
|
||||
[dynamicId]="bindId && model.id"
|
||||
[inputFormatter]="formatter"
|
||||
[name]="model.name"
|
||||
[ngbTypeahead]="search"
|
||||
[placeholder]="model.placeholder"
|
||||
[readonly]="model.readOnly"
|
||||
[resultTemplate]="rt"
|
||||
[type]="model.inputType"
|
||||
[(ngModel)]="currentValue"
|
||||
(blur)="onBlur($event)"
|
||||
(focus)="onFocus($event)"
|
||||
(change)="onChange($event)"
|
||||
(input)="onInput($event)"
|
||||
(selectItem)="onSelectItem($event)">
|
||||
|
||||
<div class="invalid-feedback" *ngIf="searchFailed">Sorry, suggestions could not be loaded.</div>
|
||||
</div>
|
@@ -0,0 +1,25 @@
|
||||
@import "../../../../../../../styles/variables";
|
||||
|
||||
/* style fa-spin */
|
||||
.fa-spin {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* align fa-spin */
|
||||
.left-addon .fa-spin { left: 0;}
|
||||
.right-addon .fa-spin { right: 0;}
|
||||
|
||||
:host /deep/ .dropdown-menu {
|
||||
width: 100% !important;
|
||||
max-height: $dropdown-menu-max-height;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
:host /deep/ .dropdown-item.active,
|
||||
:host /deep/ .dropdown-item:active,
|
||||
:host /deep/ .dropdown-item:focus,
|
||||
:host /deep/ .dropdown-item:hover {
|
||||
color: $dropdown-link-hover-color !important;
|
||||
background-color: $dropdown-link-hover-bg !important;
|
||||
}
|
@@ -0,0 +1,224 @@
|
||||
// Load the implementations that should be tested
|
||||
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { async, ComponentFixture, fakeAsync, inject, TestBed, } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/observable/of';
|
||||
|
||||
import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model';
|
||||
import { DynamicFormsCoreModule } from '@ng-dynamic-forms/core';
|
||||
import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap';
|
||||
import { AuthorityService } from '../../../../../../core/integration/authority.service';
|
||||
import { AuthorityServiceStub } from '../../../../../testing/authority-service-stub';
|
||||
import { GlobalConfig } from '../../../../../../../config/global-config.interface';
|
||||
import { GLOBAL_CONFIG } from '../../../../../../../config';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { DsDynamicTypeaheadComponent } from './dynamic-typeahead.component';
|
||||
import { DynamicTypeaheadModel } from './dynamic-typeahead.model';
|
||||
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
|
||||
import { createTestComponent } from '../../../../../testing/utils';
|
||||
|
||||
export const TYPEAHEAD_TEST_GROUP = new FormGroup({
|
||||
typeahead: new FormControl(),
|
||||
});
|
||||
|
||||
export const TYPEAHEAD_TEST_MODEL_CONFIG = {
|
||||
authorityOptions: {
|
||||
closed: false,
|
||||
metadata: 'typeahead',
|
||||
name: 'EVENTAuthority',
|
||||
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
|
||||
} as AuthorityOptions,
|
||||
disabled: false,
|
||||
id: 'typeahead',
|
||||
label: 'Conference',
|
||||
minChars: 3,
|
||||
name: 'typeahead',
|
||||
placeholder: 'Conference',
|
||||
readOnly: false,
|
||||
required: false,
|
||||
repeatable: false,
|
||||
value: undefined
|
||||
};
|
||||
|
||||
describe('DsDynamicTypeaheadComponent test suite', () => {
|
||||
|
||||
let testComp: TestComponent;
|
||||
let typeaheadComp: DsDynamicTypeaheadComponent;
|
||||
let testFixture: ComponentFixture<TestComponent>;
|
||||
let typeaheadFixture: ComponentFixture<DsDynamicTypeaheadComponent>;
|
||||
let html;
|
||||
|
||||
// async beforeEach
|
||||
beforeEach(async(() => {
|
||||
const authorityServiceStub = new AuthorityServiceStub();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
DynamicFormsCoreModule,
|
||||
DynamicFormsNGBootstrapUIModule,
|
||||
FormsModule,
|
||||
NgbModule.forRoot(),
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
declarations: [
|
||||
DsDynamicTypeaheadComponent,
|
||||
TestComponent,
|
||||
], // declare the test component
|
||||
providers: [
|
||||
ChangeDetectorRef,
|
||||
DsDynamicTypeaheadComponent,
|
||||
{provide: AuthorityService, useValue: authorityServiceStub},
|
||||
{provide: GLOBAL_CONFIG, useValue: {} as GlobalConfig},
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
describe('', () => {
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
html = `
|
||||
<ds-dynamic-typeahead [bindId]="bindId"
|
||||
[group]="group"
|
||||
[model]="model"
|
||||
[showErrorMessages]="showErrorMessages"
|
||||
(blur)="onBlur($event)"
|
||||
(change)="onValueChange($event)"
|
||||
(focus)="onFocus($event)"></ds-dynamic-typeahead>`;
|
||||
|
||||
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||
testComp = testFixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should create DsDynamicTypeaheadComponent', inject([DsDynamicTypeaheadComponent], (app: DsDynamicTypeaheadComponent) => {
|
||||
|
||||
expect(app).toBeDefined();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('', () => {
|
||||
describe('when init model value is empty', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
typeaheadFixture = TestBed.createComponent(DsDynamicTypeaheadComponent);
|
||||
typeaheadComp = typeaheadFixture.componentInstance; // FormComponent test instance
|
||||
typeaheadComp.group = TYPEAHEAD_TEST_GROUP;
|
||||
typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG);
|
||||
typeaheadFixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
typeaheadFixture.destroy();
|
||||
typeaheadComp = null;
|
||||
});
|
||||
|
||||
it('should init component properly', () => {
|
||||
expect(typeaheadComp.currentValue).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('should search when 3+ characters typed', fakeAsync(() => {
|
||||
spyOn((typeaheadComp as any).authorityService, 'getEntriesByName').and.callThrough();
|
||||
|
||||
typeaheadComp.search(Observable.of('test')).subscribe(() => {
|
||||
expect((typeaheadComp as any).authorityService.getEntriesByName).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
it('should set model.value on input type when AuthorityOptions.closed is false', () => {
|
||||
const inputDe = typeaheadFixture.debugElement.query(By.css('input.form-control'));
|
||||
const inputElement = inputDe.nativeElement;
|
||||
|
||||
inputElement.value = 'test value';
|
||||
inputElement.dispatchEvent(new Event('input'));
|
||||
|
||||
expect((typeaheadComp.model as any).value).toEqual(new FormFieldMetadataValueObject('test value'))
|
||||
|
||||
});
|
||||
|
||||
it('should not set model.value on input type when AuthorityOptions.closed is true', () => {
|
||||
typeaheadComp.model.authorityOptions.closed = true;
|
||||
typeaheadFixture.detectChanges();
|
||||
const inputDe = typeaheadFixture.debugElement.query(By.css('input.form-control'));
|
||||
const inputElement = inputDe.nativeElement;
|
||||
|
||||
inputElement.value = 'test value';
|
||||
inputElement.dispatchEvent(new Event('input'));
|
||||
|
||||
expect(typeaheadComp.model.value).not.toBeDefined();
|
||||
|
||||
});
|
||||
|
||||
it('should emit blur Event onBlur', () => {
|
||||
spyOn(typeaheadComp.blur, 'emit');
|
||||
typeaheadComp.onBlur(new Event('blur'));
|
||||
expect(typeaheadComp.blur.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit change Event onBlur when AuthorityOptions.closed is false', () => {
|
||||
typeaheadComp.inputValue = 'test value';
|
||||
typeaheadFixture.detectChanges();
|
||||
spyOn(typeaheadComp.blur, 'emit');
|
||||
spyOn(typeaheadComp.change, 'emit');
|
||||
typeaheadComp.onBlur(new Event('blur'));
|
||||
// expect(typeaheadComp.change.emit).toHaveBeenCalled();
|
||||
expect(typeaheadComp.blur.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit focus Event onFocus', () => {
|
||||
spyOn(typeaheadComp.focus, 'emit');
|
||||
typeaheadComp.onFocus(new Event('focus'));
|
||||
expect(typeaheadComp.focus.emit).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('and init model value is not empty', () => {
|
||||
beforeEach(() => {
|
||||
typeaheadFixture = TestBed.createComponent(DsDynamicTypeaheadComponent);
|
||||
typeaheadComp = typeaheadFixture.componentInstance; // FormComponent test instance
|
||||
typeaheadComp.group = TYPEAHEAD_TEST_GROUP;
|
||||
typeaheadComp.model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG);
|
||||
(typeaheadComp.model as any).value = new FormFieldMetadataValueObject('test', null, 'test001');
|
||||
typeaheadFixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
typeaheadFixture.destroy();
|
||||
typeaheadComp = null;
|
||||
});
|
||||
|
||||
it('should init component properly', () => {
|
||||
expect(typeaheadComp.currentValue).toEqual(new FormFieldMetadataValueObject('test', null, 'test001'));
|
||||
});
|
||||
|
||||
it('should emit change Event onChange and currentValue is empty', () => {
|
||||
typeaheadComp.currentValue = null;
|
||||
spyOn(typeaheadComp.change, 'emit');
|
||||
typeaheadComp.onChange(new Event('change'));
|
||||
expect(typeaheadComp.change.emit).toHaveBeenCalled();
|
||||
expect(typeaheadComp.model.value).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
// declare a test component
|
||||
@Component({
|
||||
selector: 'ds-test-cmp',
|
||||
template: ``
|
||||
})
|
||||
class TestComponent {
|
||||
|
||||
group: FormGroup = TYPEAHEAD_TEST_GROUP;
|
||||
|
||||
model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG);
|
||||
|
||||
showErrorMessages = false;
|
||||
|
||||
}
|
@@ -0,0 +1,123 @@
|
||||
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { AuthorityService } from '../../../../../../core/integration/authority.service';
|
||||
import { DynamicTypeaheadModel } from './dynamic-typeahead.model';
|
||||
import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model';
|
||||
import { isEmpty, isNotEmpty } from '../../../../../empty.util';
|
||||
import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dynamic-typeahead',
|
||||
styleUrls: ['./dynamic-typeahead.component.scss'],
|
||||
templateUrl: './dynamic-typeahead.component.html'
|
||||
})
|
||||
export class DsDynamicTypeaheadComponent implements OnInit {
|
||||
@Input() bindId = true;
|
||||
@Input() group: FormGroup;
|
||||
@Input() model: DynamicTypeaheadModel;
|
||||
@Input() showErrorMessages = false;
|
||||
|
||||
@Output() blur: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() change: EventEmitter<any> = new EventEmitter<any>();
|
||||
@Output() focus: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
searching = false;
|
||||
searchOptions: IntegrationSearchOptions;
|
||||
searchFailed = false;
|
||||
hideSearchingWhenUnsubscribed = new Observable(() => () => this.changeSearchingStatus(false));
|
||||
currentValue: any;
|
||||
inputValue: any;
|
||||
|
||||
formatter = (x: { display: string }) => {
|
||||
return (typeof x === 'object') ? x.display : x
|
||||
};
|
||||
|
||||
search = (text$: Observable<string>) =>
|
||||
text$
|
||||
.debounceTime(300)
|
||||
.distinctUntilChanged()
|
||||
.do(() => this.changeSearchingStatus(true))
|
||||
.switchMap((term) => {
|
||||
if (term === '' || term.length < this.model.minChars) {
|
||||
return Observable.of({list: []});
|
||||
} else {
|
||||
this.searchOptions.query = term;
|
||||
return this.authorityService.getEntriesByName(this.searchOptions)
|
||||
.map((authorities) => {
|
||||
// @TODO Pagination for authority is not working, to refactor when it will be fixed
|
||||
return {
|
||||
list: authorities.payload,
|
||||
pageInfo: authorities.pageInfo
|
||||
};
|
||||
})
|
||||
.do(() => this.searchFailed = false)
|
||||
.catch(() => {
|
||||
this.searchFailed = true;
|
||||
return Observable.of({list: []});
|
||||
});
|
||||
}
|
||||
})
|
||||
.map((results) => results.list)
|
||||
.do(() => this.changeSearchingStatus(false))
|
||||
.merge(this.hideSearchingWhenUnsubscribed);
|
||||
|
||||
constructor(private authorityService: AuthorityService, private cdr: ChangeDetectorRef) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.currentValue = this.model.value;
|
||||
this.searchOptions = new IntegrationSearchOptions(
|
||||
this.model.authorityOptions.scope,
|
||||
this.model.authorityOptions.name,
|
||||
this.model.authorityOptions.metadata);
|
||||
this.group.get(this.model.id).valueChanges
|
||||
.filter((value) => this.currentValue !== value)
|
||||
.subscribe((value) => {
|
||||
this.currentValue = value;
|
||||
});
|
||||
}
|
||||
|
||||
changeSearchingStatus(status: boolean) {
|
||||
this.searching = status;
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
|
||||
onInput(event) {
|
||||
if (!this.model.authorityOptions.closed && isNotEmpty(event.target.value)) {
|
||||
const valueObj = new FormFieldMetadataValueObject(event.target.value);
|
||||
this.inputValue = valueObj;
|
||||
this.model.valueUpdates.next(this.inputValue);
|
||||
}
|
||||
}
|
||||
|
||||
onBlur(event: Event) {
|
||||
if (!this.model.authorityOptions.closed && isNotEmpty(this.inputValue)) {
|
||||
this.change.emit(this.inputValue);
|
||||
this.inputValue = null;
|
||||
}
|
||||
this.blur.emit(event);
|
||||
}
|
||||
|
||||
onChange(event: Event) {
|
||||
event.stopPropagation();
|
||||
if (isEmpty(this.currentValue)) {
|
||||
this.model.valueUpdates.next(null);
|
||||
this.change.emit(null);
|
||||
}
|
||||
}
|
||||
|
||||
onFocus(event) {
|
||||
this.focus.emit(event);
|
||||
}
|
||||
|
||||
onSelectItem(event: NgbTypeaheadSelectItemEvent) {
|
||||
this.inputValue = null;
|
||||
this.currentValue = event.item;
|
||||
this.model.valueUpdates.next(event.item);
|
||||
this.change.emit(event.item);
|
||||
}
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
import { AUTOCOMPLETE_OFF, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core';
|
||||
import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model';
|
||||
|
||||
export const DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD = 'TYPEAHEAD';
|
||||
|
||||
export interface DsDynamicTypeaheadModelConfig extends DsDynamicInputModelConfig {
|
||||
minChars?: number;
|
||||
value?: any;
|
||||
}
|
||||
|
||||
export class DynamicTypeaheadModel extends DsDynamicInputModel {
|
||||
|
||||
@serializable() minChars: number;
|
||||
@serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD;
|
||||
|
||||
constructor(config: DsDynamicTypeaheadModelConfig, layout?: DynamicFormControlLayout) {
|
||||
|
||||
super(config, layout);
|
||||
|
||||
this.autoComplete = AUTOCOMPLETE_OFF;
|
||||
this.minChars = config.minChars || 3;
|
||||
}
|
||||
|
||||
}
|
830
src/app/shared/form/builder/form-builder.service.spec.ts
Normal file
830
src/app/shared/form/builder/form-builder.service.spec.ts
Normal file
@@ -0,0 +1,830 @@
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
import {
|
||||
FormArray,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
NG_ASYNC_VALIDATORS,
|
||||
NG_VALIDATORS,
|
||||
ReactiveFormsModule
|
||||
} from '@angular/forms';
|
||||
import {
|
||||
DynamicCheckboxGroupModel,
|
||||
DynamicCheckboxModel,
|
||||
DynamicColorPickerModel,
|
||||
DynamicDatePickerModel,
|
||||
DynamicEditorModel,
|
||||
DynamicFileUploadModel, DynamicFormArrayGroupModel,
|
||||
DynamicFormArrayModel,
|
||||
DynamicFormControlModel,
|
||||
DynamicFormControlValue,
|
||||
DynamicFormGroupModel,
|
||||
DynamicFormService,
|
||||
DynamicFormValidationService,
|
||||
DynamicFormValueControlModel,
|
||||
DynamicInputModel,
|
||||
DynamicRadioGroupModel,
|
||||
DynamicRatingModel,
|
||||
DynamicSelectModel,
|
||||
DynamicSliderModel,
|
||||
DynamicSwitchModel,
|
||||
DynamicTextAreaModel,
|
||||
DynamicTimePickerModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { DynamicTagModel } from './ds-dynamic-form-ui/models/tag/dynamic-tag.model';
|
||||
import { DynamicListCheckboxGroupModel } from './ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model';
|
||||
import { DynamicQualdropModel } from './ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model';
|
||||
import { DynamicScrollableDropdownModel } from './ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
|
||||
import { DynamicGroupModel } from './ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model';
|
||||
import { DynamicLookupModel } from './ds-dynamic-form-ui/models/lookup/dynamic-lookup.model';
|
||||
import { DynamicDsDatePickerModel } from './ds-dynamic-form-ui/models/date-picker/date-picker.model';
|
||||
import { DynamicTypeaheadModel } from './ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.model';
|
||||
import { DynamicListRadioGroupModel } from './ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model';
|
||||
import { AuthorityOptions } from '../../../core/integration/models/authority-options.model';
|
||||
import { FormFieldModel } from './models/form-field.model';
|
||||
import { FormRowModel, SubmissionFormsModel } from '../../../core/shared/config/config-submission-forms.model';
|
||||
import { FormBuilderService } from './form-builder.service';
|
||||
import { DynamicRowGroupModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-group-model';
|
||||
import { DsDynamicInputModel } from './ds-dynamic-form-ui/models/ds-dynamic-input.model';
|
||||
import { FormFieldMetadataValueObject } from './models/form-field-metadata-value.model';
|
||||
import { DynamicConcatModel } from './ds-dynamic-form-ui/models/ds-dynamic-concat.model';
|
||||
import { DynamicLookupNameModel } from './ds-dynamic-form-ui/models/lookup/dynamic-lookup-name.model';
|
||||
import { DynamicRowArrayModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-array-model';
|
||||
|
||||
describe('FormBuilderService test suite', () => {
|
||||
|
||||
let testModel: DynamicFormControlModel[];
|
||||
let testFormConfiguration: SubmissionFormsModel;
|
||||
let service: FormBuilderService;
|
||||
|
||||
function testValidator() {
|
||||
return {testValidator: {valid: true}};
|
||||
}
|
||||
|
||||
function testAsyncValidator() {
|
||||
return new Promise<boolean>((resolve) => setTimeout(() => resolve(true), 0));
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ReactiveFormsModule],
|
||||
providers: [
|
||||
FormBuilderService,
|
||||
DynamicFormService,
|
||||
DynamicFormValidationService,
|
||||
{provide: NG_VALIDATORS, useValue: testValidator, multi: true},
|
||||
{provide: NG_ASYNC_VALIDATORS, useValue: testAsyncValidator, multi: true}
|
||||
]
|
||||
});
|
||||
|
||||
const authorityOptions: AuthorityOptions = {
|
||||
closed: false,
|
||||
metadata: 'list',
|
||||
name: 'type_programme',
|
||||
scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23'
|
||||
};
|
||||
|
||||
testModel = [
|
||||
|
||||
new DynamicSelectModel<string>(
|
||||
{
|
||||
id: 'testSelect',
|
||||
options: [
|
||||
{
|
||||
label: 'Option 1',
|
||||
value: 'option-1'
|
||||
},
|
||||
{
|
||||
label: 'Option 2',
|
||||
value: 'option-2'
|
||||
}
|
||||
],
|
||||
value: 'option-3'
|
||||
}
|
||||
),
|
||||
|
||||
new DynamicInputModel(
|
||||
{
|
||||
id: 'testInput',
|
||||
mask: ['(', /[1-9]/, /\d/, /\d/, ')', ' ', /\d/, /\d/, /\d/, '-', /\d/, /\d/, /\d/, /\d/],
|
||||
}
|
||||
),
|
||||
|
||||
new DynamicCheckboxGroupModel(
|
||||
{
|
||||
id: 'testCheckboxGroup',
|
||||
group: [
|
||||
new DynamicCheckboxModel(
|
||||
{
|
||||
id: 'testCheckboxGroup1',
|
||||
value: true
|
||||
}
|
||||
),
|
||||
new DynamicCheckboxModel(
|
||||
{
|
||||
id: 'testCheckboxGroup2',
|
||||
value: true
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
),
|
||||
|
||||
new DynamicRadioGroupModel<string>(
|
||||
{
|
||||
id: 'testRadioGroup',
|
||||
options: [
|
||||
{
|
||||
label: 'Option 1',
|
||||
value: 'option-1'
|
||||
},
|
||||
{
|
||||
label: 'Option 2',
|
||||
value: 'option-2'
|
||||
}
|
||||
],
|
||||
value: 'option-3'
|
||||
}
|
||||
),
|
||||
|
||||
new DynamicTextAreaModel({id: 'testTextArea'}),
|
||||
|
||||
new DynamicCheckboxModel({id: 'testCheckbox'}),
|
||||
|
||||
new DynamicFormArrayModel(
|
||||
{
|
||||
id: 'testFormArray',
|
||||
initialCount: 5,
|
||||
groupFactory: () => {
|
||||
return [
|
||||
new DynamicInputModel({id: 'testFormArrayGroupInput'}),
|
||||
new DynamicFormArrayModel({
|
||||
id: 'testNestedFormArray', groupFactory: () => [
|
||||
new DynamicInputModel({id: 'testNestedFormArrayGroupInput'})
|
||||
]
|
||||
})
|
||||
];
|
||||
}
|
||||
}
|
||||
),
|
||||
|
||||
new DynamicFormGroupModel(
|
||||
{
|
||||
id: 'testFormGroup',
|
||||
group: [
|
||||
new DynamicInputModel({id: 'nestedTestInput'}),
|
||||
new DynamicTextAreaModel({id: 'nestedTestTextArea'})
|
||||
]
|
||||
}
|
||||
),
|
||||
|
||||
new DynamicSliderModel({id: 'testSlider'}),
|
||||
|
||||
new DynamicSwitchModel({id: 'testSwitch'}),
|
||||
|
||||
new DynamicDatePickerModel({id: 'testDatepicker', value: new Date()}),
|
||||
|
||||
new DynamicFileUploadModel({id: 'testFileUpload'}),
|
||||
|
||||
new DynamicEditorModel({id: 'testEditor'}),
|
||||
|
||||
new DynamicTimePickerModel({id: 'testTimePicker'}),
|
||||
|
||||
new DynamicRatingModel({id: 'testRating'}),
|
||||
|
||||
new DynamicColorPickerModel({id: 'testColorPicker'}),
|
||||
|
||||
new DynamicTypeaheadModel({id: 'testTypeahead'}),
|
||||
|
||||
new DynamicScrollableDropdownModel({id: 'testScrollableDropdown', authorityOptions: authorityOptions}),
|
||||
|
||||
new DynamicTagModel({id: 'testTag'}),
|
||||
|
||||
new DynamicListCheckboxGroupModel({id: 'testCheckboxList', authorityOptions: authorityOptions, repeatable: true}),
|
||||
|
||||
new DynamicListRadioGroupModel({id: 'testRadioList', authorityOptions: authorityOptions, repeatable: false}),
|
||||
|
||||
new DynamicGroupModel({
|
||||
id: 'testRelationGroup',
|
||||
formConfiguration: [{
|
||||
fields: [{
|
||||
hints: 'Enter the name of the author.',
|
||||
input: {type: 'onebox'},
|
||||
label: 'Authors',
|
||||
languageCodes: [],
|
||||
mandatory: 'true',
|
||||
mandatoryMessage: 'Required field!',
|
||||
repeatable: false,
|
||||
selectableMetadata: [{
|
||||
authority: 'RPAuthority',
|
||||
closed: false,
|
||||
metadata: 'dc.contributor.author'
|
||||
}],
|
||||
} as FormFieldModel]
|
||||
} as FormRowModel, {
|
||||
fields: [{
|
||||
hints: 'Enter the affiliation of the author.',
|
||||
input: {type: 'onebox'},
|
||||
label: 'Affiliation',
|
||||
languageCodes: [],
|
||||
mandatory: 'false',
|
||||
repeatable: false,
|
||||
selectableMetadata: [{
|
||||
authority: 'OUAuthority',
|
||||
closed: false,
|
||||
metadata: 'local.contributor.affiliation'
|
||||
}]
|
||||
} as FormFieldModel]
|
||||
} as FormRowModel],
|
||||
mandatoryField: '',
|
||||
name: 'testRelationGroup',
|
||||
relationFields: [],
|
||||
scopeUUID: '',
|
||||
submissionScope: ''
|
||||
}),
|
||||
|
||||
new DynamicDsDatePickerModel({id: 'testDate'}),
|
||||
|
||||
new DynamicLookupModel({id: 'testLookup'}),
|
||||
|
||||
new DynamicLookupNameModel({id: 'testLookupName'}),
|
||||
|
||||
new DynamicQualdropModel({id: 'testCombobox', readOnly: false}),
|
||||
|
||||
new DynamicRowArrayModel(
|
||||
{
|
||||
id: 'testFormRowArray',
|
||||
initialCount: 5,
|
||||
notRepeteable: false,
|
||||
groupFactory: () => {
|
||||
return [
|
||||
new DynamicInputModel({id: 'testFormRowArrayGroupInput'})
|
||||
];
|
||||
},
|
||||
}
|
||||
),
|
||||
];
|
||||
|
||||
testFormConfiguration = {
|
||||
name: 'testFormConfiguration',
|
||||
rows: [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
input: {type: 'lookup'},
|
||||
label: 'Journal',
|
||||
mandatory: 'false',
|
||||
repeatable: false,
|
||||
hints: 'Enter the name of the journal where the item has been\n\t\t\t\t\tpublished, if any.',
|
||||
selectableMetadata: [
|
||||
{
|
||||
metadata: 'journal',
|
||||
authority: 'JOURNALAuthority',
|
||||
closed: false
|
||||
}
|
||||
],
|
||||
languageCodes: []
|
||||
} as FormFieldModel,
|
||||
{
|
||||
input: {type: 'onebox'},
|
||||
label: 'Issue',
|
||||
mandatory: 'false',
|
||||
repeatable: false,
|
||||
hints: ' Enter issue number.',
|
||||
selectableMetadata: [
|
||||
{
|
||||
metadata: 'issue'
|
||||
}
|
||||
],
|
||||
languageCodes: []
|
||||
} as FormFieldModel,
|
||||
{
|
||||
input: {type: 'name'},
|
||||
label: 'Name',
|
||||
mandatory: 'false',
|
||||
repeatable: false,
|
||||
hints: 'Enter full name.',
|
||||
selectableMetadata: [
|
||||
{
|
||||
metadata: 'name'
|
||||
}
|
||||
],
|
||||
languageCodes: []
|
||||
} as FormFieldModel
|
||||
]
|
||||
} as FormRowModel,
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
hints: 'If the item has any identification numbers or codes associated with↵ it, please enter the types and the actual numbers or codes.',
|
||||
input: {type: 'onebox'},
|
||||
label: 'Identifiers',
|
||||
languageCodes: [],
|
||||
mandatory: 'false',
|
||||
repeatable: false,
|
||||
selectableMetadata: [
|
||||
{metadata: 'dc.identifier.issn', label: 'ISSN'},
|
||||
{metadata: 'dc.identifier.other', label: 'Other'},
|
||||
{metadata: 'dc.identifier.ismn', label: 'ISMN'},
|
||||
{metadata: 'dc.identifier.govdoc', label: 'Gov\'t Doc #'},
|
||||
{metadata: 'dc.identifier.uri', label: 'URI'},
|
||||
{metadata: 'dc.identifier.isbn', label: 'ISBN'},
|
||||
{metadata: 'dc.identifier.doi', label: 'DOI'},
|
||||
{metadata: 'dc.identifier.pmid', label: 'PubMed ID'},
|
||||
{metadata: 'dc.identifier.arxiv', label: 'arXiv'}
|
||||
]
|
||||
}, {
|
||||
input: {type: 'onebox'},
|
||||
label: 'Publisher',
|
||||
mandatory: 'false',
|
||||
repeatable: false,
|
||||
hints: 'Enter the name of the publisher of the previously issued instance of this item.',
|
||||
selectableMetadata: [
|
||||
{
|
||||
metadata: 'publisher'
|
||||
}
|
||||
],
|
||||
languageCodes: []
|
||||
}
|
||||
]
|
||||
} as FormRowModel,
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
input: {type: 'onebox'},
|
||||
label: 'Conference',
|
||||
mandatory: 'false',
|
||||
repeatable: false,
|
||||
hints: 'Enter the name of the events, if any.',
|
||||
selectableMetadata: [
|
||||
{
|
||||
metadata: 'conference',
|
||||
authority: 'EVENTAuthority',
|
||||
closed: false
|
||||
}
|
||||
],
|
||||
languageCodes: []
|
||||
}
|
||||
]
|
||||
} as FormRowModel
|
||||
],
|
||||
self: 'testFormConfiguration.url',
|
||||
type: 'submissionform',
|
||||
_links: {
|
||||
self: 'testFormConfiguration.url'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(inject([FormBuilderService], (formService: FormBuilderService) => service = formService));
|
||||
|
||||
it('should find a dynamic form control model by id', () => {
|
||||
|
||||
expect(service.findById('testCheckbox', testModel) instanceof DynamicFormControlModel).toBe(true);
|
||||
expect(service.findById('testCheckboxGroup', testModel) instanceof DynamicFormControlModel).toBe(true);
|
||||
expect(service.findById('testDatepicker', testModel) instanceof DynamicFormControlModel).toBe(true);
|
||||
expect(service.findById('testFormArray', testModel) instanceof DynamicFormControlModel).toBe(true);
|
||||
expect(service.findById('testInput', testModel) instanceof DynamicFormControlModel).toBe(true);
|
||||
expect(service.findById('testRadioGroup', testModel) instanceof DynamicFormControlModel).toBe(true);
|
||||
expect(service.findById('testSelect', testModel) instanceof DynamicFormControlModel).toBe(true);
|
||||
expect(service.findById('testSlider', testModel) instanceof DynamicFormControlModel).toBe(true);
|
||||
expect(service.findById('testSwitch', testModel) instanceof DynamicFormControlModel).toBe(true);
|
||||
expect(service.findById('testTextArea', testModel) instanceof DynamicFormControlModel).toBe(true);
|
||||
expect(service.findById('testFileUpload', testModel) instanceof DynamicFormControlModel).toBe(true);
|
||||
expect(service.findById('testEditor', testModel) instanceof DynamicEditorModel).toBe(true);
|
||||
expect(service.findById('testTimePicker', testModel) instanceof DynamicTimePickerModel).toBe(true);
|
||||
expect(service.findById('testRating', testModel) instanceof DynamicRatingModel).toBe(true);
|
||||
expect(service.findById('testColorPicker', testModel) instanceof DynamicColorPickerModel).toBe(true);
|
||||
});
|
||||
|
||||
it('should find a nested dynamic form control model by id', () => {
|
||||
|
||||
expect(service.findById('testCheckboxGroup1', testModel) instanceof DynamicFormControlModel).toBe(true);
|
||||
expect(service.findById('testCheckboxGroup2', testModel) instanceof DynamicFormControlModel).toBe(true);
|
||||
expect(service.findById('nestedTestInput', testModel) instanceof DynamicFormControlModel).toBe(true);
|
||||
expect(service.findById('testFormRowArrayGroupInput', testModel) instanceof DynamicFormControlModel).toBe(true);
|
||||
expect(service.findById('testFormRowArrayGroupInput', testModel, 2) instanceof DynamicFormControlModel).toBe(true);
|
||||
});
|
||||
|
||||
it('should create an array of form models', () => {
|
||||
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
|
||||
|
||||
expect(formModel[0] instanceof DynamicRowGroupModel).toBe(true);
|
||||
expect((formModel[0] as DynamicRowGroupModel).group.length).toBe(3);
|
||||
expect((formModel[0] as DynamicRowGroupModel).get(0) instanceof DynamicLookupModel).toBe(true);
|
||||
expect((formModel[0] as DynamicRowGroupModel).get(1) instanceof DsDynamicInputModel).toBe(true);
|
||||
expect((formModel[0] as DynamicRowGroupModel).get(2) instanceof DynamicConcatModel).toBe(true);
|
||||
|
||||
expect(formModel[1] instanceof DynamicRowGroupModel).toBe(true);
|
||||
expect((formModel[1] as DynamicRowGroupModel).group.length).toBe(2);
|
||||
expect((formModel[1] as DynamicRowGroupModel).get(0) instanceof DynamicQualdropModel).toBe(true);
|
||||
expect(((formModel[1] as DynamicRowGroupModel).get(0) as DynamicQualdropModel).get(0) instanceof DynamicSelectModel).toBe(true);
|
||||
expect(((formModel[1] as DynamicRowGroupModel).get(0) as DynamicQualdropModel).get(1) instanceof DsDynamicInputModel).toBe(true);
|
||||
expect((formModel[1] as DynamicRowGroupModel).get(1) instanceof DsDynamicInputModel).toBe(true);
|
||||
|
||||
expect(formModel[2] instanceof DynamicRowGroupModel).toBe(true);
|
||||
expect((formModel[2] as DynamicRowGroupModel).group.length).toBe(1);
|
||||
expect((formModel[2] as DynamicRowGroupModel).get(0) instanceof DynamicTypeaheadModel).toBe(true);
|
||||
});
|
||||
|
||||
it('should return form\'s fields value from form model', () => {
|
||||
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
|
||||
let value = {} as any;
|
||||
|
||||
expect(service.getValueFromModel(formModel)).toEqual(value);
|
||||
|
||||
((formModel[0] as DynamicRowGroupModel).get(1) as DsDynamicInputModel).valueUpdates.next('test');
|
||||
|
||||
value = {
|
||||
issue: [new FormFieldMetadataValueObject('test')]
|
||||
};
|
||||
expect(service.getValueFromModel(formModel)).toEqual(value);
|
||||
|
||||
((formModel[2] as DynamicRowGroupModel).get(0) as DynamicTypeaheadModel).valueUpdates.next('test one');
|
||||
value = {
|
||||
issue: [new FormFieldMetadataValueObject('test')],
|
||||
conference: [new FormFieldMetadataValueObject('test one')]
|
||||
};
|
||||
expect(service.getValueFromModel(formModel)).toEqual(value);
|
||||
});
|
||||
|
||||
it('should clear all form\'s fields value', () => {
|
||||
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
|
||||
const value = {} as any;
|
||||
|
||||
((formModel[0] as DynamicRowGroupModel).get(1) as DsDynamicInputModel).valueUpdates.next('test');
|
||||
((formModel[2] as DynamicRowGroupModel).get(0) as DynamicTypeaheadModel).valueUpdates.next('test one');
|
||||
|
||||
service.clearAllModelsValue(formModel);
|
||||
expect(((formModel[0] as DynamicRowGroupModel).get(1) as DynamicTypeaheadModel).value).toEqual(undefined)
|
||||
expect(((formModel[2] as DynamicRowGroupModel).get(0) as DynamicTypeaheadModel).value).toEqual(undefined)
|
||||
});
|
||||
|
||||
it('should return true when model has a custom group model as parent', () => {
|
||||
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
|
||||
let model = service.findById('dc_identifier_QUALDROP_VALUE', formModel);
|
||||
let modelParent = service.findById('dc_identifier_QUALDROP_GROUP', formModel);
|
||||
model.parent = modelParent;
|
||||
|
||||
expect(service.isModelInCustomGroup(model)).toBe(true);
|
||||
|
||||
model = service.findById('name_CONCAT_FIRST_INPUT', formModel);
|
||||
modelParent = service.findById('name_CONCAT_GROUP', formModel);
|
||||
model.parent = modelParent;
|
||||
|
||||
expect(service.isModelInCustomGroup(model)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when model value is an array', () => {
|
||||
let model = service.findById('testCheckboxList', testModel) as DynamicFormArrayModel;
|
||||
|
||||
expect(service.hasArrayGroupValue(model)).toBe(true);
|
||||
|
||||
model = service.findById('testRadioList', testModel) as DynamicFormArrayModel;
|
||||
|
||||
expect(service.hasArrayGroupValue(model)).toBe(true);
|
||||
|
||||
model = service.findById('testTag', testModel) as DynamicFormArrayModel;
|
||||
|
||||
expect(service.hasArrayGroupValue(model)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when model value is a map', () => {
|
||||
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
|
||||
const model = service.findById('dc_identifier_QUALDROP_VALUE', formModel);
|
||||
const modelParent = service.findById('dc_identifier_QUALDROP_GROUP', formModel);
|
||||
model.parent = modelParent;
|
||||
|
||||
expect(service.hasMappedGroupValue(model)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when model is a Qualdrop Group', () => {
|
||||
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
|
||||
let model = service.findById('dc_identifier_QUALDROP_GROUP', formModel);
|
||||
|
||||
expect(service.isQualdropGroup(model)).toBe(true);
|
||||
|
||||
model = service.findById('name_CONCAT_GROUP', formModel);
|
||||
|
||||
expect(service.isQualdropGroup(model)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when model is a Custom or List Group', () => {
|
||||
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
|
||||
let model = service.findById('dc_identifier_QUALDROP_GROUP', formModel);
|
||||
|
||||
expect(service.isCustomOrListGroup(model)).toBe(true);
|
||||
|
||||
model = service.findById('name_CONCAT_GROUP', formModel);
|
||||
|
||||
expect(service.isCustomOrListGroup(model)).toBe(true);
|
||||
|
||||
model = service.findById('testCheckboxList', testModel);
|
||||
|
||||
expect(service.isCustomOrListGroup(model)).toBe(true);
|
||||
|
||||
model = service.findById('testRadioList', testModel);
|
||||
|
||||
expect(service.isCustomOrListGroup(model)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when model is a Custom Group', () => {
|
||||
const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID');
|
||||
let model = service.findById('dc_identifier_QUALDROP_GROUP', formModel);
|
||||
|
||||
expect(service.isCustomGroup(model)).toBe(true);
|
||||
|
||||
model = service.findById('name_CONCAT_GROUP', formModel);
|
||||
|
||||
expect(service.isCustomGroup(model)).toBe(true);
|
||||
|
||||
model = service.findById('testCheckboxList', testModel);
|
||||
|
||||
expect(service.isCustomGroup(model)).toBe(false);
|
||||
|
||||
model = service.findById('testRadioList', testModel);
|
||||
|
||||
expect(service.isCustomGroup(model)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when model is a List Group', () => {
|
||||
let model = service.findById('testCheckboxList', testModel);
|
||||
|
||||
expect(service.isListGroup(model)).toBe(true);
|
||||
|
||||
model = service.findById('testRadioList', testModel);
|
||||
|
||||
expect(service.isListGroup(model)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when model is a Relation Group', () => {
|
||||
const model = service.findById('testRelationGroup', testModel);
|
||||
|
||||
expect(service.isRelationGroup(model)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when model is a Array Row Group', () => {
|
||||
let model = service.findById('testFormRowArray', testModel, null);
|
||||
|
||||
expect(service.isRowArrayGroup(model)).toBe(true);
|
||||
|
||||
model = service.findById('testFormArray', testModel);
|
||||
|
||||
expect(service.isRowArrayGroup(model)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when model is a Array Group', () => {
|
||||
let model = service.findById('testFormRowArray', testModel) as DynamicFormArrayModel;
|
||||
|
||||
expect(service.isArrayGroup(model)).toBe(true);
|
||||
|
||||
model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
|
||||
|
||||
expect(service.isArrayGroup(model)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return properly form control by field id', () => {
|
||||
const group = service.createFormGroup(testModel);
|
||||
const control = group.controls.testLookup;
|
||||
|
||||
expect(service.getFormControlById('testLookup', group, testModel)).toEqual(control);
|
||||
});
|
||||
|
||||
it('should return field id from model', () => {
|
||||
const model = service.findById('testRadioList', testModel);
|
||||
|
||||
expect(service.getId(model)).toEqual('testRadioList');
|
||||
});
|
||||
|
||||
it('should create a form group', () => {
|
||||
|
||||
const formGroup = service.createFormGroup(testModel);
|
||||
|
||||
expect(formGroup instanceof FormGroup).toBe(true);
|
||||
|
||||
expect(formGroup.get('testCheckbox') instanceof FormControl).toBe(true);
|
||||
expect(formGroup.get('testCheckboxGroup') instanceof FormGroup).toBe(true);
|
||||
expect(formGroup.get('testDatepicker') instanceof FormControl).toBe(true);
|
||||
expect(formGroup.get('testFormArray') instanceof FormArray).toBe(true);
|
||||
expect(formGroup.get('testInput') instanceof FormControl).toBe(true);
|
||||
expect(formGroup.get('testRadioGroup') instanceof FormControl).toBe(true);
|
||||
expect(formGroup.get('testSelect') instanceof FormControl).toBe(true);
|
||||
expect(formGroup.get('testTextArea') instanceof FormControl).toBe(true);
|
||||
expect(formGroup.get('testFileUpload') instanceof FormControl).toBe(true);
|
||||
expect(formGroup.get('testEditor') instanceof FormControl).toBe(true);
|
||||
expect(formGroup.get('testTimePicker') instanceof FormControl).toBe(true);
|
||||
expect(formGroup.get('testRating') instanceof FormControl).toBe(true);
|
||||
expect(formGroup.get('testColorPicker') instanceof FormControl).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw when unknown DynamicFormControlModel id is specified in JSON', () => {
|
||||
|
||||
expect(() => service.fromJSON([{id: 'test'}]))
|
||||
.toThrow(new Error(`unknown form control model type defined on JSON object with id "test"`));
|
||||
});
|
||||
|
||||
it('should resolve array group path', () => {
|
||||
|
||||
service.createFormGroup(testModel);
|
||||
|
||||
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
|
||||
const nestedModel = (model.get(0).get(1) as DynamicFormArrayModel).get(0);
|
||||
|
||||
expect(service.getPath(model)).toEqual(['testFormArray']);
|
||||
expect(service.getPath(nestedModel)).toEqual(['testFormArray', '0', 'testNestedFormArray', '0']);
|
||||
});
|
||||
|
||||
it('should add a form control to an existing form group', () => {
|
||||
|
||||
const formGroup = service.createFormGroup(testModel);
|
||||
const nestedFormGroup = formGroup.controls.testFormGroup as FormGroup;
|
||||
const nestedFormGroupModel = testModel[7] as DynamicFormGroupModel;
|
||||
const newModel1 = new DynamicInputModel({id: 'newInput1'});
|
||||
const newModel2 = new DynamicInputModel({id: 'newInput2'});
|
||||
|
||||
service.addFormGroupControl(formGroup, testModel, newModel1);
|
||||
service.addFormGroupControl(nestedFormGroup, nestedFormGroupModel, newModel2);
|
||||
|
||||
expect(formGroup.controls[newModel1.id]).toBeTruthy();
|
||||
expect(testModel[testModel.length - 1] === newModel1).toBe(true);
|
||||
|
||||
expect((formGroup.controls.testFormGroup as FormGroup).controls[newModel2.id]).toBeTruthy();
|
||||
expect(nestedFormGroupModel.get(nestedFormGroupModel.group.length - 1) === newModel2).toBe(true);
|
||||
});
|
||||
|
||||
it('should insert a form control to an existing form group', () => {
|
||||
|
||||
const formGroup = service.createFormGroup(testModel);
|
||||
const nestedFormGroup = formGroup.controls.testFormGroup as FormGroup;
|
||||
const nestedFormGroupModel = testModel[7] as DynamicFormGroupModel;
|
||||
const newModel1 = new DynamicInputModel({id: 'newInput1'});
|
||||
const newModel2 = new DynamicInputModel({id: 'newInput2'});
|
||||
|
||||
service.insertFormGroupControl(4, formGroup, testModel, newModel1);
|
||||
service.insertFormGroupControl(0, nestedFormGroup, nestedFormGroupModel, newModel2);
|
||||
|
||||
expect(formGroup.controls[newModel1.id]).toBeTruthy();
|
||||
expect(testModel[4] === newModel1).toBe(true);
|
||||
expect(service.getPath(testModel[4])).toEqual(['newInput1']);
|
||||
|
||||
expect((formGroup.controls.testFormGroup as FormGroup).controls[newModel2.id]).toBeTruthy();
|
||||
expect(nestedFormGroupModel.get(0) === newModel2).toBe(true);
|
||||
expect(service.getPath(nestedFormGroupModel.get(0))).toEqual(['testFormGroup', 'newInput2']);
|
||||
});
|
||||
|
||||
it('should move an existing form control to a different group position', () => {
|
||||
|
||||
const formGroup = service.createFormGroup(testModel);
|
||||
const nestedFormGroupModel = testModel[7] as DynamicFormGroupModel;
|
||||
const model1 = testModel[0];
|
||||
const model2 = nestedFormGroupModel.get(0);
|
||||
|
||||
service.moveFormGroupControl(0, 2, testModel);
|
||||
|
||||
expect(formGroup.controls[model1.id]).toBeTruthy();
|
||||
expect(testModel[2] === model1).toBe(true);
|
||||
|
||||
service.moveFormGroupControl(0, 1, nestedFormGroupModel);
|
||||
|
||||
expect((formGroup.controls.testFormGroup as FormGroup).controls[model2.id]).toBeTruthy();
|
||||
expect(nestedFormGroupModel.get(1) === model2).toBe(true);
|
||||
});
|
||||
|
||||
it('should remove a form control from an existing form group', () => {
|
||||
|
||||
const formGroup = service.createFormGroup(testModel);
|
||||
const nestedFormGroup = formGroup.controls.testFormGroup as FormGroup;
|
||||
const nestedFormGroupModel = testModel[7] as DynamicFormGroupModel;
|
||||
const length = testModel.length;
|
||||
const size = nestedFormGroupModel.size();
|
||||
const index = 1;
|
||||
const id1 = testModel[index].id;
|
||||
const id2 = nestedFormGroupModel.get(index).id;
|
||||
|
||||
service.removeFormGroupControl(index, formGroup, testModel);
|
||||
|
||||
expect(Object.keys(formGroup.controls).length).toBe(length - 1);
|
||||
expect(formGroup.controls[id1]).toBeUndefined();
|
||||
|
||||
expect(testModel.length).toBe(length - 1);
|
||||
expect(service.findById(id1, testModel)).toBeNull();
|
||||
|
||||
service.removeFormGroupControl(index, nestedFormGroup, nestedFormGroupModel);
|
||||
|
||||
expect(Object.keys(nestedFormGroup.controls).length).toBe(size - 1);
|
||||
expect(nestedFormGroup.controls[id2]).toBeUndefined();
|
||||
|
||||
expect(nestedFormGroupModel.size()).toBe(size - 1);
|
||||
expect(service.findById(id2, testModel)).toBeNull();
|
||||
});
|
||||
|
||||
it('should create a form array', () => {
|
||||
|
||||
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
|
||||
let formArray;
|
||||
|
||||
expect(service.createFormArray).toBeTruthy();
|
||||
|
||||
formArray = service.createFormArray(model);
|
||||
|
||||
expect(formArray instanceof FormArray).toBe(true);
|
||||
expect(formArray.length).toBe(model.initialCount);
|
||||
});
|
||||
|
||||
it('should add a form array group', () => {
|
||||
|
||||
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
|
||||
const formArray = service.createFormArray(model);
|
||||
|
||||
service.addFormArrayGroup(formArray, model);
|
||||
|
||||
expect(formArray.length).toBe(model.initialCount + 1);
|
||||
});
|
||||
|
||||
it('should insert a form array group', () => {
|
||||
|
||||
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
|
||||
const formArray = service.createFormArray(model);
|
||||
|
||||
service.insertFormArrayGroup(0, formArray, model);
|
||||
|
||||
expect(formArray.length).toBe(model.initialCount + 1);
|
||||
});
|
||||
|
||||
it('should move up a form array group', () => {
|
||||
|
||||
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
|
||||
const formArray = service.createFormArray(model);
|
||||
const index = 3;
|
||||
const step = 1;
|
||||
|
||||
(formArray.at(index) as FormGroup).controls.testFormArrayGroupInput.setValue('next test value 1');
|
||||
(formArray.at(index + step) as FormGroup).controls.testFormArrayGroupInput.setValue('next test value 2');
|
||||
|
||||
(model.get(index).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).valueUpdates.next('next test value 1');
|
||||
(model.get(index + step).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).valueUpdates.next('next test value 2');
|
||||
|
||||
service.moveFormArrayGroup(index, step, formArray, model);
|
||||
|
||||
expect(formArray.length).toBe(model.initialCount);
|
||||
|
||||
expect((formArray.at(index) as FormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 2');
|
||||
expect((formArray.at(index + step) as FormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 1');
|
||||
|
||||
expect((model.get(index).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).value).toEqual('next test value 2');
|
||||
expect((model.get(index + step).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).value).toEqual('next test value 1');
|
||||
});
|
||||
|
||||
it('should move down a form array group', () => {
|
||||
|
||||
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
|
||||
const formArray = service.createFormArray(model);
|
||||
const index = 3;
|
||||
const step = -1;
|
||||
|
||||
(formArray.at(index) as FormGroup).controls.testFormArrayGroupInput.setValue('next test value 1');
|
||||
(formArray.at(index + step) as FormGroup).controls.testFormArrayGroupInput.setValue('next test value 2');
|
||||
|
||||
(model.get(index).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).valueUpdates.next('next test value 1');
|
||||
(model.get(index + step).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).valueUpdates.next('next test value 2');
|
||||
|
||||
service.moveFormArrayGroup(index, step, formArray, model);
|
||||
|
||||
expect(formArray.length).toBe(model.initialCount);
|
||||
|
||||
expect((formArray.at(index) as FormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 2');
|
||||
expect((formArray.at(index + step) as FormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 1');
|
||||
|
||||
expect((model.get(index).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).value).toEqual('next test value 2');
|
||||
expect((model.get(index + step).get(0) as DynamicFormValueControlModel<DynamicFormControlValue>).value).toEqual('next test value 1');
|
||||
});
|
||||
|
||||
it('should throw when form array group is to be moved out of bounds', () => {
|
||||
|
||||
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
|
||||
const formArray = service.createFormArray(model);
|
||||
|
||||
expect(() => service.moveFormArrayGroup(2, -5, formArray, model))
|
||||
.toThrow(new Error(`form array group cannot be moved due to index or new index being out of bounds`));
|
||||
});
|
||||
|
||||
it('should remove a form array group', () => {
|
||||
|
||||
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
|
||||
const formArray = service.createFormArray(model);
|
||||
|
||||
service.removeFormArrayGroup(0, formArray, model);
|
||||
|
||||
expect(formArray.length).toBe(model.initialCount - 1);
|
||||
});
|
||||
|
||||
it('should clear a form array', () => {
|
||||
|
||||
const model = service.findById('testFormArray', testModel) as DynamicFormArrayModel;
|
||||
const formArray = service.createFormArray(model);
|
||||
|
||||
service.clearFormArray(formArray, model);
|
||||
|
||||
expect(formArray.length === 0).toBe(true);
|
||||
});
|
||||
});
|
284
src/app/shared/form/builder/form-builder.service.ts
Normal file
284
src/app/shared/form/builder/form-builder.service.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AbstractControl, FormGroup } from '@angular/forms';
|
||||
|
||||
import {
|
||||
DYNAMIC_FORM_CONTROL_TYPE_ARRAY,
|
||||
DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX_GROUP,
|
||||
DYNAMIC_FORM_CONTROL_TYPE_GROUP,
|
||||
DYNAMIC_FORM_CONTROL_TYPE_RADIO_GROUP,
|
||||
DynamicFormArrayModel,
|
||||
DynamicFormControlModel,
|
||||
DynamicFormGroupModel,
|
||||
DynamicFormService,
|
||||
DynamicPathable,
|
||||
JSONUtils,
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { isObject, isString, mergeWith } from 'lodash';
|
||||
|
||||
import { hasValue, isEmpty, isNotEmpty, isNotNull, isNotUndefined, isNull } from '../../empty.util';
|
||||
import { DynamicQualdropModel } from './ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model';
|
||||
import { SubmissionFormsModel } from '../../../core/shared/config/config-submission-forms.model';
|
||||
import {
|
||||
DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP,
|
||||
DynamicGroupModel
|
||||
} from './ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model';
|
||||
import { DYNAMIC_FORM_CONTROL_TYPE_TAG } from './ds-dynamic-form-ui/models/tag/dynamic-tag.model';
|
||||
import { RowParser } from './parsers/row-parser';
|
||||
|
||||
import { DynamicRowArrayModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-array-model';
|
||||
import { DsDynamicInputModel } from './ds-dynamic-form-ui/models/ds-dynamic-input.model';
|
||||
import { FormFieldMetadataValueObject } from './models/form-field-metadata-value.model';
|
||||
|
||||
@Injectable()
|
||||
export class FormBuilderService extends DynamicFormService {
|
||||
|
||||
findById(id: string, groupModel: DynamicFormControlModel[], arrayIndex = null): DynamicFormControlModel | null {
|
||||
|
||||
let result = null;
|
||||
const findByIdFn = (findId: string, findGroupModel: DynamicFormControlModel[], findArrayIndex): void => {
|
||||
|
||||
for (const controlModel of findGroupModel) {
|
||||
|
||||
if (controlModel.id === findId) {
|
||||
|
||||
if (this.isArrayGroup(controlModel) && isNotNull(findArrayIndex)) {
|
||||
result = (controlModel as DynamicFormArrayModel).get(findArrayIndex);
|
||||
} else {
|
||||
result = controlModel;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.isGroup(controlModel)) {
|
||||
findByIdFn(findId, (controlModel as DynamicFormGroupModel).group, findArrayIndex);
|
||||
}
|
||||
|
||||
if (this.isArrayGroup(controlModel)
|
||||
&& (isNull(findArrayIndex) || (controlModel as DynamicFormArrayModel).size > (findArrayIndex))) {
|
||||
const index = (isNull(findArrayIndex)) ? 0 : findArrayIndex;
|
||||
findByIdFn(findId, (controlModel as DynamicFormArrayModel).get(index).group, index);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
findByIdFn(id, groupModel, arrayIndex);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
clearAllModelsValue(groupModel: DynamicFormControlModel[]): void {
|
||||
|
||||
const iterateControlModels = (findGroupModel: DynamicFormControlModel[]): void => {
|
||||
|
||||
for (const controlModel of findGroupModel) {
|
||||
|
||||
if (this.isGroup(controlModel)) {
|
||||
iterateControlModels((controlModel as DynamicFormGroupModel).group);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.isArrayGroup(controlModel)) {
|
||||
iterateControlModels((controlModel as DynamicFormArrayModel).groupFactory());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (controlModel.hasOwnProperty('valueUpdates')) {
|
||||
(controlModel as any).valueUpdates.next(undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
iterateControlModels(groupModel);
|
||||
}
|
||||
|
||||
getValueFromModel(groupModel: DynamicFormControlModel[]): void {
|
||||
|
||||
let result = Object.create({});
|
||||
|
||||
const customizer = (objValue, srcValue) => {
|
||||
if (Array.isArray(objValue)) {
|
||||
return objValue.concat(srcValue);
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeValue = (controlModel, controlValue, controlModelIndex) => {
|
||||
const controlLanguage = (controlModel as DsDynamicInputModel).hasLanguages ? (controlModel as DsDynamicInputModel).language : null;
|
||||
if (isString(controlValue)) {
|
||||
return new FormFieldMetadataValueObject(controlValue, controlLanguage, null, null, controlModelIndex);
|
||||
} else if (isObject(controlValue)) {
|
||||
const authority = controlValue.authority || controlValue.id || null;
|
||||
const place = controlModelIndex || controlValue.place;
|
||||
return new FormFieldMetadataValueObject(controlValue.value, controlLanguage, authority, controlValue.display, place);
|
||||
}
|
||||
};
|
||||
|
||||
const iterateControlModels = (findGroupModel: DynamicFormControlModel[], controlModelIndex: number = 0): void => {
|
||||
let iterateResult = Object.create({});
|
||||
|
||||
// Iterate over all group's controls
|
||||
for (const controlModel of findGroupModel) {
|
||||
|
||||
if (this.isRowGroup(controlModel) && !this.isCustomOrListGroup(controlModel)) {
|
||||
iterateResult = mergeWith(iterateResult, iterateControlModels((controlModel as DynamicFormGroupModel).group), customizer);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.isGroup(controlModel) && !this.isCustomOrListGroup(controlModel)) {
|
||||
iterateResult[controlModel.name] = iterateControlModels((controlModel as DynamicFormGroupModel).group);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.isRowArrayGroup(controlModel)) {
|
||||
for (const arrayItemModel of (controlModel as DynamicRowArrayModel).groups) {
|
||||
iterateResult = mergeWith(iterateResult, iterateControlModels(arrayItemModel.group, arrayItemModel.index), customizer);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.isArrayGroup(controlModel)) {
|
||||
iterateResult[controlModel.name] = [];
|
||||
for (const arrayItemModel of (controlModel as DynamicFormArrayModel).groups) {
|
||||
iterateResult[controlModel.name].push(iterateControlModels(arrayItemModel.group, arrayItemModel.index));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let controlId;
|
||||
// Get the field's name
|
||||
if (this.isQualdropGroup(controlModel)) {
|
||||
// If is instance of DynamicQualdropModel take the qualdrop id as field's name
|
||||
controlId = (controlModel as DynamicQualdropModel).qualdropId;
|
||||
} else {
|
||||
controlId = controlModel.name;
|
||||
}
|
||||
|
||||
if (this.isRelationGroup(controlModel)) {
|
||||
const values = (controlModel as DynamicGroupModel).getGroupValue();
|
||||
values.forEach((groupValue, groupIndex) => {
|
||||
const newGroupValue = Object.create({});
|
||||
Object.keys(groupValue)
|
||||
.forEach((key) => {
|
||||
const normValue = normalizeValue(controlModel, groupValue[key], groupIndex);
|
||||
if (isNotEmpty(normValue) && normValue.hasValue()) {
|
||||
if (iterateResult.hasOwnProperty(key)) {
|
||||
iterateResult[key].push(normValue);
|
||||
} else {
|
||||
iterateResult[key] = [normValue];
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
} else if (isNotUndefined((controlModel as any).value) && isNotEmpty((controlModel as any).value)) {
|
||||
const controlArrayValue = [];
|
||||
// Normalize control value as an array of FormFieldMetadataValueObject
|
||||
const values = Array.isArray((controlModel as any).value) ? (controlModel as any).value : [(controlModel as any).value];
|
||||
values.forEach((controlValue) => {
|
||||
controlArrayValue.push(normalizeValue(controlModel, controlValue, controlModelIndex))
|
||||
});
|
||||
|
||||
if (controlId && iterateResult.hasOwnProperty(controlId) && isNotNull(iterateResult[controlId])) {
|
||||
iterateResult[controlId] = iterateResult[controlId].concat(controlArrayValue);
|
||||
} else {
|
||||
iterateResult[controlId] = isNotEmpty(controlArrayValue) ? controlArrayValue : null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return iterateResult;
|
||||
};
|
||||
|
||||
result = iterateControlModels(groupModel);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
modelFromConfiguration(json: string | SubmissionFormsModel, scopeUUID: string, initFormValues: any = {}, submissionScope?: string, readOnly = false): DynamicFormControlModel[] | never {
|
||||
let rows: DynamicFormControlModel[] = [];
|
||||
const rawData = typeof json === 'string' ? JSON.parse(json, JSONUtils.parseReviver) : json;
|
||||
|
||||
if (rawData.rows && !isEmpty(rawData.rows)) {
|
||||
rawData.rows.forEach((currentRow) => {
|
||||
const rowParsed = new RowParser(currentRow, scopeUUID, initFormValues, submissionScope, readOnly).parse();
|
||||
if (isNotNull(rowParsed)) {
|
||||
if (Array.isArray(rowParsed)) {
|
||||
rows = rows.concat(rowParsed);
|
||||
} else {
|
||||
rows.push(rowParsed);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
isModelInCustomGroup(model: DynamicFormControlModel): boolean {
|
||||
return this.isCustomGroup((model as any).parent);
|
||||
}
|
||||
|
||||
hasArrayGroupValue(model: DynamicFormControlModel): boolean {
|
||||
return model && (this.isListGroup(model) || model.type === DYNAMIC_FORM_CONTROL_TYPE_TAG);
|
||||
}
|
||||
|
||||
hasMappedGroupValue(model: DynamicFormControlModel): boolean {
|
||||
return (this.isQualdropGroup((model as any).parent)
|
||||
|| this.isRelationGroup((model as any).parent));
|
||||
}
|
||||
|
||||
isGroup(model: DynamicFormControlModel): boolean {
|
||||
return model && (model.type === DYNAMIC_FORM_CONTROL_TYPE_GROUP || model.type === DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX_GROUP);
|
||||
}
|
||||
|
||||
isQualdropGroup(model: DynamicFormControlModel): boolean {
|
||||
return (model && model.type === DYNAMIC_FORM_CONTROL_TYPE_GROUP && hasValue((model as any).qualdropId));
|
||||
}
|
||||
|
||||
isCustomGroup(model: DynamicFormControlModel): boolean {
|
||||
return model && ((model as any).type === DYNAMIC_FORM_CONTROL_TYPE_GROUP && (model as any).isCustomGroup === true);
|
||||
}
|
||||
|
||||
isRowGroup(model: DynamicFormControlModel): boolean {
|
||||
return model && ((model as any).type === DYNAMIC_FORM_CONTROL_TYPE_GROUP && (model as any).isRowGroup === true);
|
||||
}
|
||||
|
||||
isCustomOrListGroup(model: DynamicFormControlModel): boolean {
|
||||
return model &&
|
||||
(this.isCustomGroup(model)
|
||||
|| this.isListGroup(model));
|
||||
}
|
||||
|
||||
isListGroup(model: DynamicFormControlModel): boolean {
|
||||
return model &&
|
||||
((model.type === DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX_GROUP && (model as any).isListGroup === true)
|
||||
|| (model.type === DYNAMIC_FORM_CONTROL_TYPE_RADIO_GROUP && (model as any).isListGroup === true));
|
||||
}
|
||||
|
||||
isRelationGroup(model: DynamicFormControlModel): boolean {
|
||||
return model && model.type === DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP;
|
||||
}
|
||||
|
||||
isRowArrayGroup(model: DynamicFormControlModel): boolean {
|
||||
return model.type === DYNAMIC_FORM_CONTROL_TYPE_ARRAY && (model as any).isRowArray === true;
|
||||
}
|
||||
|
||||
isArrayGroup(model: DynamicFormControlModel): boolean {
|
||||
return model.type === DYNAMIC_FORM_CONTROL_TYPE_ARRAY;
|
||||
}
|
||||
|
||||
getFormControlById(id: string, formGroup: FormGroup, groupModel: DynamicFormControlModel[], index = 0): AbstractControl {
|
||||
const fieldModel = this.findById(id, groupModel, index);
|
||||
return isNotEmpty(fieldModel) ? formGroup.get(this.getPath(fieldModel)) : null;
|
||||
}
|
||||
|
||||
getId(model: DynamicPathable): string {
|
||||
if (this.isArrayGroup(model as DynamicFormControlModel)) {
|
||||
return model.index.toString();
|
||||
} else {
|
||||
return ((model as DynamicFormControlModel).id !== (model as DynamicFormControlModel).name) ?
|
||||
(model as DynamicFormControlModel).name :
|
||||
(model as DynamicFormControlModel).id;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
export class FormFieldLanguageValueObject {
|
||||
value: string;
|
||||
language: string;
|
||||
|
||||
constructor(value: string, language: string) {
|
||||
this.value = value;
|
||||
this.language = language;
|
||||
}
|
||||
}
|
||||
|
||||
export interface LanguageCode {
|
||||
display: string;
|
||||
code: string;
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
import { isNotEmpty, isNotNull } from '../../../empty.util';
|
||||
|
||||
export class FormFieldMetadataValueObject {
|
||||
metadata?: string;
|
||||
value: any;
|
||||
display: string;
|
||||
language: any;
|
||||
authority: string;
|
||||
confidence: number;
|
||||
place: number;
|
||||
closed: boolean;
|
||||
label: string;
|
||||
|
||||
constructor(value: any = null,
|
||||
language: any = null,
|
||||
authority: string = null,
|
||||
display: string = null,
|
||||
place: number = 0,
|
||||
confidence: number = -1,
|
||||
metadata: string = null) {
|
||||
this.value = isNotNull(value) ? ((typeof value === 'string') ? value.trim() : value) : null;
|
||||
this.language = language;
|
||||
this.authority = authority;
|
||||
this.display = display || value;
|
||||
|
||||
this.confidence = confidence;
|
||||
if (authority != null) {
|
||||
this.confidence = 600;
|
||||
} else if (isNotEmpty(confidence)) {
|
||||
this.confidence = confidence;
|
||||
}
|
||||
|
||||
this.place = place;
|
||||
if (isNotEmpty(metadata)) {
|
||||
this.metadata = metadata;
|
||||
}
|
||||
}
|
||||
|
||||
hasAuthority(): boolean {
|
||||
return isNotEmpty(this.authority);
|
||||
}
|
||||
|
||||
hasValue(): boolean {
|
||||
return isNotEmpty(this.value);
|
||||
}
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
export class FormFieldPreviousValueObject {
|
||||
|
||||
private _path;
|
||||
private _value;
|
||||
|
||||
constructor(path: any[] = null, value: any = null) {
|
||||
this._path = path;
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
get path() {
|
||||
return this._path;
|
||||
}
|
||||
|
||||
set path(path: any[]) {
|
||||
this._path = path;
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
set value(value: any) {
|
||||
this._value = value;
|
||||
}
|
||||
|
||||
public delete() {
|
||||
this._value = null;
|
||||
this._path = null;
|
||||
}
|
||||
|
||||
public isPathEqual(path) {
|
||||
return this._path && isEqual(this._path, path);
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
export interface FormFieldChangedObject {
|
||||
string: any
|
||||
}
|
43
src/app/shared/form/builder/models/form-field.model.ts
Normal file
43
src/app/shared/form/builder/models/form-field.model.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { autoserialize } from 'cerialize';
|
||||
import { FormRowModel } from '../../../../core/shared/config/config-submission-forms.model';
|
||||
import { LanguageCode } from './form-field-language-value.model';
|
||||
import { FormFieldMetadataValueObject } from './form-field-metadata-value.model';
|
||||
|
||||
export class FormFieldModel {
|
||||
|
||||
@autoserialize
|
||||
hints: string;
|
||||
|
||||
@autoserialize
|
||||
label: string;
|
||||
|
||||
@autoserialize
|
||||
languageCodes: LanguageCode[];
|
||||
|
||||
@autoserialize
|
||||
mandatoryMessage: string;
|
||||
|
||||
@autoserialize
|
||||
mandatory: string;
|
||||
|
||||
@autoserialize
|
||||
repeatable: boolean;
|
||||
|
||||
@autoserialize
|
||||
input: {
|
||||
type: string;
|
||||
regex?: string;
|
||||
};
|
||||
|
||||
@autoserialize
|
||||
selectableMetadata: FormFieldMetadataValueObject[];
|
||||
|
||||
@autoserialize
|
||||
rows: FormRowModel[];
|
||||
|
||||
@autoserialize
|
||||
scope: string;
|
||||
|
||||
@autoserialize
|
||||
value: any;
|
||||
}
|
105
src/app/shared/form/builder/parsers/concat-field-parser.ts
Normal file
105
src/app/shared/form/builder/parsers/concat-field-parser.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { FieldParser } from './field-parser';
|
||||
import { FormFieldModel } from '../models/form-field.model';
|
||||
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||
import { DynamicFormControlLayout, DynamicInputModel, DynamicInputModelConfig } from '@ng-dynamic-forms/core';
|
||||
import {
|
||||
CONCAT_FIRST_INPUT_SUFFIX,
|
||||
CONCAT_GROUP_SUFFIX,
|
||||
CONCAT_SECOND_INPUT_SUFFIX,
|
||||
DynamicConcatModel,
|
||||
DynamicConcatModelConfig
|
||||
} from '../ds-dynamic-form-ui/models/ds-dynamic-concat.model';
|
||||
import { isNotEmpty } from '../../../empty.util';
|
||||
import { ParserOptions } from './parser-options';
|
||||
|
||||
export class ConcatFieldParser extends FieldParser {
|
||||
|
||||
constructor(protected configData: FormFieldModel,
|
||||
protected initFormValues,
|
||||
protected parserOptions: ParserOptions,
|
||||
protected separator: string,
|
||||
protected firstPlaceholder: string = null,
|
||||
protected secondPlaceholder: string = null) {
|
||||
super(configData, initFormValues, parserOptions);
|
||||
|
||||
this.separator = separator;
|
||||
this.firstPlaceholder = firstPlaceholder;
|
||||
this.secondPlaceholder = secondPlaceholder;
|
||||
}
|
||||
|
||||
public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any {
|
||||
|
||||
let clsGroup: DynamicFormControlLayout;
|
||||
let clsInput: DynamicFormControlLayout;
|
||||
let newId: string;
|
||||
|
||||
if (this.configData.selectableMetadata[0].metadata.includes('.')) {
|
||||
newId = this.configData.selectableMetadata[0].metadata
|
||||
.split('.')
|
||||
.slice(0, this.configData.selectableMetadata[0].metadata.split('.').length - 1)
|
||||
.join('.');
|
||||
} else {
|
||||
newId = this.configData.selectableMetadata[0].metadata
|
||||
}
|
||||
|
||||
clsInput = {
|
||||
grid: {
|
||||
host: 'col-sm-6'
|
||||
}
|
||||
};
|
||||
|
||||
const groupId = newId.replace(/\./g, '_') + CONCAT_GROUP_SUFFIX;
|
||||
const concatGroup: DynamicConcatModelConfig = this.initModel(groupId, false, false);
|
||||
|
||||
concatGroup.group = [];
|
||||
concatGroup.separator = this.separator;
|
||||
|
||||
const input1ModelConfig: DynamicInputModelConfig = this.initModel(newId + CONCAT_FIRST_INPUT_SUFFIX, label, false, false);
|
||||
const input2ModelConfig: DynamicInputModelConfig = this.initModel(newId + CONCAT_SECOND_INPUT_SUFFIX, label, true, false);
|
||||
|
||||
if (this.configData.mandatory) {
|
||||
input1ModelConfig.required = true;
|
||||
}
|
||||
|
||||
if (isNotEmpty(this.firstPlaceholder)) {
|
||||
input1ModelConfig.placeholder = this.firstPlaceholder;
|
||||
}
|
||||
|
||||
if (isNotEmpty(this.secondPlaceholder)) {
|
||||
input2ModelConfig.placeholder = this.secondPlaceholder;
|
||||
}
|
||||
|
||||
// Init values
|
||||
if (isNotEmpty(fieldValue)) {
|
||||
const values = fieldValue.value.split(this.separator);
|
||||
|
||||
if (values.length > 1) {
|
||||
input1ModelConfig.value = values[0].trim();
|
||||
input2ModelConfig.value = values[1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Split placeholder if is like 'placeholder1/placeholder2'
|
||||
const placeholder = this.configData.label.split('/');
|
||||
if (placeholder.length === 2) {
|
||||
input1ModelConfig.placeholder = placeholder[0];
|
||||
input2ModelConfig.placeholder = placeholder[1];
|
||||
}
|
||||
|
||||
const model1 = new DynamicInputModel(input1ModelConfig, clsInput);
|
||||
const model2 = new DynamicInputModel(input2ModelConfig, clsInput);
|
||||
concatGroup.group.push(model1);
|
||||
concatGroup.group.push(model2);
|
||||
|
||||
clsGroup = {
|
||||
element: {
|
||||
control: 'form-row',
|
||||
}
|
||||
};
|
||||
const concatModel = new DynamicConcatModel(concatGroup, clsGroup);
|
||||
concatModel.name = this.getFieldId();
|
||||
|
||||
return concatModel;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
import { FormFieldModel } from '../models/form-field.model';
|
||||
import { DynamicConcatModel } from '../ds-dynamic-form-ui/models/ds-dynamic-concat.model';
|
||||
import { SeriesFieldParser } from './series-field-parser';
|
||||
import { DateFieldParser } from './date-field-parser';
|
||||
import { DynamicDsDatePickerModel } from '../ds-dynamic-form-ui/models/date-picker/date-picker.model';
|
||||
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||
import { ParserOptions } from './parser-options';
|
||||
|
||||
describe('DateFieldParser test suite', () => {
|
||||
let field: FormFieldModel;
|
||||
let initFormValues: any = {};
|
||||
|
||||
const parserOptions: ParserOptions = {
|
||||
readOnly: false,
|
||||
submissionScope: null,
|
||||
authorityUuid: null
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
field = {
|
||||
input: {
|
||||
type: 'date'
|
||||
},
|
||||
label: 'Date of Issue.',
|
||||
mandatory: 'true',
|
||||
repeatable: false,
|
||||
hints: 'Please give the date of previous publication or public distribution. You can leave out the day and/or month if they aren\'t applicable.',
|
||||
mandatoryMessage: 'You must enter at least the year.',
|
||||
selectableMetadata: [
|
||||
{
|
||||
metadata: 'date',
|
||||
}
|
||||
],
|
||||
languageCodes: []
|
||||
} as FormFieldModel;
|
||||
|
||||
});
|
||||
|
||||
it('should init parser properly', () => {
|
||||
const parser = new DateFieldParser(field, initFormValues, parserOptions);
|
||||
|
||||
expect(parser instanceof DateFieldParser).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a DynamicDsDatePickerModel object when repeatable option is false', () => {
|
||||
const parser = new DateFieldParser(field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
expect(fieldModel instanceof DynamicDsDatePickerModel).toBe(true);
|
||||
});
|
||||
|
||||
it('should set init value properly', () => {
|
||||
initFormValues = {
|
||||
date: [new FormFieldMetadataValueObject('1983-11-18')],
|
||||
};
|
||||
const expectedValue = '1983-11-18';
|
||||
|
||||
const parser = new DateFieldParser(field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
expect(fieldModel.value).toEqual(expectedValue);
|
||||
});
|
||||
});
|
36
src/app/shared/form/builder/parsers/date-field-parser.ts
Normal file
36
src/app/shared/form/builder/parsers/date-field-parser.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { FieldParser } from './field-parser';
|
||||
import { DynamicDatePickerModelConfig } from '@ng-dynamic-forms/core';
|
||||
import { DynamicDsDatePickerModel } from '../ds-dynamic-form-ui/models/date-picker/date-picker.model';
|
||||
import { isNotEmpty } from '../../../empty.util';
|
||||
import { DS_DATE_PICKER_SEPARATOR } from '../ds-dynamic-form-ui/models/date-picker/date-picker.component';
|
||||
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||
|
||||
export class DateFieldParser extends FieldParser {
|
||||
|
||||
public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any {
|
||||
let malformedDate = false;
|
||||
const inputDateModelConfig: DynamicDatePickerModelConfig = this.initModel(null, label);
|
||||
|
||||
inputDateModelConfig.toggleIcon = 'fa fa-calendar';
|
||||
this.setValues(inputDateModelConfig as any, fieldValue);
|
||||
// Init Data and validity check
|
||||
if (isNotEmpty(inputDateModelConfig.value)) {
|
||||
const value = inputDateModelConfig.value.toString();
|
||||
if (value.length >= 4) {
|
||||
const valuesArray = value.split(DS_DATE_PICKER_SEPARATOR);
|
||||
if (valuesArray.length < 4) {
|
||||
for (let i = 0; i < valuesArray.length; i++) {
|
||||
const len = i === 0 ? 4 : 2;
|
||||
if (valuesArray[i].length !== len) {
|
||||
malformedDate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
const dateModel = new DynamicDsDatePickerModel(inputDateModelConfig);
|
||||
dateModel.malformedDate = malformedDate;
|
||||
return dateModel;
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
import { FormFieldModel } from '../models/form-field.model';
|
||||
import { DropdownFieldParser } from './dropdown-field-parser';
|
||||
import { DynamicScrollableDropdownModel } from '../ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
|
||||
import { ParserOptions } from './parser-options';
|
||||
|
||||
describe('DropdownFieldParser test suite', () => {
|
||||
let field: FormFieldModel;
|
||||
|
||||
const initFormValues = {};
|
||||
const parserOptions: ParserOptions = {
|
||||
readOnly: false,
|
||||
submissionScope: 'testScopeUUID',
|
||||
authorityUuid: null
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
field = {
|
||||
input: {
|
||||
type: 'dropdown'
|
||||
},
|
||||
label: 'Type',
|
||||
mandatory: 'false',
|
||||
repeatable: false,
|
||||
hints: 'Select the tyupe.',
|
||||
selectableMetadata: [
|
||||
{
|
||||
metadata: 'type',
|
||||
authority: 'common_types_dataset',
|
||||
closed: false
|
||||
}
|
||||
],
|
||||
languageCodes: []
|
||||
} as FormFieldModel;
|
||||
|
||||
});
|
||||
|
||||
it('should init parser properly', () => {
|
||||
const parser = new DropdownFieldParser(field, initFormValues, parserOptions);
|
||||
|
||||
expect(parser instanceof DropdownFieldParser).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a DynamicScrollableDropdownModel object when repeatable option is false', () => {
|
||||
const parser = new DropdownFieldParser(field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
expect(fieldModel instanceof DynamicScrollableDropdownModel).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw when authority is not passed', () => {
|
||||
field.selectableMetadata[0].authority = null;
|
||||
const parser = new DropdownFieldParser(field, initFormValues, parserOptions);
|
||||
|
||||
expect(() => parser.parse())
|
||||
.toThrow();
|
||||
});
|
||||
|
||||
});
|
35
src/app/shared/form/builder/parsers/dropdown-field-parser.ts
Normal file
35
src/app/shared/form/builder/parsers/dropdown-field-parser.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { FieldParser } from './field-parser';
|
||||
import { DynamicFormControlLayout, } from '@ng-dynamic-forms/core';
|
||||
import {
|
||||
DynamicScrollableDropdownModel,
|
||||
DynamicScrollableDropdownModelConfig
|
||||
} from '../ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
|
||||
import { isNotEmpty } from '../../../empty.util';
|
||||
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||
|
||||
export class DropdownFieldParser extends FieldParser {
|
||||
|
||||
public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any {
|
||||
const dropdownModelConfig: DynamicScrollableDropdownModelConfig = this.initModel(null, label);
|
||||
let layout: DynamicFormControlLayout;
|
||||
|
||||
if (isNotEmpty(this.configData.selectableMetadata[0].authority)) {
|
||||
this.setAuthorityOptions(dropdownModelConfig, this.parserOptions.authorityUuid);
|
||||
if (isNotEmpty(fieldValue)) {
|
||||
dropdownModelConfig.value = fieldValue;
|
||||
}
|
||||
layout = {
|
||||
element: {
|
||||
control: 'col'
|
||||
},
|
||||
grid: {
|
||||
host: 'col'
|
||||
}
|
||||
};
|
||||
const dropdownModel = new DynamicScrollableDropdownModel(dropdownModelConfig, layout);
|
||||
return dropdownModel;
|
||||
} else {
|
||||
throw Error(`Authority name is not available. Please checks form configuration file.`);
|
||||
}
|
||||
}
|
||||
}
|
307
src/app/shared/form/builder/parsers/field-parser.ts
Normal file
307
src/app/shared/form/builder/parsers/field-parser.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
import { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../../empty.util';
|
||||
import { FormFieldModel } from '../models/form-field.model';
|
||||
|
||||
import { uniqueId } from 'lodash';
|
||||
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||
import {
|
||||
DynamicRowArrayModel,
|
||||
DynamicRowArrayModelConfig
|
||||
} from '../ds-dynamic-form-ui/models/ds-dynamic-row-array-model';
|
||||
import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model';
|
||||
import { DynamicFormControlLayout } from '@ng-dynamic-forms/core';
|
||||
import { setLayout } from './parser.utils';
|
||||
import { AuthorityOptions } from '../../../../core/integration/models/authority-options.model';
|
||||
import { ParserOptions } from './parser-options';
|
||||
|
||||
export abstract class FieldParser {
|
||||
|
||||
protected fieldId: string;
|
||||
|
||||
constructor(protected configData: FormFieldModel, protected initFormValues, protected parserOptions: ParserOptions) {
|
||||
}
|
||||
|
||||
public abstract modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any;
|
||||
|
||||
public parse() {
|
||||
if (((this.getInitValueCount() > 1 && !this.configData.repeatable) || (this.configData.repeatable))
|
||||
&& (this.configData.input.type !== 'list')
|
||||
&& (this.configData.input.type !== 'tag')
|
||||
&& (this.configData.input.type !== 'group')
|
||||
) {
|
||||
let arrayCounter = 0;
|
||||
let fieldArrayCounter = 0;
|
||||
|
||||
const config = {
|
||||
id: uniqueId() + '_array',
|
||||
label: this.configData.label,
|
||||
initialCount: this.getInitArrayIndex(),
|
||||
notRepeteable: !this.configData.repeatable,
|
||||
groupFactory: () => {
|
||||
let model;
|
||||
if ((arrayCounter === 0)) {
|
||||
model = this.modelFactory();
|
||||
arrayCounter++;
|
||||
} else {
|
||||
const fieldArrayOfValueLenght = this.getInitValueCount(arrayCounter - 1);
|
||||
let fieldValue = null;
|
||||
if (fieldArrayOfValueLenght > 0) {
|
||||
fieldValue = this.getInitFieldValue(arrayCounter - 1, fieldArrayCounter++);
|
||||
if (fieldArrayCounter === fieldArrayOfValueLenght) {
|
||||
fieldArrayCounter = 0;
|
||||
arrayCounter++;
|
||||
}
|
||||
}
|
||||
model = this.modelFactory(fieldValue, false);
|
||||
}
|
||||
setLayout(model, 'element', 'host', 'col');
|
||||
if (model.hasLanguages) {
|
||||
setLayout(model, 'grid', 'control', 'col');
|
||||
}
|
||||
return [model];
|
||||
}
|
||||
} as DynamicRowArrayModelConfig;
|
||||
|
||||
const layout: DynamicFormControlLayout = {
|
||||
grid: {
|
||||
group: 'form-row'
|
||||
}
|
||||
};
|
||||
|
||||
return new DynamicRowArrayModel(config, layout);
|
||||
|
||||
} else {
|
||||
const model = this.modelFactory(this.getInitFieldValue());
|
||||
if (model.hasLanguages) {
|
||||
setLayout(model, 'grid', 'control', 'col');
|
||||
}
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
protected getInitValueCount(index = 0, fieldId?): number {
|
||||
const fieldIds = fieldId || this.getAllFieldIds();
|
||||
if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length === 1 && this.initFormValues.hasOwnProperty(fieldIds[0])) {
|
||||
return this.initFormValues[fieldIds[0]].length;
|
||||
} else if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length > 1) {
|
||||
const values = [];
|
||||
fieldIds.forEach((id) => {
|
||||
if (this.initFormValues.hasOwnProperty(id)) {
|
||||
values.push(this.initFormValues[id].length);
|
||||
}
|
||||
});
|
||||
return values[index];
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
protected getInitGroupValues(): FormFieldMetadataValueObject[] {
|
||||
const fieldIds = this.getAllFieldIds();
|
||||
if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length === 1 && this.initFormValues.hasOwnProperty(fieldIds[0])) {
|
||||
return this.initFormValues[fieldIds[0]];
|
||||
}
|
||||
}
|
||||
|
||||
protected getInitFieldValues(fieldId): FormFieldMetadataValueObject[] {
|
||||
if (isNotEmpty(this.initFormValues) && isNotNull(fieldId) && this.initFormValues.hasOwnProperty(fieldId)) {
|
||||
return this.initFormValues[fieldId];
|
||||
}
|
||||
}
|
||||
|
||||
protected getInitFieldValue(outerIndex = 0, innerIndex = 0, fieldId?): FormFieldMetadataValueObject {
|
||||
const fieldIds = fieldId || this.getAllFieldIds();
|
||||
if (isNotEmpty(this.initFormValues)
|
||||
&& isNotNull(fieldIds)
|
||||
&& fieldIds.length === 1
|
||||
&& this.initFormValues.hasOwnProperty(fieldIds[outerIndex])
|
||||
&& this.initFormValues[fieldIds[outerIndex]].length > innerIndex) {
|
||||
return this.initFormValues[fieldIds[outerIndex]][innerIndex];
|
||||
} else if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length > 1) {
|
||||
const values: FormFieldMetadataValueObject[] = [];
|
||||
fieldIds.forEach((id) => {
|
||||
if (this.initFormValues.hasOwnProperty(id)) {
|
||||
const valueObj: FormFieldMetadataValueObject = Object.assign(new FormFieldMetadataValueObject(), this.initFormValues[id][innerIndex]);
|
||||
valueObj.metadata = id;
|
||||
// valueObj.value = this.initFormValues[id][innerIndex];
|
||||
values.push(valueObj);
|
||||
}
|
||||
});
|
||||
return values[outerIndex];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected getInitArrayIndex() {
|
||||
const fieldIds: any = this.getAllFieldIds();
|
||||
if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length === 1 && this.initFormValues.hasOwnProperty(fieldIds)) {
|
||||
return this.initFormValues[fieldIds].length;
|
||||
} else if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length > 1) {
|
||||
let counter = 0;
|
||||
fieldIds.forEach((id) => {
|
||||
if (this.initFormValues.hasOwnProperty(id)) {
|
||||
counter = counter + this.initFormValues[id].length;
|
||||
}
|
||||
});
|
||||
return (counter === 0) ? 1 : counter;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
protected getFieldId(): string {
|
||||
const ids = this.getAllFieldIds();
|
||||
return isNotNull(ids) ? ids[0] : null;
|
||||
}
|
||||
|
||||
protected getAllFieldIds(): string[] {
|
||||
if (Array.isArray(this.configData.selectableMetadata)) {
|
||||
if (this.configData.selectableMetadata.length === 1) {
|
||||
return [this.configData.selectableMetadata[0].metadata];
|
||||
} else {
|
||||
const ids = [];
|
||||
this.configData.selectableMetadata.forEach((entry) => ids.push(entry.metadata));
|
||||
return ids;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected initModel(id?: string, label = true, labelEmpty = false, setErrors = true) {
|
||||
|
||||
const controlModel = Object.create(null);
|
||||
|
||||
// Sets input ID
|
||||
this.fieldId = id ? id : this.getFieldId();
|
||||
|
||||
// Sets input name (with the original field's id value)
|
||||
controlModel.name = this.fieldId;
|
||||
|
||||
// input ID doesn't allow dots, so replace them
|
||||
controlModel.id = (this.fieldId).replace(/\./g, '_');
|
||||
|
||||
// Set read only option
|
||||
controlModel.readOnly = this.parserOptions.readOnly;
|
||||
controlModel.disabled = this.parserOptions.readOnly;
|
||||
|
||||
// Set label
|
||||
this.setLabel(controlModel, label, labelEmpty);
|
||||
|
||||
controlModel.placeholder = this.configData.label;
|
||||
|
||||
if (this.configData.mandatory && setErrors) {
|
||||
this.markAsRequired(controlModel);
|
||||
}
|
||||
|
||||
if (this.hasRegex()) {
|
||||
this.addPatternValidator(controlModel);
|
||||
}
|
||||
|
||||
// Available Languages
|
||||
if (this.configData.languageCodes && this.configData.languageCodes.length > 0) {
|
||||
(controlModel as DsDynamicInputModel).languageCodes = this.configData.languageCodes;
|
||||
}
|
||||
|
||||
return controlModel;
|
||||
}
|
||||
|
||||
protected hasRegex() {
|
||||
return hasValue(this.configData.input.regex);
|
||||
}
|
||||
|
||||
protected addPatternValidator(controlModel) {
|
||||
const regex = new RegExp(this.configData.input.regex);
|
||||
controlModel.validators = Object.assign({}, controlModel.validators, {pattern: regex});
|
||||
controlModel.errorMessages = Object.assign(
|
||||
{},
|
||||
controlModel.errorMessages,
|
||||
{pattern: 'error.validation.pattern'});
|
||||
|
||||
}
|
||||
|
||||
protected markAsRequired(controlModel) {
|
||||
controlModel.required = true;
|
||||
controlModel.validators = Object.assign({}, controlModel.validators, {required: null});
|
||||
controlModel.errorMessages = Object.assign(
|
||||
{},
|
||||
controlModel.errorMessages,
|
||||
{required: this.configData.mandatoryMessage});
|
||||
}
|
||||
|
||||
protected setLabel(controlModel, label = true, labelEmpty = false) {
|
||||
if (label) {
|
||||
controlModel.label = (labelEmpty) ? ' ' : this.configData.label;
|
||||
}
|
||||
}
|
||||
|
||||
protected setOptions(controlModel) {
|
||||
// Checks if field has multiple values and sets options available
|
||||
if (isNotUndefined(this.configData.selectableMetadata) && this.configData.selectableMetadata.length > 1) {
|
||||
controlModel.options = [];
|
||||
this.configData.selectableMetadata.forEach((option, key) => {
|
||||
if (key === 0) {
|
||||
controlModel.value = option.metadata;
|
||||
}
|
||||
controlModel.options.push({label: option.label, value: option.metadata});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public setAuthorityOptions(controlModel, authorityUuid) {
|
||||
if (isNotEmpty(this.configData.selectableMetadata[0].authority)) {
|
||||
controlModel.authorityOptions = new AuthorityOptions(
|
||||
this.configData.selectableMetadata[0].authority,
|
||||
this.configData.selectableMetadata[0].metadata,
|
||||
authorityUuid,
|
||||
this.configData.selectableMetadata[0].closed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public setValues(modelConfig: DsDynamicInputModelConfig, fieldValue: any, forceValueAsObj: boolean = false, groupModel?: boolean) {
|
||||
if (isNotEmpty(fieldValue)) {
|
||||
if (groupModel) {
|
||||
// Array, values is an array
|
||||
modelConfig.value = this.getInitGroupValues();
|
||||
if (Array.isArray(modelConfig.value) && modelConfig.value.length > 0 && modelConfig.value[0].language) {
|
||||
// Array Item has language, ex. AuthorityModel
|
||||
modelConfig.language = modelConfig.value[0].language;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof fieldValue === 'object') {
|
||||
modelConfig.language = fieldValue.language;
|
||||
if (forceValueAsObj) {
|
||||
modelConfig.value = fieldValue;
|
||||
} else {
|
||||
modelConfig.value = fieldValue.value;
|
||||
}
|
||||
// if (hasValue(fieldValue.language)) {
|
||||
// // Instance of FormFieldLanguageValueObject
|
||||
// modelConfig.value = fieldValue.value;
|
||||
// } else if (hasValue(fieldValue.metadata)) {
|
||||
// // Is a combobox field's value
|
||||
// modelConfig.value = fieldValue.value;
|
||||
// } else {
|
||||
// // Instance of FormFieldMetadataValueObject
|
||||
// modelConfig.value = fieldValue;
|
||||
// }
|
||||
} else {
|
||||
if (forceValueAsObj) {
|
||||
// If value isn't an instance of FormFieldMetadataValueObject instantiate it
|
||||
modelConfig.value = new FormFieldMetadataValueObject(fieldValue);
|
||||
} else {
|
||||
if (typeof fieldValue === 'string') {
|
||||
// Case only string
|
||||
modelConfig.value = fieldValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modelConfig;
|
||||
}
|
||||
|
||||
}
|
111
src/app/shared/form/builder/parsers/group-field-parser.spec.ts
Normal file
111
src/app/shared/form/builder/parsers/group-field-parser.spec.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { FormFieldModel } from '../models/form-field.model';
|
||||
import { GroupFieldParser } from './group-field-parser';
|
||||
import { DynamicGroupModel } from '../ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model';
|
||||
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||
import { ParserOptions } from './parser-options';
|
||||
|
||||
describe('GroupFieldParser test suite', () => {
|
||||
let field: FormFieldModel;
|
||||
let initFormValues = {};
|
||||
|
||||
const parserOptions: ParserOptions = {
|
||||
readOnly: false,
|
||||
submissionScope: 'testScopeUUID',
|
||||
authorityUuid: 'WORKSPACE'
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
field = {
|
||||
input: {
|
||||
type: 'group'
|
||||
},
|
||||
rows: [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
input: {
|
||||
type: 'onebox'
|
||||
},
|
||||
label: 'Author',
|
||||
mandatory: 'false',
|
||||
repeatable: false,
|
||||
hints: 'Enter the name of the author.',
|
||||
selectableMetadata: [
|
||||
{
|
||||
metadata: 'author'
|
||||
}
|
||||
],
|
||||
languageCodes: []
|
||||
},
|
||||
{
|
||||
input: {
|
||||
type: 'onebox'
|
||||
},
|
||||
label: 'Affiliation',
|
||||
mandatory: false,
|
||||
repeatable: true,
|
||||
hints: 'Enter the affiliation of the author.',
|
||||
selectableMetadata: [
|
||||
{
|
||||
metadata: 'affiliation'
|
||||
}
|
||||
],
|
||||
languageCodes: []
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
label: 'Authors',
|
||||
mandatory: 'true',
|
||||
repeatable: false,
|
||||
mandatoryMessage: 'Entering at least the first author is mandatory.',
|
||||
hints: 'Enter the names of the authors of this item.',
|
||||
selectableMetadata: [
|
||||
{
|
||||
metadata: 'author'
|
||||
}
|
||||
],
|
||||
languageCodes: []
|
||||
} as FormFieldModel;
|
||||
|
||||
});
|
||||
|
||||
it('should init parser properly', () => {
|
||||
const parser = new GroupFieldParser(field, initFormValues, parserOptions);
|
||||
|
||||
expect(parser instanceof GroupFieldParser).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a DynamicGroupModel object', () => {
|
||||
const parser = new GroupFieldParser(field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
|
||||
expect(fieldModel instanceof DynamicGroupModel).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw when rows configuration is empty', () => {
|
||||
field.rows = null;
|
||||
const parser = new GroupFieldParser(field, initFormValues, parserOptions);
|
||||
|
||||
expect(() => parser.parse())
|
||||
.toThrow();
|
||||
});
|
||||
|
||||
it('should set group init value properly', () => {
|
||||
initFormValues = {
|
||||
author: [new FormFieldMetadataValueObject('test author')],
|
||||
affiliation: [new FormFieldMetadataValueObject('test affiliation')]
|
||||
};
|
||||
const parser = new GroupFieldParser(field, initFormValues, parserOptions);
|
||||
|
||||
const fieldModel = parser.parse();
|
||||
const expectedValue = [{
|
||||
author: new FormFieldMetadataValueObject('test author'),
|
||||
affiliation: new FormFieldMetadataValueObject('test affiliation')
|
||||
}];
|
||||
|
||||
expect(fieldModel.value).toEqual(expectedValue);
|
||||
});
|
||||
|
||||
});
|
62
src/app/shared/form/builder/parsers/group-field-parser.ts
Normal file
62
src/app/shared/form/builder/parsers/group-field-parser.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { FieldParser } from './field-parser';
|
||||
import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model';
|
||||
import { FormFieldModel } from '../models/form-field.model';
|
||||
import {
|
||||
DynamicGroupModel,
|
||||
DynamicGroupModelConfig,
|
||||
PLACEHOLDER_PARENT_METADATA
|
||||
} from '../ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model';
|
||||
import { isNotEmpty } from '../../../empty.util';
|
||||
import { FormRowModel } from '../../../../core/shared/config/config-submission-forms.model';
|
||||
|
||||
export class GroupFieldParser extends FieldParser {
|
||||
|
||||
public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean) {
|
||||
const modelConfiguration: DynamicGroupModelConfig = this.initModel(null, label);
|
||||
|
||||
modelConfiguration.scopeUUID = this.parserOptions.authorityUuid;
|
||||
modelConfiguration.submissionScope = this.parserOptions.submissionScope;
|
||||
if (this.configData && this.configData.rows && this.configData.rows.length > 0) {
|
||||
modelConfiguration.formConfiguration = this.configData.rows;
|
||||
modelConfiguration.relationFields = [];
|
||||
this.configData.rows.forEach((row: FormRowModel) => {
|
||||
row.fields.forEach((field: FormFieldModel) => {
|
||||
if (field.selectableMetadata[0].metadata === this.configData.selectableMetadata[0].metadata) {
|
||||
if (!field.mandatory) {
|
||||
// throw new Error(`Configuration not valid: Main field ${this.configData.selectableMetadata[0].metadata} may be mandatory`);
|
||||
}
|
||||
modelConfiguration.mandatoryField = this.configData.selectableMetadata[0].metadata;
|
||||
} else {
|
||||
modelConfiguration.relationFields.push(field.selectableMetadata[0].metadata);
|
||||
}
|
||||
})
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Configuration not valid: ${modelConfiguration.name}`);
|
||||
}
|
||||
|
||||
if (isNotEmpty(this.getInitGroupValues())) {
|
||||
modelConfiguration.value = [];
|
||||
const mandatoryFieldEntries: FormFieldMetadataValueObject[] = this.getInitFieldValues(modelConfiguration.mandatoryField);
|
||||
mandatoryFieldEntries.forEach((entry, index) => {
|
||||
const item = Object.create(null);
|
||||
const listFields = [modelConfiguration.mandatoryField].concat(modelConfiguration.relationFields);
|
||||
listFields.forEach((fieldId) => {
|
||||
const value = this.getInitFieldValue(0, index, [fieldId]);
|
||||
item[fieldId] = isNotEmpty(value) ? value : PLACEHOLDER_PARENT_METADATA;
|
||||
});
|
||||
modelConfiguration.value.push(item);
|
||||
})
|
||||
}
|
||||
const cls = {
|
||||
element: {
|
||||
container: 'mb-3'
|
||||
}
|
||||
};
|
||||
|
||||
const model = new DynamicGroupModel(modelConfiguration, cls);
|
||||
model.name = this.getFieldId();
|
||||
return model;
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user