From 7a32d18b1b200045310739c049365d6b4d1a7177 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 9 May 2018 12:14:18 +0200 Subject: [PATCH 01/41] Merged dynamic form module --- package.json | 9 + resources/i18n/en.json | 15 + src/app/app.reducer.ts | 67 +-- src/app/core/cache/response-cache.models.ts | 11 + src/app/core/core.module.ts | 2 + src/app/core/data/request.models.ts | 10 + src/app/core/integration/authority.service.ts | 19 + src/app/core/integration/integration-data.ts | 12 + .../integration/integration-object-factory.ts | 17 + .../integration-response-parsing.service.ts | 47 ++ .../core/integration/integration.service.ts | 85 ++++ src/app/core/integration/intergration-type.ts | 4 + .../models/authority-options.model.ts | 16 + .../models/authority-value.model.ts | 20 + .../models/integration-options.model.ts | 14 + .../integration/models/integration.model.ts | 12 + .../shared/config/config-authority.model.ts | 23 + .../shared/config/config-object-factory.ts | 4 + .../config/config-submission-forms.model.ts | 10 +- src/app/core/shared/config/config-type.ts | 3 +- src/app/shared/animations/shrink.ts | 13 + src/app/shared/chips/chips.component.html | 33 ++ src/app/shared/chips/chips.component.scss | 9 + src/app/shared/chips/chips.component.ts | 95 ++++ .../shared/chips/models/chips-item.model.ts | 73 +++ src/app/shared/chips/models/chips.model.ts | 130 ++++++ src/app/shared/date.util.ts | 19 + .../ds-dynamic-form-control.component.html | 442 ++++++++++++++++++ .../ds-dynamic-form-control.component.spec.ts | 0 .../ds-dynamic-form-control.component.ts | 176 +++++++ .../ds-dynamic-form.component.html | 12 + .../ds-dynamic-form.component.scss | 5 + .../ds-dynamic-form.component.ts | 47 ++ .../ds-date-picker.component.html | 47 ++ .../ds-date-picker.component.scss | 3 + .../ds-date-picker.component.ts | 170 +++++++ .../ds-date-picker/ds-date-picker.model.ts | 21 + .../models/ds-dynamic-combobox.model.ts | 64 +++ .../models/ds-dynamic-concat.model.ts | 50 ++ .../dynamic-group.component.html | 65 +++ .../dynamic-group.component.scss | 3 + .../dynamic-group.components.ts | 224 +++++++++ .../ds-dynamic-group/dynamic-group.model.ts | 41 ++ .../models/ds-dynamic-input.model.ts | 86 ++++ .../models/ds-dynamic-row-array-model.ts | 18 + .../models/ds-dynamic-row-group-model.ts | 5 + .../models/ds-dynamic-textarea.model.ts | 30 ++ .../list/dynamic-list-checkbox-group.model.ts | 59 +++ .../list/dynamic-list-radio-group.model.ts | 35 ++ .../models/list/dynamic-list.component.html | 66 +++ .../models/list/dynamic-list.component.scss | 1 + .../models/list/dynamic-list.component.ts | 137 ++++++ .../lookup/dynamic-lookup.component.html | 130 ++++++ .../lookup/dynamic-lookup.component.scss | 68 +++ .../models/lookup/dynamic-lookup.component.ts | 220 +++++++++ .../models/lookup/dynamic-lookup.model.ts | 37 ++ ...dynamic-scrollable-dropdown.component.html | 43 ++ ...dynamic-scrollable-dropdown.component.scss | 26 ++ .../dynamic-scrollable-dropdown.component.ts | 94 ++++ .../dynamic-scrollable-dropdown.model.ts | 27 ++ .../models/tag/dynamic-tag.component.html | 46 ++ .../models/tag/dynamic-tag.component.scss | 34 ++ .../models/tag/dynamic-tag.component.ts | 194 ++++++++ .../models/tag/dynamic-tag.model.spec.ts | 143 ++++++ .../models/tag/dynamic-tag.model.ts | 28 ++ .../dynamic-typeahead.component.html | 25 + .../dynamic-typeahead.component.scss | 25 + .../typeahead/dynamic-typeahead.component.ts | 121 +++++ .../typeahead/dynamic-typeahead.model.ts | 25 + .../form/builder/form-builder.service.ts | 212 +++++++++ .../models/form-field-language-value.model.ts | 14 + .../models/form-field-metadata-value.model.ts | 42 ++ .../form-field-previous-value-object.ts | 37 ++ .../form-field-unexpected-object.model.ts | 3 + .../form/builder/models/form-field.model.ts | 42 ++ .../builder/parsers/concat-field-parser.ts | 99 ++++ .../form/builder/parsers/date-field-parser.ts | 49 ++ .../builder/parsers/dropdown-field-parser.ts | 44 ++ .../form/builder/parsers/field-parser.ts | 272 +++++++++++ .../builder/parsers/group-field-parser.ts | 67 +++ .../form/builder/parsers/list-field-parser.ts | 52 +++ .../builder/parsers/lookup-field-parser.ts | 30 ++ .../parsers/lookup-name-field-parser.ts | 22 + .../form/builder/parsers/name-field-parser.ts | 10 + .../builder/parsers/onebox-field-parser.ts | 95 ++++ .../form/builder/parsers/parser.utils.ts | 17 + .../shared/form/builder/parsers/row-parser.ts | 162 +++++++ .../builder/parsers/series-field-parser.ts | 10 + .../form/builder/parsers/tag-field-parser.ts | 31 ++ .../builder/parsers/textarea-field-parser.ts | 32 ++ .../builder/parsers/twobox-field-parser.ts | 10 + src/app/shared/form/form.actions.ts | 126 +++++ src/app/shared/form/form.component.html | 61 +++ src/app/shared/form/form.component.scss | 23 + src/app/shared/form/form.component.spec.ts | 175 +++++++ src/app/shared/form/form.component.ts | 274 +++++++++++ src/app/shared/form/form.effects.ts | 11 + src/app/shared/form/form.reducers.ts | 179 +++++++ src/app/shared/form/form.service.ts | 118 +++++ src/app/shared/form/selectors.ts | 10 + .../number-picker.component.html | 33 ++ .../number-picker.component.scss | 44 ++ .../number-picker/number-picker.component.ts | 147 ++++++ src/app/shared/shared.module.ts | 62 ++- .../shared/uploader/uploader-options.model.ts | 13 + .../shared/uploader/uploader.component.html | 51 ++ .../shared/uploader/uploader.component.scss | 40 ++ src/app/shared/uploader/uploader.component.ts | 168 +++++++ src/app/shared/uploader/uploader.service.ts | 18 + src/app/shared/utils/console.pipe.ts | 11 + yarn.lock | 42 ++ 111 files changed, 6778 insertions(+), 45 deletions(-) create mode 100644 src/app/core/integration/authority.service.ts create mode 100644 src/app/core/integration/integration-data.ts create mode 100644 src/app/core/integration/integration-object-factory.ts create mode 100644 src/app/core/integration/integration-response-parsing.service.ts create mode 100644 src/app/core/integration/integration.service.ts create mode 100644 src/app/core/integration/intergration-type.ts create mode 100644 src/app/core/integration/models/authority-options.model.ts create mode 100644 src/app/core/integration/models/authority-value.model.ts create mode 100644 src/app/core/integration/models/integration-options.model.ts create mode 100644 src/app/core/integration/models/integration.model.ts create mode 100644 src/app/core/shared/config/config-authority.model.ts create mode 100644 src/app/shared/animations/shrink.ts create mode 100644 src/app/shared/chips/chips.component.html create mode 100644 src/app/shared/chips/chips.component.scss create mode 100644 src/app/shared/chips/chips.component.ts create mode 100644 src/app/shared/chips/models/chips-item.model.ts create mode 100644 src/app/shared/chips/models/chips.model.ts create mode 100644 src/app/shared/date.util.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.spec.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.scss create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component.html create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component.scss create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.model.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-combobox.model.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.component.html create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.component.scss create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.components.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.model.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-group-model.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.html create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.scss create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.scss create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.model.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.scss create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.html create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.scss create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.model.spec.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.model.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.html create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.scss create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts create mode 100644 src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.model.ts create mode 100644 src/app/shared/form/builder/form-builder.service.ts create mode 100644 src/app/shared/form/builder/models/form-field-language-value.model.ts create mode 100644 src/app/shared/form/builder/models/form-field-metadata-value.model.ts create mode 100644 src/app/shared/form/builder/models/form-field-previous-value-object.ts create mode 100644 src/app/shared/form/builder/models/form-field-unexpected-object.model.ts create mode 100644 src/app/shared/form/builder/models/form-field.model.ts create mode 100644 src/app/shared/form/builder/parsers/concat-field-parser.ts create mode 100644 src/app/shared/form/builder/parsers/date-field-parser.ts create mode 100644 src/app/shared/form/builder/parsers/dropdown-field-parser.ts create mode 100644 src/app/shared/form/builder/parsers/field-parser.ts create mode 100644 src/app/shared/form/builder/parsers/group-field-parser.ts create mode 100644 src/app/shared/form/builder/parsers/list-field-parser.ts create mode 100644 src/app/shared/form/builder/parsers/lookup-field-parser.ts create mode 100644 src/app/shared/form/builder/parsers/lookup-name-field-parser.ts create mode 100644 src/app/shared/form/builder/parsers/name-field-parser.ts create mode 100644 src/app/shared/form/builder/parsers/onebox-field-parser.ts create mode 100644 src/app/shared/form/builder/parsers/parser.utils.ts create mode 100644 src/app/shared/form/builder/parsers/row-parser.ts create mode 100644 src/app/shared/form/builder/parsers/series-field-parser.ts create mode 100644 src/app/shared/form/builder/parsers/tag-field-parser.ts create mode 100644 src/app/shared/form/builder/parsers/textarea-field-parser.ts create mode 100644 src/app/shared/form/builder/parsers/twobox-field-parser.ts create mode 100644 src/app/shared/form/form.actions.ts create mode 100644 src/app/shared/form/form.component.html create mode 100644 src/app/shared/form/form.component.scss create mode 100644 src/app/shared/form/form.component.spec.ts create mode 100644 src/app/shared/form/form.component.ts create mode 100644 src/app/shared/form/form.effects.ts create mode 100644 src/app/shared/form/form.reducers.ts create mode 100644 src/app/shared/form/form.service.ts create mode 100644 src/app/shared/form/selectors.ts create mode 100644 src/app/shared/number-picker/number-picker.component.html create mode 100644 src/app/shared/number-picker/number-picker.component.scss create mode 100644 src/app/shared/number-picker/number-picker.component.ts create mode 100644 src/app/shared/uploader/uploader-options.model.ts create mode 100644 src/app/shared/uploader/uploader.component.html create mode 100644 src/app/shared/uploader/uploader.component.scss create mode 100644 src/app/shared/uploader/uploader.component.ts create mode 100644 src/app/shared/uploader/uploader.service.ts create mode 100644 src/app/shared/utils/console.pipe.ts diff --git a/package.json b/package.json index 2878daf1c8..dcc22c6d71 100644 --- a/package.json +++ b/package.json @@ -80,14 +80,19 @@ "@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", "@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", "angulartics2": "^5.2.0", + "angular2-text-mask": "8.0.4", "body-parser": "1.18.2", "bootstrap": "^4.0.0", "cerialize": "0.1.18", @@ -103,10 +108,14 @@ "jsonschema": "1.2.2", "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", diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 53ae9015f6..dc316af0bf 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -154,5 +154,20 @@ "item": "Error fetching item", "objects": "Error fetching objects", "search-results": "Error fetching search results" + }, + "form": { + "submit": "Submit", + "cancel": "Cancel", + "search": "Search", + "remove": "Remove", + "first-name": "First name", + "last-name": "Last name", + "loading": "Loading...", + "no-results": "No results found", + "no-value": "No value entered", + "group-collapse": "Collapse", + "group-expand": "Expand", + "group-collapse-help": "Click here to collapse", + "group-expand-help": "Click here to expand and add more element" } } diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts index 117710e3c0..0e5cb17a96 100644 --- a/src/app/app.reducer.ts +++ b/src/app/app.reducer.ts @@ -1,32 +1,35 @@ -import { ActionReducerMap } from '@ngrx/store'; -import * as fromRouter from '@ngrx/router-store'; - -import { headerReducer, HeaderState } from './header/header.reducer'; -import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer'; -import { - SearchSidebarState, - sidebarReducer -} from './+search-page/search-sidebar/search-sidebar.reducer'; -import { - filterReducer, - SearchFiltersState -} from './+search-page/search-filters/search-filter/search-filter.reducer'; -import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer'; - -export interface AppState { - router: fromRouter.RouterReducerState; - hostWindow: HostWindowState; - header: HeaderState; - searchSidebar: SearchSidebarState; - searchFilter: SearchFiltersState; - truncatable: TruncatablesState; -} - -export const appReducers: ActionReducerMap = { - router: fromRouter.routerReducer, - hostWindow: hostWindowReducer, - header: headerReducer, - searchSidebar: sidebarReducer, - searchFilter: filterReducer, - truncatable: truncatableReducer -}; +import { ActionReducerMap } from '@ngrx/store'; +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.reducers'; +import { + SearchSidebarState, + sidebarReducer +} from './+search-page/search-sidebar/search-sidebar.reducer'; +import { + filterReducer, + SearchFiltersState +} from './+search-page/search-filters/search-filter/search-filter.reducer'; +import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer'; + +export interface AppState { + router: fromRouter.RouterReducerState; + hostWindow: HostWindowState; + header: HeaderState; + forms: FormState; + searchSidebar: SearchSidebarState; + searchFilter: SearchFiltersState; + truncatable: TruncatablesState; +} + +export const appReducers: ActionReducerMap = { + router: fromRouter.routerReducer, + hostWindow: hostWindowReducer, + header: headerReducer, + forms: formReducer, + searchSidebar: sidebarReducer, + searchFilter: filterReducer, + truncatable: truncatableReducer +}; diff --git a/src/app/core/cache/response-cache.models.ts b/src/app/core/cache/response-cache.models.ts index f061e78e6c..d86b437695 100644 --- a/src/app/core/cache/response-cache.models.ts +++ b/src/app/core/cache/response-cache.models.ts @@ -5,6 +5,7 @@ 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'; /* tslint:disable:max-classes-per-file */ export class RestResponse { @@ -106,4 +107,14 @@ export class ConfigSuccessResponse extends RestResponse { super(true, statusCode); } } + +export class IntegrationSuccessResponse extends RestResponse { + constructor( + public dataDefinition: IntegrationModel[], + public statusCode: string, + public pageInfo?: PageInfo + ) { + super(true, statusCode); + } +} /* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 86abf87d62..3ae9fd5407 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -44,6 +44,7 @@ import { HALEndpointService } from './shared/hal-endpoint.service'; import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service'; import { FacetValueMapResponseParsingService } from './data/facet-value-map-response-parsing.service'; import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service'; +import { UploaderService } from '../shared/uploader/uploader.service'; const IMPORTS = [ CommonModule, @@ -88,6 +89,7 @@ const PROVIDERS = [ SubmissionDefinitionsConfigService, SubmissionFormsConfigService, SubmissionSectionsConfigService, + UploaderService, UUIDService, { provide: NativeWindowService, useFactory: NativeWindowFactory } ]; diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index 21df69b3a2..1fea85de50 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -7,6 +7,7 @@ import { ResponseParsingService } from './parsing.service'; import { EndpointMapResponseParsingService } from './endpoint-map-response-parsing.service'; import { BrowseResponseParsingService } from './browse-response-parsing.service'; import { ConfigResponseParsingService } from './config-response-parsing.service'; +import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service'; /* tslint:disable:max-classes-per-file */ @@ -174,6 +175,15 @@ export class ConfigRequest extends GetRequest { } } +export class IntegrationRequest extends GetRequest { + constructor(uuid: string, href: string) { + super(uuid, href); + } + + getResponseParser(): GenericConstructor { + return IntegrationResponseParsingService; + } +} export class RequestError extends Error { statusText: string; } diff --git a/src/app/core/integration/authority.service.ts b/src/app/core/integration/authority.service.ts new file mode 100644 index 0000000000..cb2595adc4 --- /dev/null +++ b/src/app/core/integration/authority.service.ts @@ -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(); + } +} diff --git a/src/app/core/integration/integration-data.ts b/src/app/core/integration/integration-data.ts new file mode 100644 index 0000000000..cdcec605da --- /dev/null +++ b/src/app/core/integration/integration-data.ts @@ -0,0 +1,12 @@ +import { PageInfo } from '../shared/page-info.model'; +import { IntegrationModel } from './models/integration.model'; + +/** + * A class to represent the data retrieved by a Eperson service + */ +export class IntegrationData { + constructor( + public pageInfo: PageInfo, + public payload: IntegrationModel[] + ) { } +} diff --git a/src/app/core/integration/integration-object-factory.ts b/src/app/core/integration/integration-object-factory.ts new file mode 100644 index 0000000000..4f69dbd6fe --- /dev/null +++ b/src/app/core/integration/integration-object-factory.ts @@ -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 { + switch (type) { + case IntegrationType.Authority: { + return AuthorityValueModel; + } + default: { + return undefined; + } + } + } +} diff --git a/src/app/core/integration/integration-response-parsing.service.ts b/src/app/core/integration/integration-response-parsing.service.ts new file mode 100644 index 0000000000..5d6ce09114 --- /dev/null +++ b/src/app/core/integration/integration-response-parsing.service.ts @@ -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(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} + ) + ); + } + } + +} diff --git a/src/app/core/integration/integration.service.ts b/src/app/core/integration/integration.service.ts new file mode 100644 index 0000000000..f1c770336a --- /dev/null +++ b/src/app/core/integration/integration.service.ts @@ -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 { + 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 { + 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(); + } + +} diff --git a/src/app/core/integration/intergration-type.ts b/src/app/core/integration/intergration-type.ts new file mode 100644 index 0000000000..882dc6d8ce --- /dev/null +++ b/src/app/core/integration/intergration-type.ts @@ -0,0 +1,4 @@ + +export enum IntegrationType { + Authority = 'authority' +} diff --git a/src/app/core/integration/models/authority-options.model.ts b/src/app/core/integration/models/authority-options.model.ts new file mode 100644 index 0000000000..0b826f7f9c --- /dev/null +++ b/src/app/core/integration/models/authority-options.model.ts @@ -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; + } +} diff --git a/src/app/core/integration/models/authority-value.model.ts b/src/app/core/integration/models/authority-value.model.ts new file mode 100644 index 0000000000..e2ef9ce9db --- /dev/null +++ b/src/app/core/integration/models/authority-value.model.ts @@ -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; +} diff --git a/src/app/core/integration/models/integration-options.model.ts b/src/app/core/integration/models/integration-options.model.ts new file mode 100644 index 0000000000..5f158bd47c --- /dev/null +++ b/src/app/core/integration/models/integration-options.model.ts @@ -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) { + + } +} diff --git a/src/app/core/integration/models/integration.model.ts b/src/app/core/integration/models/integration.model.ts new file mode 100644 index 0000000000..d3383ab94a --- /dev/null +++ b/src/app/core/integration/models/integration.model.ts @@ -0,0 +1,12 @@ +import { autoserialize } from 'cerialize'; + +export abstract class IntegrationModel { + + @autoserialize + public type: string; + + @autoserialize + public _links: { + [name: string]: string + } +} diff --git a/src/app/core/shared/config/config-authority.model.ts b/src/app/core/shared/config/config-authority.model.ts new file mode 100644 index 0000000000..bbb8605bcc --- /dev/null +++ b/src/app/core/shared/config/config-authority.model.ts @@ -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; + +} diff --git a/src/app/core/shared/config/config-object-factory.ts b/src/app/core/shared/config/config-object-factory.ts index 4f56a84812..4cb5016983 100644 --- a/src/app/core/shared/config/config-object-factory.ts +++ b/src/app/core/shared/config/config-object-factory.ts @@ -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 { @@ -22,6 +23,9 @@ export class ConfigObjectFactory { case ConfigType.SubmissionSections: { return SubmissionSectionModel } + case ConfigType.Authority: { + return ConfigAuthorityModel + } default: { return undefined; } diff --git a/src/app/core/shared/config/config-submission-forms.model.ts b/src/app/core/shared/config/config-submission-forms.model.ts index 0b094091a7..98d3bf9ce7 100644 --- a/src/app/core/shared/config/config-submission-forms.model.ts +++ b/src/app/core/shared/config/config-submission-forms.model.ts @@ -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[]; } diff --git a/src/app/core/shared/config/config-type.ts b/src/app/core/shared/config/config-type.ts index ab0a18e516..17ed099229 100644 --- a/src/app/core/shared/config/config-type.ts +++ b/src/app/core/shared/config/config-type.ts @@ -10,5 +10,6 @@ export enum ConfigType { SubmissionForm = 'submissionform', SubmissionForms = 'submissionforms', SubmissionSections = 'submissionsections', - SubmissionSection = 'submissionsection' + SubmissionSection = 'submissionsection', + Authority = 'authority' } diff --git a/src/app/shared/animations/shrink.ts b/src/app/shared/animations/shrink.ts new file mode 100644 index 0000000000..28f1d2a567 --- /dev/null +++ b/src/app/shared/animations/shrink.ts @@ -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})) + ]) +]); diff --git a/src/app/shared/chips/chips.component.html b/src/app/shared/chips/chips.component.html new file mode 100644 index 0000000000..870f7db396 --- /dev/null +++ b/src/app/shared/chips/chips.component.html @@ -0,0 +1,33 @@ +
+ +
diff --git a/src/app/shared/chips/chips.component.scss b/src/app/shared/chips/chips.component.scss new file mode 100644 index 0000000000..9d7eae7edd --- /dev/null +++ b/src/app/shared/chips/chips.component.scss @@ -0,0 +1,9 @@ +@import "../../../styles/variables"; + +.chip-selected { + background-color: map-get($theme-colors, info) !important; +} + +.chip-label { + max-width: 10rem; +} diff --git a/src/app/shared/chips/chips.component.ts b/src/app/shared/chips/chips.component.ts new file mode 100644 index 0000000000..eea0993c98 --- /dev/null +++ b/src/app/shared/chips/chips.component.ts @@ -0,0 +1,95 @@ +import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, } from '@angular/core'; + +import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; +import { SortablejsOptions } from 'angular-sortablejs'; + +import { Chips } from './models/chips.model'; +import { ChipsItem } from './models/chips-item.model'; +import { UploaderService } from '../uploader/uploader.service'; + +@Component({ + selector: 'ds-chips', + styleUrls: ['./chips.component.scss'], + templateUrl: './chips.component.html', +}) + +export class ChipsComponent implements OnChanges { + @Input() chips: Chips; + @Input() wrapperClass: string; + @Input() editable: boolean; + + @Output() selected: EventEmitter = new EventEmitter(); + @Output() remove: EventEmitter = new EventEmitter(); + @Output() change: EventEmitter = new EventEmitter(); + + options: SortablejsOptions; + dragged = -1; + tipText: string; + + constructor(private cdr: ChangeDetectorRef, private uploaderService: UploaderService) { + this.options = { + animation: 300, + chosenClass: 'm-0', + dragClass: 'm-0', + filter: '.chips-sort-ignore', + ghostClass: 'm-0' + }; + } + + ngOnInit() { + if (!this.editable) { + this.editable = false; + } + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.chips && !changes.chips.isFirstChange()) { + this.chips = changes.chips.currentValue; + } + } + + chipsSelected(event: Event, index: number) { + event.preventDefault(); + if (this.editable) { + this.chips.getChips().forEach((item: ChipsItem, i: number) => { + if (i === index) { + item.setEditMode(); + } else { + item.unsetEditMode(); + } + }); + this.selected.emit(index); + } + } + + removeChips(event: Event, index: number) { + event.preventDefault(); + event.stopPropagation(); + // Can't remove if this element is in editMode + if (!this.chips.getChipByIndex(index).editMode) { + this.chips.remove(this.chips.getChipByIndex(index)); + } + } + + onDragStart(tooltip: NgbTooltip, index) { + tooltip.close(); + this.uploaderService.overrideDragOverPage(); + this.dragged = index; + } + + onDragEnd(event) { + this.uploaderService.allowDragOverPage(); + this.dragged = -1; + this.chips.updateOrder(); + } + + showTooltip(tooltip: NgbTooltip, index, content) { + tooltip.close(); + if (!this.chips.getChipByIndex(index).editMode && this.dragged === -1) { + this.tipText = content; + this.cdr.detectChanges(); + tooltip.open(); + } + } + +} diff --git a/src/app/shared/chips/models/chips-item.model.ts b/src/app/shared/chips/models/chips-item.model.ts new file mode 100644 index 0000000000..0efdd0984b --- /dev/null +++ b/src/app/shared/chips/models/chips-item.model.ts @@ -0,0 +1,73 @@ +import { uniqueId } from 'lodash'; +import { isNotEmpty } from '../../empty.util'; + +export interface ChipsItemIcon { + style: string; + tooltip?: any; +} + +export class ChipsItem { + public id: string; + public display: string; + public item: any; + public editMode?: boolean; + public icons?: ChipsItemIcon[]; + + private fieldToDisplay: string; + private objToDisplay: string; + + constructor(item: any, + fieldToDisplay: string, + objToDisplay: string, + icons?: ChipsItemIcon[], + editMode?: boolean) { + + this.id = uniqueId(); + this.item = item; + this.fieldToDisplay = fieldToDisplay; + this.objToDisplay = objToDisplay; + this.setDisplayText(); + this.editMode = editMode || false; + this.icons = icons || []; + } + + hasIcons(): boolean { + return isNotEmpty(this.icons); + } + + setEditMode(): void { + this.editMode = true; + } + + updateIcons(icons: ChipsItemIcon[]): void { + this.icons = icons; + } + + updateItem(item: any): void { + this.item = item; + this.setDisplayText(); + } + + unsetEditMode(): void { + this.editMode = false; + } + + private setDisplayText(): void { + let value = this.item; + if ( typeof this.item === 'object') { + // Check If displayField is in an internal object + const obj = this.objToDisplay ? this.item[this.objToDisplay] : this.item; + const displayFieldBkp = 'value'; + + if (obj instanceof Object && obj && obj[this.fieldToDisplay]) { + value = obj[this.fieldToDisplay]; + } else if (obj instanceof Object && obj && obj[displayFieldBkp]) { + value = obj[displayFieldBkp]; + } else { + value = obj; + } + } + + this.display = value; + } +} diff --git a/src/app/shared/chips/models/chips.model.ts b/src/app/shared/chips/models/chips.model.ts new file mode 100644 index 0000000000..4476c1cd77 --- /dev/null +++ b/src/app/shared/chips/models/chips.model.ts @@ -0,0 +1,130 @@ +import { findIndex, isEqual } from 'lodash'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import { ChipsItem, ChipsItemIcon } from './chips-item.model'; +import { hasValue } from '../../empty.util'; +import { FormFieldMetadataValueObject } from '../../form/builder/models/form-field-metadata-value.model'; +import { AuthorityValueModel } from '../../../core/integration/models/authority-value.model'; + +export interface ChipsIconsConfig { + [metadata: string]: string; +} + +export class Chips { + chipsItems: BehaviorSubject; + displayField: string; + displayObj: string; + iconsConfig: ChipsIconsConfig; + + private _items: ChipsItem[]; + + constructor(items: any[] = [], + displayField: string = 'display', + displayObj?: string, + iconsConfig?: ChipsIconsConfig) { + + this.displayField = displayField; + this.displayObj = displayObj; + this.iconsConfig = iconsConfig || Object.create({}); + if (Array.isArray(items)) { + this.setInitialItems(items); + } + } + + public add(item: any): void { + const icons = this.getChipsIcons(item); + const chipsItem = new ChipsItem(item, this.displayField, this.displayObj, icons); + + const duplicatedIndex = findIndex(this._items, {display: chipsItem.display.trim()}); + if (duplicatedIndex === -1 || !isEqual(item, this.getChipByIndex(duplicatedIndex).item)) { + this._items.push(chipsItem); + this.chipsItems.next(this._items); + } + } + + public getChipById(id): ChipsItem { + const index = findIndex(this._items, {id: id}); + return this.getChipByIndex(index); + } + + public getChipByIndex(index): ChipsItem { + if (this._items.length > 0 && this._items[index]) { + return this._items[index]; + } else { + return null; + } + } + + public getChips(): ChipsItem[] { + return this._items; + } + + /** + * To use to get items before to store it + * @returns {any[]} + */ + public getChipsItems(): any[] { + const out = []; + this._items.forEach((item) => { + out.push(item.item); + }); + return out; + } + + public hasItems(): boolean { + return this._items.length > 0; + } + + public remove(chipsItem: ChipsItem): void { + const index = findIndex(this._items, {id: chipsItem.id}); + this._items.splice(index, 1); + this.chipsItems.next(this._items); + } + + public update(id: string, item: any): void { + const chipsItemTarget = this.getChipById(id); + const icons = this.getChipsIcons(item); + + chipsItemTarget.updateItem(item); + chipsItemTarget.updateIcons(icons); + chipsItemTarget.unsetEditMode(); + this.chipsItems.next(this._items); + } + + public updateOrder(): void { + this.chipsItems.next(this._items); + } + + private getChipsIcons(item) { + const icons = []; + Object.keys(item) + .forEach((metadata) => { + const value = item[metadata]; + if (hasValue(value) + && (value instanceof FormFieldMetadataValueObject || value instanceof AuthorityValueModel) + && ((value as FormFieldMetadataValueObject).authority || (value as AuthorityValueModel).id) + && this.iconsConfig.hasOwnProperty(metadata)) { + + const icon: ChipsItemIcon = { + style: this.iconsConfig[metadata] + }; + icons.push(icon); + } + }); + + return icons; + } + + /** + * Sets initial items, used in edit mode + */ + private setInitialItems(items: any[]): void { + this._items = []; + items.forEach((item) => { + const icons = this.getChipsIcons(item); + const chipsItem = new ChipsItem(item, this.displayField, this.displayObj, icons); + this._items.push(chipsItem); + }); + + this.chipsItems = new BehaviorSubject(this._items); + } +} diff --git a/src/app/shared/date.util.ts b/src/app/shared/date.util.ts new file mode 100644 index 0000000000..90f9ff9b39 --- /dev/null +++ b/src/app/shared/date.util.ts @@ -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`; + +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html new file mode 100644 index 0000000000..de30440dc2 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html @@ -0,0 +1,442 @@ +
+ + + + + +
+
+ + + + +
+ +
+ + + + +
+
+ + + + + + +
+ + +
+ + +
+ +
+ + +
+ + +
+ +
+
+ + +
+ + +
+ + +
+ +
+ + + + + +
+ + + + +
+ + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {{ message }} +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + + + + +
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts new file mode 100644 index 0000000000..dc60697c1f --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts @@ -0,0 +1,176 @@ +import { + ChangeDetectorRef, + Component, + ContentChildren, + EventEmitter, + Input, + OnChanges, + Output, + QueryList, + SimpleChanges +} from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { + DynamicDatePickerModel, + DynamicFormArrayGroupModel, + DynamicFormControlComponent, + DynamicFormControlEvent, + DynamicFormControlModel, + DynamicFormLayout, + DynamicFormLayoutService, + DynamicFormValidationService, + DynamicTemplateDirective, + DYNAMIC_FORM_CONTROL_TYPE_ARRAY, + DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX, + DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX_GROUP, + DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER, + DYNAMIC_FORM_CONTROL_TYPE_GROUP, + DYNAMIC_FORM_CONTROL_TYPE_INPUT, + DYNAMIC_FORM_CONTROL_TYPE_RADIO_GROUP, + DYNAMIC_FORM_CONTROL_TYPE_SELECT, + DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA, + DYNAMIC_FORM_CONTROL_TYPE_TIMEPICKER, +} from '@ng-dynamic-forms/core'; +import { DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD } from './models/typeahead/dynamic-typeahead.model'; +import { DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; +import { DYNAMIC_FORM_CONTROL_TYPE_TAG } from './models/tag/dynamic-tag.model'; +import { DYNAMIC_FORM_CONTROL_TYPE_RELATION } from './models/ds-dynamic-group/dynamic-group.model'; +import { DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER } from './models/ds-date-picker/ds-date-picker.model'; +import { DYNAMIC_FORM_CONTROL_TYPE_LOOKUP } from './models/lookup/dynamic-lookup.model'; +import { DynamicListCheckboxGroupModel } from './models/list/dynamic-list-checkbox-group.model'; +import { DynamicListRadioGroupModel } from './models/list/dynamic-list-radio-group.model'; +import { isNotEmpty } from '../../../empty.util'; + +export const enum NGBootstrapFormControlType { + + Array = 1, // 'ARRAY', + Calendar = 2, // 'CALENDAR', + Checkbox = 3, // 'CHECKBOX', + CheckboxGroup = 4, // 'CHECKBOX_GROUP', + DatePicker = 5, // 'DATEPICKER', + Group = 6, // 'GROUP', + Input = 7, // 'INPUT', + RadioGroup = 8, // 'RADIO_GROUP', + Select = 9, // 'SELECT', + TextArea = 10, // 'TEXTAREA', + TimePicker = 11, // 'TIMEPICKER' + TypeAhead = 12, // 'TYPEAHEAD' + ScrollableDropdown = 13, // 'SCROLLABLE_DROPDOWN' + TypeTag = 14, // 'TYPETAG' + List = 15, // 'TYPELIST' + Relation = 16, // Dynamic Group + DsDatePicker = 17, // Ds Date Picker + Lookup = 18, // LOOKUP +} + +@Component({ + selector: 'ds-dynamic-form-control', + styleUrls: ['../../form.component.scss', './ds-dynamic-form.component.scss'], + templateUrl: './ds-dynamic-form-control.component.html' +}) +export class DsDynamicFormControlComponent extends DynamicFormControlComponent implements OnChanges { + + @ContentChildren(DynamicTemplateDirective) contentTemplateList: QueryList; + // tslint:disable-next-line:no-input-rename + @Input('templates') inputTemplateList: QueryList; + + @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 = new EventEmitter(); + @Output('dfChange') change: EventEmitter = new EventEmitter(); + @Output('dfFocus') focus: EventEmitter = new EventEmitter(); + /* tslint:enable:no-output-rename */ + + type: NGBootstrapFormControlType | null; + + static getFormControlType(model: DynamicFormControlModel): NGBootstrapFormControlType | null { + + switch (model.type) { + + case DYNAMIC_FORM_CONTROL_TYPE_ARRAY: + return NGBootstrapFormControlType.Array; + + case DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX: + return NGBootstrapFormControlType.Checkbox; + + case DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX_GROUP: + return (model instanceof DynamicListCheckboxGroupModel) ? NGBootstrapFormControlType.List : NGBootstrapFormControlType.CheckboxGroup; + + case DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER: + const datepickerModel = model as DynamicDatePickerModel; + + return datepickerModel.inline ? NGBootstrapFormControlType.Calendar : NGBootstrapFormControlType.DatePicker; + + case DYNAMIC_FORM_CONTROL_TYPE_GROUP: + // return (model instanceof DynamicGroupModel) ? NGBootstrapFormControlType.DynamicGroup : NGBootstrapFormControlType.Group; + return NGBootstrapFormControlType.Group; + + case DYNAMIC_FORM_CONTROL_TYPE_INPUT: + return NGBootstrapFormControlType.Input; + + case DYNAMIC_FORM_CONTROL_TYPE_RADIO_GROUP: + return (model instanceof DynamicListRadioGroupModel) ? NGBootstrapFormControlType.List : NGBootstrapFormControlType.RadioGroup; + + case DYNAMIC_FORM_CONTROL_TYPE_SELECT: + return NGBootstrapFormControlType.Select; + + case DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA: + return NGBootstrapFormControlType.TextArea; + + case DYNAMIC_FORM_CONTROL_TYPE_TIMEPICKER: + return NGBootstrapFormControlType.TimePicker; + + case DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD: + return NGBootstrapFormControlType.TypeAhead; + + case DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN: + return NGBootstrapFormControlType.ScrollableDropdown; + + case DYNAMIC_FORM_CONTROL_TYPE_TAG: + return NGBootstrapFormControlType.TypeTag; + + case DYNAMIC_FORM_CONTROL_TYPE_RELATION: + return NGBootstrapFormControlType.Relation; + + case DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER: + return NGBootstrapFormControlType.DsDatePicker; + + case DYNAMIC_FORM_CONTROL_TYPE_LOOKUP: + return NGBootstrapFormControlType.Lookup; + + default: + return null; + } + } + + constructor(protected changeDetectorRef: ChangeDetectorRef, protected layoutService: DynamicFormLayoutService, + protected validationService: DynamicFormValidationService) { + + super(changeDetectorRef, layoutService, validationService); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes) { + super.ngOnChanges(changes); + } + + if (changes.model) { + this.type = DsDynamicFormControlComponent.getFormControlType(this.model); + } + } + + onChangeLanguage(event) { + if (isNotEmpty((this.model as any).value)) { + this.onValueChange(event); + } + } + +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html new file mode 100644 index 0000000000..ea151726f4 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.html @@ -0,0 +1,12 @@ + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.scss new file mode 100644 index 0000000000..52facc2f2c --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.scss @@ -0,0 +1,5 @@ +:host { + display: flex; + flex-direction: column; + justify-content: center; +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.ts new file mode 100644 index 0000000000..7789d910a8 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.ts @@ -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 = new EventEmitter(); + @Output('dfChange') change: EventEmitter = new EventEmitter(); + @Output('dfFocus') focus: EventEmitter = new EventEmitter(); + /* tslint:enable:no-output-rename */ + + @ContentChildren(DynamicTemplateDirective) templates: QueryList; + + @ViewChildren(DsDynamicFormControlComponent) components: QueryList; + + constructor(protected formService: FormBuilderService, protected layoutService: DynamicFormLayoutService) { + super(formService, layoutService); + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component.html new file mode 100644 index 0000000000..81733ff999 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component.html @@ -0,0 +1,47 @@ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component.scss new file mode 100644 index 0000000000..9eab449eeb --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component.scss @@ -0,0 +1,3 @@ +.col-lg-1 { + width: auto; +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component.ts new file mode 100644 index 0000000000..6b661c65c9 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component.ts @@ -0,0 +1,170 @@ +import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { DynamicDsDatePickerModel } from './ds-date-picker.model'; + +export const DS_DATE_PICKER_SEPARATOR = '-'; + +@Component({ + selector: 'ds-date-picker', + styleUrls: ['./ds-date-picker.component.scss'], + templateUrl: './ds-date-picker.component.html', +}) + +export class DsDatePickerComponent implements OnInit { + @Input() bindId = true; + @Input() group: FormGroup; + @Input() model: DynamicDsDatePickerModel; + @Input() showErrorMessages = false; + // @Input() + // minDate; + // @Input() + // maxDate; + + @Output() + selected = new EventEmitter(); + @Output() + remove = new EventEmitter(); + @Output() + change = new EventEmitter(); + + initialYear: number; + initialMonth: number; + initialDay: number; + + year: number; + month: number; + day: number; + + minYear: 0; + maxYear: number; + minMonth = 1; + maxMonth = 12; + minDay = 1; + maxDay = 31; + + yearPlaceholder = 'year'; + monthPlaceholder = 'month'; + dayPlaceholder = 'day'; + + disabledMonth = true; + disabledDay = true; + invalid = false; + + ngOnInit() {// TODO Manage fields when not setted + const now = new Date(); + this.initialYear = now.getFullYear(); + this.initialMonth = now.getMonth() + 1; + this.initialDay = now.getDate(); + + if (this.model.value && this.model.value !== null) { + const values = this.model.value.toString().split(DS_DATE_PICKER_SEPARATOR); + if (values.length > 0) { + this.initialYear = parseInt(values[0], 10); + this.year = this.initialYear; + this.disabledMonth = false; + } + if (values.length > 1) { + this.initialMonth = parseInt(values[1], 10); + this.month = this.initialMonth; + this.disabledDay = false; + } + if (values.length > 2) { + this.initialDay = parseInt(values[2], 10); + this.day = this.initialDay; + } + } + + this.maxYear = this.initialYear + 100; + + // Invalid state for year + this.group.get(this.model.id).statusChanges.subscribe((state) => { + if (state === 'INVALID' || this.model.malformedDate) { + this.invalid = true; + } else { + this.invalid = false; + this.model.malformedDate = false; + } + }); + } + + onChange(event) { + // update year-month-day + switch (event.field) { + case 'year': { + if (event.value !== null) { + this.year = event.value; + } else { + this.year = undefined; + this.month = undefined; + this.day = undefined; + } + break; + } + case 'month': { + if (event.value !== null) { + this.month = event.value; + } else { + this.month = undefined; + this.day = undefined; + } + break; + } + case 'day': { + if (event.value !== null) { + this.day = event.value; + } else { + this.day = undefined; + } + break; + } + } + + // set max for days by month/year + if (!this.disabledDay) { + const month = this.month ? this.month - 1 : 0; + const date = new Date(this.year, month, 1); + this.maxDay = this.getLastDay(date); + if (this.day > this.maxDay) { + this.day = this.maxDay; + } + } + + // Manage disable + if (!this.model.value && event.field === 'year') { + this.disabledMonth = false; + } else if (this.disabledDay && event.field === 'month') { + this.disabledDay = false; + } + + // update value + let value = null; + if (this.year) { + let yyyy = this.year.toString(); + while (yyyy.length < 4) { + yyyy = '0' + yyyy; + } + value = yyyy; + } + if (this.month) { + const mm = this.month.toString().length === 1 + ? '0' + this.month.toString() + : this.month.toString(); + value += DS_DATE_PICKER_SEPARATOR + mm; + } + if (this.day) { + const dd = this.day.toString().length === 1 + ? '0' + this.day.toString() + : this.day.toString(); + value += DS_DATE_PICKER_SEPARATOR + dd; + } + this.model.valueUpdates.next(value); + this.change.emit(event); + } + + getLastDay(date: Date) { + // Last Day of the same month (+1 month, -1 day) + date.setMonth(date.getMonth() + 1, 0); + return date.getDate(); + } + +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.model.ts new file mode 100644 index 0000000000..5713d813b7 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.model.ts @@ -0,0 +1,21 @@ +import { DynamicDateControlModel, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core'; +import { DynamicDateControlModelConfig } from '@ng-dynamic-forms/core/src/model/dynamic-date-control.model'; +import { Subject } from 'rxjs/Subject'; + +export const DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER = 'DSDATEPICKER'; + +/** + * Dynamic Date Picker Model class + */ +export class DynamicDsDatePickerModel extends DynamicDateControlModel { + @serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER; + valueUpdates: Subject; + malformedDate: boolean; + hasLanguages = false; + + constructor(config: DynamicDateControlModelConfig, layout?: DynamicFormControlLayout) { + super(config, layout); + this.malformedDate = false; + } + +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-combobox.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-combobox.model.ts new file mode 100644 index 0000000000..b8306bd491 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-combobox.model.ts @@ -0,0 +1,64 @@ +import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicInputModelConfig, serializable } from '@ng-dynamic-forms/core'; +import { DsDynamicInputModel, DsDynamicInputModelConfig } from './ds-dynamic-input.model'; +import { Subject } from 'rxjs/Subject'; +import { DynamicFormGroupModelConfig } from '@ng-dynamic-forms/core/src/model/form-group/dynamic-form-group.model'; +import { LanguageCode } from '../../models/form-field-language-value.model'; + +export const COMBOBOX_GROUP_SUFFIX = '_COMBO_GROUP'; +export const COMBOBOX_METADATA_SUFFIX = '_COMBO_METADATA'; +export const COMBOBOX_VALUE_SUFFIX = '_COMBO_VALUE'; + +export interface DsDynamicComboboxModelConfig extends DynamicFormGroupModelConfig { + languageCodes: LanguageCode[]; + language: string; + readOnly: boolean; +} + +export class DynamicComboboxModel extends DynamicFormGroupModel { + @serializable() private _language: string; + @serializable() private _languageCodes: LanguageCode[]; + @serializable() languageUpdates: Subject; + @serializable() hasLanguages = false; + @serializable() readOnly: boolean; + + constructor(config: DsDynamicComboboxModelConfig, layout?: DynamicFormControlLayout) { + super(config, layout); + + this.readOnly = config.readOnly; + this.language = config.language; + this.languageCodes = config.languageCodes; + + this.languageUpdates = new Subject(); + 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; + } + } + +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts new file mode 100644 index 0000000000..aee2e65f3a --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts @@ -0,0 +1,50 @@ +import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicFormGroupModelConfig, serializable } from '@ng-dynamic-forms/core'; +import { isNotEmpty } from '../../../../empty.util'; +import { DsDynamicInputModel } from './ds-dynamic-input.model'; +import { AuthorityValueModel } from '../../../../../core/integration/models/authority-value.model'; + +export const CONCAT_GROUP_SUFFIX = '_CONCAT_GROUP'; +export const CONCAT_FIRST_INPUT_SUFFIX = '_CONCAT_FIRST_INPUT'; +export const CONCAT_SECOND_INPUT_SUFFIX = '_CONCAT_SECOND_INPUT'; + +export interface DynamicConcatModelConfig extends DynamicFormGroupModelConfig { + separator: string; +} + +export class DynamicConcatModel extends DynamicFormGroupModel { + + @serializable() separator: string; + @serializable() hasLanguages = false; + + constructor(config: DynamicConcatModelConfig, layout?: DynamicFormControlLayout) { + + super(config, layout); + + this.separator = config.separator + ' '; + } + + get value() { + const firstValue = (this.get(0) as DsDynamicInputModel).value; + const secondValue = (this.get(1) as DsDynamicInputModel).value; + if (isNotEmpty(firstValue) && isNotEmpty(secondValue)) { + return firstValue + this.separator + secondValue; + } else { + return null + } + } + + set value(value: string | AuthorityValueModel) { + let values; + if (typeof value === 'string') { + values = value ? value.split(this.separator) : [null, null]; + } else { + values = value ? value.value.split(this.separator) : [null, null]; + } + + if (values.length > 1) { + (this.get(0) as DsDynamicInputModel).valueUpdates.next(values[0]); + (this.get(1) as DsDynamicInputModel).valueUpdates.next(values[1]); + } + } + +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.component.html new file mode 100644 index 0000000000..916026a85b --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.component.html @@ -0,0 +1,65 @@ + + + + + + + +
+
+ + + +
+ + + + + +
+
+
+ +
+
+ +
+ +
+
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.component.scss new file mode 100644 index 0000000000..d380554157 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.component.scss @@ -0,0 +1,3 @@ +.close { + top: -2.5rem; +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.components.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.components.ts new file mode 100644 index 0000000000..f74de35110 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.components.ts @@ -0,0 +1,224 @@ +import { + ChangeDetectorRef, + Component, + EventEmitter, + Inject, + Input, + OnDestroy, + OnInit, + Output, + ViewChild +} from '@angular/core'; + +import { Observable } from 'rxjs/Observable'; +import { DynamicFormControlModel, DynamicFormGroupModel, DynamicInputModel } from '@ng-dynamic-forms/core'; +import { isEqual } from 'lodash'; + +import { DynamicGroupModel, PLACEHOLDER_PARENT_METADATA } from './dynamic-group.model'; +import { FormBuilderService } from '../../../form-builder.service'; +import { SubmissionFormsModel } from '../../../../../../core/shared/config/config-submission-forms.model'; +import { FormService } from '../../../../form.service'; +import { FormComponent } from '../../../../form.component'; +import { Chips } from '../../../../../chips/models/chips.model'; +import { DynamicLookupModel } from '../lookup/dynamic-lookup.model'; +import { hasValue, isEmpty, isNotEmpty } from '../../../../../empty.util'; +import { shrinkInOut } from '../../../../../animations/shrink'; +import { ChipsItem } from '../../../../../chips/models/chips-item.model'; +import { GlobalConfig } from '../../../../../../../config/global-config.interface'; +import { GLOBAL_CONFIG } from '../../../../../../../config'; +import { FormGroup } from '@angular/forms'; +import { Subscription } from 'rxjs/Subscription'; + +@Component({ + selector: 'ds-dynamic-group', + styleUrls: ['./dynamic-group.component.scss'], + templateUrl: './dynamic-group.component.html', + animations: [shrinkInOut] +}) +export class DsDynamicGroupComponent implements OnDestroy, OnInit { + + @Input() formId: string; + @Input() group: FormGroup; + @Input() model: DynamicGroupModel; + + @Output() blur: EventEmitter = new EventEmitter(); + @Output() change: EventEmitter = new EventEmitter(); + @Output() focus: EventEmitter = new EventEmitter(); + + public chips: Chips; + public formCollapsed = Observable.of(false); + public formModel: DynamicFormControlModel[]; + public editMode = false; + public invalid = false; + + private selectedChipItem: ChipsItem; + private subs: Subscription[] = []; + + @ViewChild('formRef') private formRef: FormComponent; + + constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + private formBuilderService: FormBuilderService, + private formService: FormService, + private cdr: ChangeDetectorRef) { + } + + ngOnInit() { + console.log(this.model.hasErrorMessages); + const config = {rows: this.model.formConfiguration} as SubmissionFormsModel; + if (isNotEmpty(this.model.value)) { + this.formCollapsed = Observable.of(true); + } + this.formId = this.formService.getUniqueId(this.model.id); + this.formModel = this.formBuilderService.modelFromConfiguration(config, this.model.scopeUUID, {}); + this.chips = new Chips(this.model.value, 'value', this.model.mandatoryField); + this.subs.push( + this.chips.chipsItems + .subscribe((subItems: any[]) => { + const items = this.chips.getChipsItems(); + // Does not emit change if model value is equal to the current value + if (!isEqual(items, this.model.value)) { + if (isEmpty(items)) { + // If items is empty, last element has been removed + // so emit an empty value that allows to dispatch + // a remove JSON PATCH operation + const emptyItem = Object.create({}); + Object.keys(this.model.value[0]) + .forEach((key) => { + emptyItem[key] = null; + }); + items.push(emptyItem); + } + + this.model.valueUpdates.next(items); + this.change.emit(); + } + }), + // Invalid state for year + this.group.get(this.model.id).statusChanges.subscribe((state) => { + if (state === 'INVALID') { + this.invalid = true; + } else { + this.invalid = false; + } + }) + ) + } + + isMandatoryFieldEmpty() { + // formModel[0].group[0].value == null + let res = true; + this.formModel.forEach((row) => { + const modelRow = row as DynamicFormGroupModel; + modelRow.group.forEach((model: DynamicInputModel) => { + if (model.name === this.model.mandatoryField) { + res = model.value == null; + return; + } + }); + }); + return res; + } + + onChipSelected(event) { + this.expandForm(); + this.selectedChipItem = this.chips.getChipByIndex(event); + this.formModel.forEach((row) => { + const modelRow = row as DynamicFormGroupModel; + modelRow.group.forEach((model: DynamicInputModel) => { + const value = this.selectedChipItem.item[model.name] === PLACEHOLDER_PARENT_METADATA ? null : this.selectedChipItem.item[model.name]; + if (model instanceof DynamicLookupModel) { + (model as DynamicLookupModel).valueUpdates.next(value); + } else if (model instanceof DynamicInputModel) { + model.valueUpdates.next(value); + } else { + (model as any).value = value; + } + }); + }); + + this.editMode = true; + } + + collapseForm() { + this.formCollapsed = Observable.of(true); + this.clear(); + } + + expandForm() { + this.formCollapsed = Observable.of(false); + } + + clear() { + if (this.editMode) { + this.selectedChipItem.editMode = false; + this.selectedChipItem = null; + this.editMode = false; + } + this.resetForm(); + // this.change.emit(event); + } + + save() { + if (this.editMode) { + this.modifyChip(); + } else { + this.addToChips(); + } + } + + delete() { + this.chips.remove(this.selectedChipItem); + this.clear(); + } + + private addToChips() { + if (!this.formRef.formGroup.valid) { + this.formService.validateAllFormFields(this.formRef.formGroup); + return; + } + + // Item to add + if (!this.isMandatoryFieldEmpty()) { + const item = this.buildChipItem(); + this.chips.add(item); + + this.resetForm(); + } + } + + private modifyChip() { + if (!this.formRef.formGroup.valid) { + this.formService.validateAllFormFields(this.formRef.formGroup); + return; + } + + if (!this.isMandatoryFieldEmpty()) { + const item = this.buildChipItem(); + this.chips.update(this.selectedChipItem.id, item); + this.resetForm(); + this.cdr.detectChanges(); + } + } + + private buildChipItem() { + const item = Object.create({}); + this.formModel.forEach((row) => { + const modelRow = row as DynamicFormGroupModel; + modelRow.group.forEach((control: DynamicInputModel) => { + item[control.name] = control.value || PLACEHOLDER_PARENT_METADATA; + }); + }); + return item; + } + + private resetForm() { + this.formService.resetForm(this.formRef.formGroup, this.formModel, this.formId); + } + + ngOnDestroy(): void { + this.subs + .filter((sub) => hasValue(sub)) + .forEach((sub) => sub.unsubscribe()); + } + +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.model.ts new file mode 100644 index 0000000000..4ec029661b --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.model.ts @@ -0,0 +1,41 @@ +import { DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core'; +import { FormRowModel } from '../../../../../../core/shared/config/config-submission-forms.model'; +import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model'; + +export const DYNAMIC_FORM_CONTROL_TYPE_RELATION = 'RELATION'; +export const PLACEHOLDER_PARENT_METADATA = '#PLACEHOLDER_PARENT_METADATA_VALUE#'; + +/** + * Dynamic Group Model configuration interface + */ +export interface DynamicGroupModelConfig extends DsDynamicInputModelConfig { + formConfiguration: FormRowModel[], + mandatoryField: string, + name: string, + relationFields: string[], + scopeUUID: string, + value?: any; +} + +/** + * Dynamic Group Model class + */ +export class DynamicGroupModel extends DsDynamicInputModel { + @serializable() formConfiguration: FormRowModel[]; + @serializable() mandatoryField: string; + @serializable() relationFields: string[]; + @serializable() scopeUUID: string; + @serializable() value: any[]; + @serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_RELATION; + + constructor(config: DynamicGroupModelConfig, layout?: DynamicFormControlLayout) { + super(config, layout); + + this.formConfiguration = config.formConfiguration; + this.mandatoryField = config.mandatoryField; + this.relationFields = config.relationFields; + this.scopeUUID = config.scopeUUID; + const value = config.value || []; + this.valueUpdates.next(value) + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts new file mode 100644 index 0000000000..d8869f2829 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts @@ -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; + + 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(); + 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; + } + } + +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts new file mode 100644 index 0000000000..0a056b7d32 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts @@ -0,0 +1,18 @@ +import { + DynamicFormArrayModel, DynamicFormArrayModelConfig, DynamicFormControlLayout, + serializable +} from '@ng-dynamic-forms/core'; + +export interface DynamicRowArrayModelConfig extends DynamicFormArrayModelConfig { + notRepeteable: boolean; +} + +export class DynamicRowArrayModel extends DynamicFormArrayModel { + @serializable() notRepeteable = false; + + constructor(config: DynamicRowArrayModelConfig, layout?: DynamicFormControlLayout) { + super(config, layout); + this.notRepeteable = config.notRepeteable; + } + +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-group-model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-group-model.ts new file mode 100644 index 0000000000..3e163f1f13 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-group-model.ts @@ -0,0 +1,5 @@ +import { DynamicFormGroupModel } from '@ng-dynamic-forms/core'; + +export class DynamicRowGroupModel extends DynamicFormGroupModel { + +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model.ts new file mode 100644 index 0000000000..9aead0447f --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model.ts @@ -0,0 +1,30 @@ +import { + DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA, + DynamicFormControlLayout, DynamicTextAreaModel, DynamicTextAreaModelConfig, + serializable +} from '@ng-dynamic-forms/core'; +import { Subject } from 'rxjs/Subject'; +import { LanguageCode } from '../../models/form-field-language-value.model'; +import { DsDynamicInputModel, DsDynamicInputModelConfig } from './ds-dynamic-input.model'; + +export interface DsDynamicTextAreaModelConfig extends DsDynamicInputModelConfig { + cols?: number; + rows?: number; + wrap?: string; +} + +export class DsDynamicTextAreaModel extends DsDynamicInputModel { + @serializable() cols: number; + @serializable() rows: number; + @serializable() wrap: string; + @serializable() type = DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA; + + constructor(config: DsDynamicTextAreaModelConfig, layout?: DynamicFormControlLayout) { + super(config, layout); + + this.cols = config.cols; + this.rows = config.rows; + this.wrap = config.wrap; + } + +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts new file mode 100644 index 0000000000..775da3bd33 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts @@ -0,0 +1,59 @@ +import { Subject } from 'rxjs/Subject'; + +import { + DynamicCheckboxGroupModel, DynamicFormControlLayout, + DynamicFormGroupModelConfig, + serializable +} from '@ng-dynamic-forms/core'; +import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model'; +import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model'; +import { hasValue } from '../../../../../empty.util'; + +export interface DynamicListCheckboxGroupModelConfig extends DynamicFormGroupModelConfig { + authorityOptions: AuthorityOptions; + groupLength: number; + repeatable: boolean; + value?: any; +} + +export class DynamicListCheckboxGroupModel extends DynamicCheckboxGroupModel { + + @serializable() authorityOptions: AuthorityOptions; + @serializable() repeatable: boolean; + @serializable() groupLength: number; + @serializable() _value: AuthorityValueModel[]; + valueUpdates: Subject; + + 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(); + 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 + } + } + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model.ts new file mode 100644 index 0000000000..a6bba88fc6 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model.ts @@ -0,0 +1,35 @@ +import { + DynamicFormControlLayout, + DynamicRadioGroupModel, + DynamicRadioGroupModelConfig, + serializable +} from '@ng-dynamic-forms/core'; +import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model'; +import { hasValue } from '../../../../../empty.util'; + +export interface DynamicListModelConfig extends DynamicRadioGroupModelConfig { + authorityOptions: AuthorityOptions; + groupLength: number; + repeatable: boolean; + value?: any; +} + +export class DynamicListRadioGroupModel extends DynamicRadioGroupModel { + + @serializable() authorityOptions: AuthorityOptions; + @serializable() repeatable: boolean; + @serializable() groupLength: number; + + constructor(config: DynamicListModelConfig, layout?: DynamicFormControlLayout) { + super(config, layout); + + this.authorityOptions = config.authorityOptions; + this.groupLength = config.groupLength || 5; + this.repeatable = config.repeatable; + this.valueUpdates.next(config.value); + } + + get hasAuthority(): boolean { + return this.authorityOptions && hasValue(this.authorityOptions.name); + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.html new file mode 100644 index 0000000000..97cfa39b2b --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.html @@ -0,0 +1,66 @@ +
+
+ +
+ +
+ + + +
+
+
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.scss new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.scss @@ -0,0 +1 @@ + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts new file mode 100644 index 0000000000..a3baa3de88 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts @@ -0,0 +1,137 @@ +import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { findKey } from 'lodash'; + +import { AuthorityService } from '../../../../../../core/integration/authority.service'; +import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model'; +import { hasValue, isNotEmpty } from '../../../../../empty.util'; +import { DynamicListCheckboxGroupModel } from './dynamic-list-checkbox-group.model'; +import { ConfigData } from '../../../../../../core/config/config-data'; +import { ConfigAuthorityModel } from '../../../../../../core/shared/config/config-authority.model'; +import { FormBuilderService } from '../../../form-builder.service'; +import { DynamicCheckboxModel } from '@ng-dynamic-forms/core'; +import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model'; +import { DynamicListRadioGroupModel } from './dynamic-list-radio-group.model'; + +export interface ListItem { + id: string, + label: string, + value: boolean, + index: number +} + +@Component({ + selector: 'ds-dynamic-list', + styleUrls: ['./dynamic-list.component.scss'], + templateUrl: './dynamic-list.component.html' +}) + +// TODO Fare questo componente da zero +export class DsDynamicListComponent implements OnInit { + @Input() bindId = true; + @Input() group: FormGroup; + @Input() model: DynamicListCheckboxGroupModel | DynamicListRadioGroupModel; + @Input() showErrorMessages = false; + + @Output() blur: EventEmitter = new EventEmitter(); + @Output() change: EventEmitter = new EventEmitter(); + @Output() focus: EventEmitter = new EventEmitter(); + + public items: ListItem[][] = []; + protected authorityList: AuthorityValueModel[]; + protected searchOptions: IntegrationSearchOptions; + + constructor(private authorityService: AuthorityService, + private cdr: ChangeDetectorRef, + private formBuilderService: FormBuilderService) { + } + + ngOnInit() { + if (this.hasAuthorityOptions()) { + // TODO Replace max elements 1000 with a paginated request when pagination bug is resolved + this.searchOptions = new IntegrationSearchOptions( + this.model.authorityOptions.scope, + this.model.authorityOptions.name, + this.model.authorityOptions.metadata, + '', + 1000, // Max elements + 1);// Current Page + this.setOptionsFromAuthority(); + } + } + + onBlurEvent(event: Event) { + this.blur.emit(event); + } + + onFocusEvent(event: Event) { + this.focus.emit(event); + } + + onChangeEvent(event: Event) { + const target = event.target as any; + if (this.model.repeatable) { + // Target tabindex coincide with the array index of the value into the authority list + const authorityValue: AuthorityValueModel = this.authorityList[target.tabIndex]; + if (target.checked) { + this.model.valueUpdates.next(authorityValue); + } else { + const newValue = []; + this.model.value + .filter((item) => item.value !== authorityValue.value) + .forEach((item) => newValue.push(item)); + this.model.valueUpdates.next(newValue); + } + } else { + (this.model as DynamicListRadioGroupModel).valueUpdates.next(this.authorityList[target.value]); + } + this.change.emit(event); + } + + protected setOptionsFromAuthority() { + if (this.model.authorityOptions.name && this.model.authorityOptions.name.length > 0) { + const listGroup = this.group.controls[this.model.id] as FormGroup; + this.authorityService.getEntriesByName(this.searchOptions).subscribe((authorities: ConfigData) => { + let groupCounter = 0; + let itemsPerGroup = 0; + let tempList: ListItem[] = []; + this.authorityList = authorities.payload as ConfigAuthorityModel[]; + // Make a list of available options (checkbox/radio) and split in groups of 'model.groupLength' + (authorities.payload as ConfigAuthorityModel[]).forEach((option, key) => { + const value = option.id || option.value; + const checked: boolean = isNotEmpty(findKey( + this.model.value, + {value: option.value})); + + const item: ListItem = { + id: value, + label: option.display, + value: checked, + index: key + }; + if (this.model.repeatable) { + this.formBuilderService.addFormGroupControl(listGroup, (this.model as DynamicListCheckboxGroupModel), new DynamicCheckboxModel(item)); + } else { + (this.model as DynamicListRadioGroupModel).options.push({label: item.label, value: option}); + } + tempList.push(item); + itemsPerGroup++; + this.items[groupCounter] = tempList; + if (itemsPerGroup === this.model.groupLength) { + groupCounter++; + itemsPerGroup = 0; + tempList = []; + } + }); + this.cdr.detectChanges(); + }); + + } + } + + protected hasAuthorityOptions() { + return (hasValue(this.model.authorityOptions.scope) + && hasValue(this.model.authorityOptions.name) + && hasValue(this.model.authorityOptions.metadata)); + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html new file mode 100644 index 0000000000..972538efad --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html @@ -0,0 +1,130 @@ +
+ + +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+
+ +
+ +
+ +
+
+
+
+ + +
+
+ + + +
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.scss new file mode 100644 index 0000000000..a333fc881f --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.scss @@ -0,0 +1,68 @@ +@import "../../../../../../../styles/variables"; + +/* enable absolute positioning */ +.spinner-addon { + position: relative; +} + +/* style fa-spin */ +.spinner-addon .fa-spin { + color: map-get($theme-colors, primary); + position: absolute; + margin-top: 3px; + padding: 0; + pointer-events: none; +} + +/* align fa-spin */ +.left-addon .fa-spin { + left: 0px; +} + +.right-addon .fa-spin { + right: 0px; +} + +/* add padding */ +.left-addon input { + padding-left: 30px; +} + +.right-addon input { + padding-right: 30px; +} + +:host /deep/ .dropdown-menu { + width: 100% !important; + max-height: 200px; + overflow-y: auto !important; + overflow-x: hidden; +} + +:host /deep/ .dropdown-item.active, +:host /deep/ .dropdown-item:active, +:host /deep/ .dropdown-item:focus, +:host /deep/ .dropdown-item:hover { + color: $dropdown-link-hover-color !important; + background-color: $dropdown-link-hover-bg !important; +} + +.dropdown-menu { + margin-top: -10px; +} + +div { + overflow: visible; + //padding-right: 0 !important; +} + +button { + margin-right: 10px; +} + +//@media (min-width: $screen-sm-min) { +// .firstName { +// padding-left: 0 !important; +// margin-left: -5px !important; +// } +//} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts new file mode 100644 index 0000000000..1ecf384748 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts @@ -0,0 +1,220 @@ +import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +import { AuthorityService } from '../../../../../../core/integration/authority.service'; +import { DynamicLookupModel } from './dynamic-lookup.model'; +import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model'; +import { hasValue, isEmpty, isNotEmpty, isNull, isUndefined } from '../../../../../empty.util'; +import { IntegrationData } from '../../../../../../core/integration/integration-data'; +import { PageInfo } from '../../../../../../core/shared/page-info.model'; +import { Subscription } from 'rxjs/Subscription'; +import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; +import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model'; + +@Component({ + selector: 'ds-dynamic-lookup', + styleUrls: ['./dynamic-lookup.component.scss'], + templateUrl: './dynamic-lookup.component.html' +}) +export class DsDynamicLookupComponent implements OnDestroy, OnInit { + @Input() bindId = true; + @Input() group: FormGroup; + @Input() model: DynamicLookupModel; + @Input() showErrorMessages = false; + + @Output() blur: EventEmitter = new EventEmitter(); + @Output() change: EventEmitter = new EventEmitter(); + @Output() focus: EventEmitter = new EventEmitter(); + + public firstInputValue = ''; + public secondInputValue = ''; + public loading = false; + public pageInfo: PageInfo; + public optionsList: any; + public isLookupName: boolean; + public name2: string; + + protected searchOptions: IntegrationSearchOptions; + protected sub: Subscription; + + constructor(private authorityService: AuthorityService, + private cdr: ChangeDetectorRef) { + } + + ngOnInit() { + this.searchOptions = new IntegrationSearchOptions( + this.model.authorityOptions.scope, + this.model.authorityOptions.name, + this.model.authorityOptions.metadata, + '', + this.model.maxOptions, + 1); + + // Switch Lookup/LookupName + if (this.model.separator) { + this.isLookupName = true; + this.name2 = this.model.name + '2'; + } + + this.setInputsValue(this.model.value); + + this.model.valueUpdates + .subscribe((value) => { + if (isEmpty(value)) { + this.resetFields(); + } else { + this.setInputsValue(this.model.value); + } + }); + } + + public formatItemForInput(item: any, field: number): string { + if (isUndefined(item) || isNull(item)) { + return ''; + } + return (typeof item === 'string') ? item : this.inputFormatter(item, field); + } + + // inputFormatter = (x: { display: string }) => x.display; + inputFormatter = (x: { display: string }, y: number) => { + // this.splitValues(); + return y === 1 ? this.firstInputValue : this.secondInputValue; + }; + + onInput(event) { + if (!this.model.authorityOptions.closed) { + if (isNotEmpty(this.getCurrentValue())) { + const currentValue = new FormFieldMetadataValueObject(this.getCurrentValue()); + this.onSelect(currentValue); + } else { + this.remove(); + } + } + } + + onScroll() { + if (!this.loading && this.pageInfo.currentPage <= this.pageInfo.totalPages) { + this.searchOptions.currentPage++; + this.search(); + } + } + + protected setInputsValue(value) { + if (hasValue(value)) { + let displayValue = value; + if (value instanceof FormFieldMetadataValueObject || value instanceof AuthorityValueModel) { + displayValue = value.display; + } + + if (hasValue(displayValue)) { + if (this.isLookupName) { + const values = displayValue.split(this.model.separator); + + this.firstInputValue = (values[0] || '').trim(); + this.secondInputValue = (values[1] || '').trim(); + } else { + this.firstInputValue = displayValue || ''; + } + } + } + } + + protected getCurrentValue(): string { + let result = ''; + if (!this.isLookupName) { + result = this.firstInputValue; + } else { + if (isNotEmpty(this.firstInputValue)) { + result = this.firstInputValue; + } + if (isNotEmpty(this.secondInputValue)) { + result = isEmpty(result) + ? this.secondInputValue + : this.firstInputValue + this.model.separator + ' ' + this.secondInputValue; + } + } + return result; + } + + search() { + this.optionsList = null; + this.pageInfo = null; + + // Query + this.searchOptions.query = this.getCurrentValue(); + + this.loading = true; + this.authorityService.getEntriesByName(this.searchOptions) + .distinctUntilChanged() + .subscribe((object: IntegrationData) => { + this.optionsList = object.payload; + this.pageInfo = object.pageInfo; + this.loading = false; + this.cdr.detectChanges(); + }); + } + + clearFields() { + // Clear inputs whether there is no results and authority is closed + if (this.model.authorityOptions.closed) { + this.resetFields(); + } + } + + protected resetFields() { + this.firstInputValue = ''; + if (this.isLookupName) { + this.secondInputValue = ''; + } + } + + onSelect(event) { + this.group.markAsDirty(); + this.model.valueUpdates.next(event); + this.setInputsValue(event); + this.change.emit(event); + this.optionsList = null; + this.pageInfo = null; + } + + isInputDisabled() { + return this.model.authorityOptions.closed && hasValue(this.model.value); + } + + isSearchDisabled() { + // if (this.firstInputValue === '' + // && (this.isLookupName ? this.secondInputValue === '' : true)) { + // return true; + // } + // return false; + return isEmpty(this.firstInputValue); + } + + remove() { + this.group.markAsPristine(); + this.model.valueUpdates.next(null); + this.change.emit(null); + } + + openChange(isOpened: boolean) { + if (!isOpened) { + if (this.model.authorityOptions.closed) { + this.setInputsValue(''); + } + } + } + + onBlurEvent(event: Event) { + this.blur.emit(event); + } + + onFocusEvent(event) { + this.focus.emit(event); + } + + ngOnDestroy() { + if (hasValue(this.sub)) { + this.sub.unsubscribe(); + } + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.model.ts new file mode 100644 index 0000000000..21b11d0bf8 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.model.ts @@ -0,0 +1,37 @@ +import { AUTOCOMPLETE_OFF, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core'; +import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model'; +import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model'; +import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model'; + +export const DYNAMIC_FORM_CONTROL_TYPE_LOOKUP = 'LOOKUP'; + +export interface DynamicLookupModelConfig extends DsDynamicInputModelConfig { + maxOptions: number; + value: any; + separator: string; +} + +export class DynamicLookupModel extends DsDynamicInputModel { + + @serializable() maxOptions: number; + @serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_LOOKUP; + @serializable() value: any; + @serializable() separator: string; // Defined only for lookup-name + + @serializable() placeholder: string; + @serializable() placeholder2: string; + + constructor(config: DynamicLookupModelConfig, layout?: DynamicFormControlLayout) { + + super(config, layout); + + this.autoComplete = AUTOCOMPLETE_OFF; + this.maxOptions = config.maxOptions; + this.separator = config.separator; // Defined only for lookup-name + + this.valueUpdates.next(config.value); + // this.valueUpdates.subscribe(() => { + // this.setInputsValue(); + // }); + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html new file mode 100644 index 0000000000..1ea1b62cd0 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html @@ -0,0 +1,43 @@ +
+ + + + +
+ + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.scss new file mode 100644 index 0000000000..5b278312d3 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.scss @@ -0,0 +1,26 @@ +@import '../../../../form.component'; + +.scrollable-menu { + height: auto; + max-height: 200px; + overflow-x: hidden; +} + +.collection-item { + border-bottom: $dropdown-border-width solid $dropdown-border-color; +} + +.scrollable-dropdown-loading { + background-color: map-get($theme-colors, primary); + color: white; + height: 2rem !important; + line-height: 2rem; + position: sticky; + bottom: 0; +} + +.scrollable-dropdown-menu { + left: 0 !important; + margin-bottom: 1rem; + z-index: 1000; +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts new file mode 100644 index 0000000000..91aad741b6 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts @@ -0,0 +1,94 @@ +import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +import { DynamicScrollableDropdownModel } from './dynamic-scrollable-dropdown.model'; +import { PageInfo } from '../../../../../../core/shared/page-info.model'; +import { isNull, isUndefined } from '../../../../../empty.util'; +import { AuthorityService } from '../../../../../../core/integration/authority.service'; +import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model'; +import { IntegrationData } from '../../../../../../core/integration/integration-data'; +import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model'; +import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ds-dynamic-scrollable-dropdown', + styleUrls: ['./dynamic-scrollable-dropdown.component.scss'], + templateUrl: './dynamic-scrollable-dropdown.component.html' +}) +export class DsDynamicScrollableDropdownComponent implements OnInit { + @Input() bindId = true; + @Input() group: FormGroup; + @Input() model: DynamicScrollableDropdownModel; + @Input() showErrorMessages = false; + + @Output() blur: EventEmitter = new EventEmitter(); + @Output() change: EventEmitter = new EventEmitter(); + @Output() focus: EventEmitter = new EventEmitter(); + + public loading = false; + public pageInfo: PageInfo; + public optionsList: any; + + protected searchOptions: IntegrationSearchOptions; + + constructor(private authorityService: AuthorityService, private cdr: ChangeDetectorRef) {} + + ngOnInit() { + this.searchOptions = new IntegrationSearchOptions( + this.model.authorityOptions.scope, + this.model.authorityOptions.name, + this.model.authorityOptions.metadata, + '', + this.model.maxOptions, + 1); + this.authorityService.getEntriesByName(this.searchOptions) + .subscribe((object: IntegrationData) => { + this.optionsList = object.payload; + this.pageInfo = object.pageInfo; + this.cdr.detectChanges(); + }) + } + + public formatItemForInput(item: any): string { + if (isUndefined(item) || isNull(item)) { return '' } + return (typeof item === 'string') ? item : this.inputFormatter(item); + } + + inputFormatter = (x: AuthorityValueModel) => x.display || x.value; + + openDropdown(sdRef: NgbDropdown) { + if (!this.model.readOnly) { + sdRef.open(); + } + } + + onScroll() { + if (!this.loading && this.pageInfo.currentPage <= this.pageInfo.totalPages) { + this.loading = true; + this.searchOptions.currentPage++; + this.authorityService.getEntriesByName(this.searchOptions) + .do(() => this.loading = false) + .subscribe((object: IntegrationData) => { + this.optionsList = this.optionsList.concat(object.payload); + this.pageInfo = object.pageInfo; + this.cdr.detectChanges(); + }) + } + } + + onBlurEvent(event: Event) { + this.blur.emit(event); + } + + onFocusEvent(event) { + this.focus.emit(event); + } + + onSelect(event) { + this.group.markAsDirty(); + // (this.model as DynamicScrollableDropdownModel).parent as + // this.group.get(this.model.id).setValue(event); + this.model.valueUpdates.next(event) + this.change.emit(event); + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model.ts new file mode 100644 index 0000000000..69cdb475e2 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model.ts @@ -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; + } + +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.html new file mode 100644 index 0000000000..5c7a2b9713 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.html @@ -0,0 +1,46 @@ + + {{ r.display }} + + + + + + + + + + + + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.scss new file mode 100644 index 0000000000..15cc0dc5da --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.scss @@ -0,0 +1,34 @@ +@import "../../../../../../../styles/variables"; + +/* style fa-spin */ +.fa-spin { + pointer-events: none; + right: 0; +} +.chips-left { + left: 0; + padding-right: 100%; +} + +:host /deep/ .dropdown-menu { + width: 100% !important; + max-height: 200px; + overflow-y: scroll; + overflow-x: hidden; + left: 0 !important; + margin-top: 15px !important; +} + +:host /deep/ .dropdown-item.active, +:host /deep/ .dropdown-item:active, +:host /deep/ .dropdown-item:focus, +:host /deep/ .dropdown-item:hover { + color: $dropdown-link-hover-color !important; + background-color: $dropdown-link-hover-bg !important; +} + +.tag-input { + flex-grow: 1; + outline: none; + width: auto !important; +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts new file mode 100644 index 0000000000..e2b951d4e9 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts @@ -0,0 +1,194 @@ +import { ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +import { Observable } from 'rxjs/Observable'; +import { NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap'; + +import { AuthorityService } from '../../../../../../core/integration/authority.service'; +import { DynamicTagModel } from './dynamic-tag.model'; +import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model'; +import { Chips } from '../../../../../chips/models/chips.model'; +import { hasValue, isEmpty, isNotEmpty } from '../../../../../empty.util'; +import { isEqual } from 'lodash'; +import { GlobalConfig } from '../../../../../../../config/global-config.interface'; +import { GLOBAL_CONFIG } from '../../../../../../../config'; + +@Component({ + selector: 'ds-dynamic-tag', + styleUrls: ['./dynamic-tag.component.scss'], + templateUrl: './dynamic-tag.component.html' +}) +export class DsDynamicTagComponent implements OnInit { + @Input() bindId = true; + @Input() group: FormGroup; + @Input() model: DynamicTagModel; + @Input() showErrorMessages = false; + + @Output() blur: EventEmitter = new EventEmitter(); + @Output() change: EventEmitter = new EventEmitter(); + @Output() focus: EventEmitter = new EventEmitter(); + + 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) => + text$ + .debounceTime(300) + .distinctUntilChanged() + .do(() => this.changeSearchingStatus(true)) + .switchMap((term) => { + if (term === '' || term.length < this.model.minChars) { + return Observable.of({list: []}); + } else { + this.searchOptions.query = term; + return this.authorityService.getEntriesByName(this.searchOptions) + .map((authorities) => { + // @TODO Pagination for authority is not working, to refactor when it will be fixed + return { + list: authorities.payload, + pageInfo: authorities.pageInfo + }; + }) + .do(() => this.searchFailed = false) + .catch(() => { + this.searchFailed = true; + return Observable.of({list: []}); + }); + } + }) + .map((results) => results.list) + .do(() => this.changeSearchingStatus(false)) + .merge(this.hideSearchingWhenUnsubscribed); + + constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + private authorityService: AuthorityService, + private cdr: ChangeDetectorRef) { + } + + ngOnInit() { + this.hasAuthority = this.model.authorityOptions && hasValue(this.model.authorityOptions.name); + if (this.hasAuthority) { + this.searchOptions = new IntegrationSearchOptions( + this.model.authorityOptions.scope, + this.model.authorityOptions.name, + this.model.authorityOptions.metadata); + } + + this.chips = new Chips(this.model.value, 'display'); + + this.chips.chipsItems + .subscribe((subItems: any[]) => { + const items = this.chips.getChipsItems(); + // Does not emit change if model value is equal to the current value + if (!isEqual(items, this.model.value)) { + this.model.valueUpdates.next(items); + this.change.emit(event); + } + }) + } + + changeSearchingStatus(status: boolean) { + this.searching = status; + this.cdr.detectChanges(); + } + + onInput(event) { + if (event.data) { + this.group.markAsDirty(); + } + this.cdr.detectChanges(); + } + + onBlurEvent(event: Event) { + if (isNotEmpty(this.currentValue)) { + this.addTagsToChips(); + } + this.blur.emit(event); + } + + onFocusEvent($event) { + this.focus.emit(event); + } + + onSelectItem(event: NgbTypeaheadSelectItemEvent) { + this.chips.add(event.item); + // this.group.controls[this.model.id].setValue(this.model.value); + this.updateModel(event); + + setTimeout(() => { + // Reset the input text after x ms, mandatory or the formatter overwrite it + this.currentValue = null; + this.cdr.detectChanges(); + }, 50); + } + + updateModel(event) { + this.model.valueUpdates.next(this.chips.getChipsItems()); + this.change.emit(event); + } + + onKeyUp(event) { + if (event.keyCode === 13 || event.keyCode === 188) { + event.preventDefault(); + // Key: Enter or , or ; + this.addTagsToChips(); + event.stopPropagation(); + } + } + + preventEventsPropagation(event) { + event.stopPropagation(); + if (event.keyCode === 13) { + // Key: Enter or , or ; + event.preventDefault(); + } + } + + private addTagsToChips() { + if (!this.hasAuthority || !this.model.authorityOptions.closed) { + let res: string[] = []; + res = this.currentValue.split(','); + + const res1 = []; + res.forEach((item) => { + item.split(';').forEach((i) => { + res1.push(i); + }); + }); + + res1.forEach((c) => { + c = c.trim(); + if (c.length > 0) { + this.chips.add(c); + } + }); + + // this.currentValue = ''; + setTimeout(() => { + // Reset the input text after x ms, mandatory or the formatter overwrite it + this.currentValue = null; + this.cdr.detectChanges(); + }, 50); + this.updateModel(event); + } + } + + removeChips(event) { + // console.log("Removed chips index: "+event); + this.model.valueUpdates.next(this.chips.getChipsItems()); + this.change.emit(event); + } + + changeChips(event) { + this.model.valueUpdates.next(this.chips.getChipsItems()); + this.change.emit(event); + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.model.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.model.spec.ts new file mode 100644 index 0000000000..08f26a99de --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.model.spec.ts @@ -0,0 +1,143 @@ +// import { AUTOCOMPLETE_OFF, DYNAMIC_FORM_CONTROL_INPUT_TYPE_TEXT } from '@ng-dynamic-forms/core'; +// import { Observable } from 'rxjs/Observable'; +// +// import { +// DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD, DynamicTypeaheadModel, +// DynamicTypeaheadResponseModel +// } from './dynamic-tag.model'; +// import { PageInfo } from '../../../../../../core/shared/page-info.model'; +// +// describe('DynamicTypeaheadModel test suite', () => { +// +// let model: any; +// const search = (text: string): Observable => +// Observable.of({ +// list: ['One', 'Two', 'Three'], +// pageInfo: new PageInfo() +// }); +// const config = { +// id: 'input', +// minChars: 3, +// search: search +// }; +// +// beforeEach(() => model = new DynamicTypeaheadModel(config)); +// +// it('tests if correct default type property is set', () => { +// +// expect(model.type).toEqual(DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD); +// }); +// +// it('tests if correct default input type property is set', () => { +// +// expect(model.inputType).toEqual(DYNAMIC_FORM_CONTROL_INPUT_TYPE_TEXT); +// }); +// +// it('tests if correct default autoComplete property is set', () => { +// +// expect(model.autoComplete).toEqual(AUTOCOMPLETE_OFF); +// }); +// +// it('tests if correct default autoFocus property is set', () => { +// +// expect(model.autoFocus).toBe(false); +// }); +// +// it('tests if correct default cls properties aree set', () => { +// +// expect(model.cls).toBeDefined(); +// expect(model.cls.element.container).toEqual(''); +// expect(model.cls.element.control).toEqual(''); +// expect(model.cls.element.errors).toEqual(''); +// expect(model.cls.element.label).toEqual(''); +// expect(model.cls.grid.container).toEqual(''); +// expect(model.cls.grid.control).toEqual(''); +// expect(model.cls.grid.errors).toEqual(''); +// expect(model.cls.grid.label).toEqual(''); +// }); +// +// it('tests if correct default hint property is set', () => { +// +// expect(model.hint).toBeNull(); +// }); +// +// it('tests if correct default label property is set', () => { +// +// expect(model.label).toBeNull(); +// }); +// +// it('tests if correct default max property is set', () => { +// +// expect(model.max).toBeNull(); +// }); +// +// it('tests if correct default maxLength property is set', () => { +// +// expect(model.maxLength).toBeNull(); +// }); +// +// it('tests if correct default minLength property is set', () => { +// +// expect(model.minLength).toBeNull(); +// }); +// +// it('tests if correct minChars property is set', () => { +// +// expect(model.minChars).toEqual(3); +// }); +// +// it('tests if correct default min property is set', () => { +// +// expect(model.min).toBeNull(); +// }); +// +// it('tests if correct default placeholder property is set', () => { +// +// expect(model.placeholder).toEqual(''); +// }); +// +// it('tests if correct default readonly property is set', () => { +// +// expect(model.readOnly).toBe(false); +// }); +// +// it('tests if correct default required property is set', () => { +// +// expect(model.required).toBe(false); +// }); +// +// it('tests if correct default spellcheck property is set', () => { +// +// expect(model.spellCheck).toBe(false); +// }); +// +// it('tests if correct default step property is set', () => { +// +// expect(model.step).toBeNull(); +// }); +// +// it('tests if correct default prefix property is set', () => { +// +// expect(model.prefix).toBeNull(); +// }); +// +// it('tests if correct default suffix property is set', () => { +// +// expect(model.suffix).toBeNull(); +// }); +// +// it('tests if correct search function is set', () => { +// +// expect(model.search).toBe(search); +// }); +// +// it('should serialize correctly', () => { +// +// const json = JSON.parse(JSON.stringify(model)); +// +// expect(json.id).toEqual(model.id); +// expect(json.disabled).toEqual(model.disabled); +// expect(json.value).toBe(model.value); +// expect(json.type).toEqual(DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD); +// }); +// }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.model.ts new file mode 100644 index 0000000000..73018122f3 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.model.ts @@ -0,0 +1,28 @@ +import { AUTOCOMPLETE_OFF, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core'; +import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model'; +import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model'; + +export const DYNAMIC_FORM_CONTROL_TYPE_TAG = 'TYPETAG'; + +export interface DynamicTagModelConfig extends DsDynamicInputModelConfig { + minChars: number; + value?: any; +} + +export class DynamicTagModel extends DsDynamicInputModel { + + @serializable() minChars: number; + @serializable() value: any[]; + @serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_TAG; + + constructor(config: DynamicTagModelConfig, layout?: DynamicFormControlLayout) { + + super(config, layout); + + this.autoComplete = AUTOCOMPLETE_OFF; + this.minChars = config.minChars; + const value = config.value || []; + this.valueUpdates.next(value) + } + +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.html new file mode 100644 index 0000000000..354c3d7532 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.html @@ -0,0 +1,25 @@ + + {{ r.display}} + +
+ + + +
Sorry, suggestions could not be loaded.
+
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.scss new file mode 100644 index 0000000000..e4f6722c65 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.scss @@ -0,0 +1,25 @@ +@import "../../../../../../../styles/variables"; + +/* style fa-spin */ +.fa-spin { + pointer-events: none; +} + +/* align fa-spin */ +.left-addon .fa-spin { left: 0;} +.right-addon .fa-spin { right: 0;} + +:host /deep/ .dropdown-menu { + width: 100% !important; + max-height: 200px; + overflow-y: auto !important; + overflow-x: hidden; +} + +:host /deep/ .dropdown-item.active, +:host /deep/ .dropdown-item:active, +:host /deep/ .dropdown-item:focus, +:host /deep/ .dropdown-item:hover { + color: $dropdown-link-hover-color !important; + background-color: $dropdown-link-hover-bg !important; +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts new file mode 100644 index 0000000000..a261eef8f7 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts @@ -0,0 +1,121 @@ +import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +import { Observable } from 'rxjs/Observable'; +import { NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap'; + +import { AuthorityService } from '../../../../../../core/integration/authority.service'; +import { DynamicTypeaheadModel } from './dynamic-typeahead.model'; +import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model'; +import { isEmpty, isNotEmpty } from '../../../../../empty.util'; +import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; + +@Component({ + selector: 'ds-dynamic-typeahead', + styleUrls: ['./dynamic-typeahead.component.scss'], + templateUrl: './dynamic-typeahead.component.html' +}) +export class DsDynamicTypeaheadComponent implements OnInit { + @Input() bindId = true; + @Input() group: FormGroup; + @Input() model: DynamicTypeaheadModel; + @Input() showErrorMessages = false; + + @Output() blur: EventEmitter = new EventEmitter(); + @Output() change: EventEmitter = new EventEmitter(); + @Output() focus: EventEmitter = new EventEmitter(); + + searching = false; + searchOptions: IntegrationSearchOptions; + searchFailed = false; + hideSearchingWhenUnsubscribed = new Observable(() => () => this.changeSearchingStatus(false)); + currentValue: any; + + formatter = (x: { display: string }) => { + return (typeof x === 'object') ? x.display : x + }; + + search = (text$: Observable) => + text$ + .debounceTime(300) + .distinctUntilChanged() + .do(() => this.changeSearchingStatus(true)) + .switchMap((term) => { + if (term === '' || term.length < this.model.minChars) { + return Observable.of({list: []}); + } else { + this.searchOptions.query = term; + return this.authorityService.getEntriesByName(this.searchOptions) + .map((authorities) => { + // @TODO Pagination for authority is not working, to refactor when it will be fixed + return { + list: authorities.payload, + pageInfo: authorities.pageInfo + }; + }) + .do(() => this.searchFailed = false) + .catch(() => { + this.searchFailed = true; + return Observable.of({list: []}); + }); + } + }) + .map((results) => results.list) + .do(() => this.changeSearchingStatus(false)) + .merge(this.hideSearchingWhenUnsubscribed); + + constructor(private authorityService: AuthorityService, private cdr: ChangeDetectorRef) { + } + + ngOnInit() { + this.currentValue = this.model.value; + this.searchOptions = new IntegrationSearchOptions( + this.model.authorityOptions.scope, + this.model.authorityOptions.name, + this.model.authorityOptions.metadata); + this.group.get(this.model.id).valueChanges + .filter((value) => this.currentValue !== value) + .subscribe((value) => { + this.currentValue = value; + }); + } + + changeSearchingStatus(status: boolean) { + this.searching = status; + this.cdr.detectChanges(); + } + + onInput(event) { + if (!this.model.authorityOptions.closed && isNotEmpty(event.target.value)) { + const valueObj = new FormFieldMetadataValueObject(event.target.value); + this.currentValue = valueObj; + this.model.valueUpdates.next(valueObj as any); + this.change.emit(valueObj); + } + if (event.data) { + // this.group.markAsDirty(); + } + } + + onBlurEvent(event: Event) { + this.blur.emit(event); + } + + onChangeEvent(event: Event) { + event.stopPropagation(); + if (isEmpty(this.currentValue)) { + this.model.valueUpdates.next(null); + this.change.emit(null); + } + } + + onFocusEvent(event) { + this.focus.emit(event); + } + + onSelectItem(event: NgbTypeaheadSelectItemEvent) { + this.currentValue = event.item; + this.model.valueUpdates.next(event.item); + this.change.emit(event.item); + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.model.ts new file mode 100644 index 0000000000..054127825d --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.model.ts @@ -0,0 +1,25 @@ +import { AUTOCOMPLETE_OFF, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core'; +import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model'; +import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model'; + +export const DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD = 'TYPEAHEAD'; + +export interface DsDynamicTypeaheadModelConfig extends DsDynamicInputModelConfig { + minChars: number; + value: any; +} + +export class DynamicTypeaheadModel extends DsDynamicInputModel { + + @serializable() minChars: number; + @serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD; + + constructor(config: DsDynamicTypeaheadModelConfig, layout?: DynamicFormControlLayout) { + + super(config, layout); + + this.autoComplete = AUTOCOMPLETE_OFF; + this.minChars = config.minChars; + } + +} diff --git a/src/app/shared/form/builder/form-builder.service.ts b/src/app/shared/form/builder/form-builder.service.ts new file mode 100644 index 0000000000..8e61f80af4 --- /dev/null +++ b/src/app/shared/form/builder/form-builder.service.ts @@ -0,0 +1,212 @@ +import { Injectable } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +import { + DynamicFormArrayGroupModel, + DynamicFormArrayModel, + DynamicFormControlModel, + DynamicFormGroupModel, + DynamicFormService, + DynamicPathable, + JSONUtils, +} from '@ng-dynamic-forms/core'; +import { mergeWith } from 'lodash'; + +import { isEmpty, isNotEmpty, isNotNull, isNull } from '../../empty.util'; +import { DynamicComboboxModel } from './ds-dynamic-form-ui/models/ds-dynamic-combobox.model'; +import { SubmissionFormsModel } from '../../../core/shared/config/config-submission-forms.model'; +import { DynamicConcatModel } from './ds-dynamic-form-ui/models/ds-dynamic-concat.model'; +import { DynamicListCheckboxGroupModel } from './ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model'; +import { DynamicGroupModel } from './ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.model'; +import { DynamicTagModel } from './ds-dynamic-form-ui/models/tag/dynamic-tag.model'; +import { DynamicListRadioGroupModel } from './ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model'; +import { RowParser } from './parsers/row-parser'; + +import { DynamicRowArrayModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-array-model'; +import { DynamicRowGroupModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-group-model'; + +@Injectable() +export class FormBuilderService extends DynamicFormService { + + findById(id: string, groupModel: DynamicFormControlModel[], arrayIndex = null): DynamicFormControlModel | null { + + let result = null; + const findByIdFn = (findId: string, findGroupModel: DynamicFormControlModel[]): void => { + + for (const controlModel of findGroupModel) { + + if (controlModel.id === findId) { + if (controlModel instanceof DynamicFormArrayModel && isNotNull(arrayIndex)) { + result = controlModel.get(arrayIndex); + } else { + result = controlModel; + } + break; + } + + if (controlModel instanceof DynamicFormGroupModel) { + findByIdFn(findId, (controlModel as DynamicFormGroupModel).group); + } + + if (controlModel instanceof DynamicFormArrayModel && (isNull(arrayIndex) || controlModel.size > (arrayIndex))) { + arrayIndex = (isNull(arrayIndex)) ? 0 : arrayIndex; + findByIdFn(findId, controlModel.get(arrayIndex).group); + } + } + }; + + findByIdFn(id, groupModel); + + return result; + } + + clearAllModelsValue(groupModel: DynamicFormControlModel[]): void { + + const iterateControlModels = (findGroupModel: DynamicFormControlModel[]): void => { + + for (const controlModel of findGroupModel) { + + if (controlModel instanceof DynamicFormGroupModel) { + iterateControlModels((controlModel as DynamicFormGroupModel).group); + continue; + } + + if (controlModel instanceof DynamicFormArrayModel) { + iterateControlModels(controlModel.groupFactory()); + continue; + } + + if (controlModel.hasOwnProperty('valueUpdates')) { + (controlModel as any).valueUpdates.next(undefined); + } + } + }; + + iterateControlModels(groupModel); + } + + getValueFromModel(groupModel: DynamicFormControlModel[]): void { + + let result = Object.create({}); + + const customizer = (objValue, srcValue) => { + if (Array.isArray(objValue)) { + return objValue.concat(srcValue); + } + }; + + const iterateControlModels = (findGroupModel: DynamicFormControlModel[]): void => { + let iterateResult = Object.create({}); + + // Iterate over all group's controls + for (const controlModel of findGroupModel) { + + if (controlModel instanceof DynamicRowGroupModel && !this.isCustomGroup(controlModel)) { + iterateResult = mergeWith(iterateResult, iterateControlModels((controlModel as DynamicFormGroupModel).group), customizer); + continue; + } + + if (controlModel instanceof DynamicFormGroupModel && !this.isCustomGroup(controlModel)) { + iterateResult[controlModel.name] = iterateControlModels((controlModel as DynamicFormGroupModel).group); + continue; + } + + if (controlModel instanceof DynamicRowArrayModel) { + for (const arrayItemModel of controlModel.groups) { + iterateResult = mergeWith(iterateResult, iterateControlModels(arrayItemModel.group), customizer); + } + continue; + } + + if (controlModel instanceof DynamicFormArrayModel) { + iterateResult[controlModel.name] = []; + for (const arrayItemModel of controlModel.groups) { + iterateResult[controlModel.name].push(iterateControlModels(arrayItemModel.group)); + } + continue; + } + + let controlId; + // Get the field's name + if (controlModel instanceof DynamicComboboxModel) { + // If is instance of DynamicComboboxModel take the qualdrop id as field's name + controlId = controlModel.qualdropId; + } else { + controlId = controlModel.name; + } + + const controlValue = (controlModel as any).value || null; + if (controlId && iterateResult.hasOwnProperty(controlId) && isNotNull(iterateResult[controlId])) { + iterateResult[controlId].push(controlValue); + } else { + iterateResult[controlId] = isNotEmpty(controlValue) ? (Array.isArray(controlValue) ? controlValue : [controlValue]) : null; + } + } + + return iterateResult; + }; + + result = iterateControlModels(groupModel); + + return result; + } + + modelFromConfiguration(json: string | SubmissionFormsModel, scopeUUID: string, initFormValues: any, submissionScope?: string, readOnly = false): DynamicFormControlModel[] | never { + let rows: DynamicFormControlModel[] = []; + const rawData = typeof json === 'string' ? JSON.parse(json, JSONUtils.parseReviver) : json; + + if (rawData.rows && !isEmpty(rawData.rows)) { + rawData.rows.forEach((currentRow) => { + const rowParsed = new RowParser(currentRow, scopeUUID, initFormValues, submissionScope, readOnly).parse(); + if (isNotNull(rowParsed)) { + if (Array.isArray(rowParsed)) { + rows = rows.concat(rowParsed); + } else { + rows.push(rowParsed); + } + } + }); + } + + return rows; + } + + isModelInCustomGroup(model: DynamicFormControlModel) { + return model.parent && + (model.parent instanceof DynamicConcatModel + || model.parent instanceof DynamicComboboxModel); + } + + hasMappedGroupValue(model: DynamicFormControlModel) { + return ((model.parent && model.parent instanceof DynamicComboboxModel) + || model.parent instanceof DynamicGroupModel); + } + + isCustomGroup(model: DynamicFormControlModel) { + return model && + (model instanceof DynamicConcatModel + || model instanceof DynamicComboboxModel + || model instanceof DynamicListCheckboxGroupModel + || model instanceof DynamicListRadioGroupModel); + } + + isModelInAuthorityGroup(model: DynamicFormControlModel) { + return (model instanceof DynamicListCheckboxGroupModel || model instanceof DynamicTagModel); + } + + getFormControlById(id: string, formGroup: FormGroup, groupModel: DynamicFormControlModel[], index = 0) { + const fieldModel = this.findById(id, groupModel, index); + return isNotEmpty(fieldModel) ? formGroup.get(this.getPath(fieldModel)) : null; + } + + getId(model: DynamicPathable) { + if (model instanceof DynamicFormArrayGroupModel) { + return model.index.toString(); + } else { + return ((model as DynamicFormControlModel).id !== (model as DynamicFormControlModel).name) ? + (model as DynamicFormControlModel).name : + (model as DynamicFormControlModel).id; + } + } + +} diff --git a/src/app/shared/form/builder/models/form-field-language-value.model.ts b/src/app/shared/form/builder/models/form-field-language-value.model.ts new file mode 100644 index 0000000000..e626c0262d --- /dev/null +++ b/src/app/shared/form/builder/models/form-field-language-value.model.ts @@ -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; +} diff --git a/src/app/shared/form/builder/models/form-field-metadata-value.model.ts b/src/app/shared/form/builder/models/form-field-metadata-value.model.ts new file mode 100644 index 0000000000..7379d62de6 --- /dev/null +++ b/src/app/shared/form/builder/models/form-field-metadata-value.model.ts @@ -0,0 +1,42 @@ +import { isNotEmpty } from '../../../empty.util'; + +export class FormFieldMetadataValueObject { + metadata?: string; + value: string; + display: string; + language: any; + authority: string; + confidence: number; + place: number; + closed: boolean; + label: string; + + constructor(value: string, + language: any = null, + authority: string = null, + display: string = null, + confidence: number = -1, + place: number = -1, + metadata: string = null) { + this.value = value; + this.language = language; + this.authority = authority; + this.display = display || value; + + this.confidence = confidence; + if (authority != null) { + this.confidence = 600; + } else if (isNotEmpty(confidence)) { + this.confidence = confidence; + } + + this.place = place; + if (isNotEmpty(metadata)) { + this.metadata = metadata; + } + } + + hasAuthority(): boolean { + return isNotEmpty(this.authority); + } +} diff --git a/src/app/shared/form/builder/models/form-field-previous-value-object.ts b/src/app/shared/form/builder/models/form-field-previous-value-object.ts new file mode 100644 index 0000000000..f0ead99f91 --- /dev/null +++ b/src/app/shared/form/builder/models/form-field-previous-value-object.ts @@ -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); + } +} diff --git a/src/app/shared/form/builder/models/form-field-unexpected-object.model.ts b/src/app/shared/form/builder/models/form-field-unexpected-object.model.ts new file mode 100644 index 0000000000..1b37498a64 --- /dev/null +++ b/src/app/shared/form/builder/models/form-field-unexpected-object.model.ts @@ -0,0 +1,3 @@ +export interface FormFieldChangedObject { + string: any +} diff --git a/src/app/shared/form/builder/models/form-field.model.ts b/src/app/shared/form/builder/models/form-field.model.ts new file mode 100644 index 0000000000..faf51f14d3 --- /dev/null +++ b/src/app/shared/form/builder/models/form-field.model.ts @@ -0,0 +1,42 @@ +import { autoserialize } from 'cerialize'; +import { FormRowModel } from '../../../../core/shared/config/config-submission-forms.model'; +import { LanguageCode } from './form-field-language-value.model'; +import { FormFieldMetadataValueObject } from './form-field-metadata-value.model'; + +export class FormFieldModel { + + @autoserialize + hints: string; + + @autoserialize + label: string; + + @autoserialize + languageCodes: LanguageCode[]; + + @autoserialize + mandatoryMessage: string; + + @autoserialize + mandatory: string; + + @autoserialize + repeatable: boolean; + + @autoserialize + input: { + type: string; + }; + + @autoserialize + selectableMetadata: FormFieldMetadataValueObject[]; + + @autoserialize + rows: FormRowModel[]; + + @autoserialize + scope: string; + + @autoserialize + value: any; +} diff --git a/src/app/shared/form/builder/parsers/concat-field-parser.ts b/src/app/shared/form/builder/parsers/concat-field-parser.ts new file mode 100644 index 0000000000..14150e9ccc --- /dev/null +++ b/src/app/shared/form/builder/parsers/concat-field-parser.ts @@ -0,0 +1,99 @@ +import { FieldParser } from './field-parser'; +import { FormFieldModel } from '../models/form-field.model'; +import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; +import { + DynamicFormControlLayout, DynamicInputModel, + DynamicInputModelConfig +} from '@ng-dynamic-forms/core'; +import { + CONCAT_FIRST_INPUT_SUFFIX, + CONCAT_GROUP_SUFFIX, CONCAT_SECOND_INPUT_SUFFIX, + DynamicConcatModel, DynamicConcatModelConfig +} from '../ds-dynamic-form-ui/models/ds-dynamic-concat.model'; +import { isNotEmpty } from '../../../empty.util'; + +export class ConcatFieldParser extends FieldParser { + + constructor(protected configData: FormFieldModel, + protected initFormValues, + protected readOnly: boolean, + private separator: string, + protected firstPlaceholder: string = null, + protected secondPlaceholder: string = null) { + super(configData, initFormValues, readOnly); + + this.separator = separator; + this.firstPlaceholder = firstPlaceholder; + this.secondPlaceholder = secondPlaceholder; + } + + public modelFactory(fieldValue: FormFieldMetadataValueObject | any): any { + + let clsGroup: DynamicFormControlLayout; + let clsInput: DynamicFormControlLayout; + const newId = this.configData.selectableMetadata[0].metadata + .split('.') + .slice(0, this.configData.selectableMetadata[0].metadata.split('.').length - 1) + .join('.'); + + clsInput = { + grid: { + host: 'col-sm-6' + } + }; + + const groupId = newId.replace(/\./g, '_') + CONCAT_GROUP_SUFFIX; + const concatGroup: DynamicConcatModelConfig = this.initModel(groupId, false, false); + + concatGroup.group = []; + concatGroup.separator = this.separator; + + const input1ModelConfig: DynamicInputModelConfig = this.initModel(newId + CONCAT_FIRST_INPUT_SUFFIX, true, false, false); + const input2ModelConfig: DynamicInputModelConfig = this.initModel(newId + CONCAT_SECOND_INPUT_SUFFIX, true, true, false); + + if (this.configData.mandatory) { + input1ModelConfig.required = true; + } + + if (isNotEmpty(this.firstPlaceholder)) { + input1ModelConfig.placeholder = this.firstPlaceholder; + } + + if (isNotEmpty(this.secondPlaceholder)) { + input2ModelConfig.placeholder = this.secondPlaceholder; + } + + // Init values + if (isNotEmpty(fieldValue)) { + const values = fieldValue.split(this.separator); + + if (values.length > 1) { + input1ModelConfig.value = values[0]; + input2ModelConfig.value = values[1]; + } + } + + // Split placeholder if is like 'placeholder1/placeholder2' + const placeholder = this.configData.label.split('/'); + if (placeholder.length === 2) { + input1ModelConfig.placeholder = placeholder[0]; + input2ModelConfig.placeholder = placeholder[1]; + } + + const model1 = new DynamicInputModel(input1ModelConfig, clsInput); + const model2 = new DynamicInputModel(input2ModelConfig, clsInput); + concatGroup.group.push(model1); + concatGroup.group.push(model2); + + clsGroup = { + element: { + control: 'form-row', + } + }; + const concatModel = new DynamicConcatModel(concatGroup, clsGroup); + concatModel.name = this.getFieldId()[0]; + + return concatModel; + } + +} diff --git a/src/app/shared/form/builder/parsers/date-field-parser.ts b/src/app/shared/form/builder/parsers/date-field-parser.ts new file mode 100644 index 0000000000..b4b7f62a9e --- /dev/null +++ b/src/app/shared/form/builder/parsers/date-field-parser.ts @@ -0,0 +1,49 @@ +import { FieldParser } from './field-parser'; +import { DynamicDatePickerModelConfig } from '@ng-dynamic-forms/core'; +import { FormFieldModel } from '../models/form-field.model'; +import { DynamicDsDatePickerModel } from '../ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.model'; +import { isNotEmpty } from '../../../empty.util'; +import { DS_DATE_PICKER_SEPARATOR } from '../ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component'; + +export class DateFieldParser extends FieldParser { + + public modelFactory(): any { + const inputDateModelConfig: DynamicDatePickerModelConfig = this.initModel(); + + inputDateModelConfig.toggleIcon = 'fa fa-calendar'; + + const dateModel = new DynamicDsDatePickerModel(inputDateModelConfig); + + // Init Data and validity check + if (isNotEmpty(this.getInitFieldValue())) { + let malformedData = false; + const value = this.getInitFieldValue().toString(); + if (value.length >= 4) { + const valuesArray = value.split(DS_DATE_PICKER_SEPARATOR); + if (valuesArray.length < 4) { + for (let i = 0; i < valuesArray.length; i++) { + const len = i === 0 ? 4 : 2; + if (valuesArray[i].length !== len) { + malformedData = true; + } + } + } + + if (!malformedData) { + dateModel.valueUpdates.next(this.getInitFieldValue()); + } else { + // TODO Set error message + dateModel.malformedDate = true; + // TODO + // const errorMessage = 'The stored date is not compliant'; + // dateModel.validators = Object.assign({}, dateModel.validators, {malformedDate: null}); + // dateModel.errorMessages = Object.assign({}, dateModel.errorMessages, {malformedDate: errorMessage}); + + // this.formService.addErrorToField(this.group.get(this.model.id), this.model, errorMessage) + } + } + + } + return dateModel; + } +} diff --git a/src/app/shared/form/builder/parsers/dropdown-field-parser.ts b/src/app/shared/form/builder/parsers/dropdown-field-parser.ts new file mode 100644 index 0000000000..431e126641 --- /dev/null +++ b/src/app/shared/form/builder/parsers/dropdown-field-parser.ts @@ -0,0 +1,44 @@ +import { FieldParser } from './field-parser'; +import { DynamicFormControlLayout, } from '@ng-dynamic-forms/core'; +import { + DynamicScrollableDropdownModel, + DynamicScrollableDropdownModelConfig +} from '../ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; +import { FormFieldModel } from '../models/form-field.model'; +import { isNotEmpty } from '../../../empty.util'; +import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; + +export class DropdownFieldParser extends FieldParser { + + constructor(protected configData: FormFieldModel, + protected initFormValues, + protected readOnly: boolean, + protected authorityUuid: string) { + super(configData, initFormValues, readOnly); + } + + public modelFactory(fieldValue: FormFieldMetadataValueObject): any { + const dropdownModelConfig: DynamicScrollableDropdownModelConfig = this.initModel(); + let layout: DynamicFormControlLayout; + + if (isNotEmpty(this.configData.selectableMetadata[0].authority)) { + this.setAuthorityOptions(dropdownModelConfig, this.authorityUuid); + dropdownModelConfig.maxOptions = 10; + if (isNotEmpty(fieldValue)) { + dropdownModelConfig.value = fieldValue; + } + layout = { + element: { + control: 'col' + }, + grid: { + host: 'col' + } + }; + const dropdownModel = new DynamicScrollableDropdownModel(dropdownModelConfig, layout); + return dropdownModel; + } else { + throw Error(`Authority name is not available. Please checks form configuration file.`); + } + } +} diff --git a/src/app/shared/form/builder/parsers/field-parser.ts b/src/app/shared/form/builder/parsers/field-parser.ts new file mode 100644 index 0000000000..29a133171d --- /dev/null +++ b/src/app/shared/form/builder/parsers/field-parser.ts @@ -0,0 +1,272 @@ +import { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../../empty.util'; +import { FormFieldModel } from '../models/form-field.model'; + +import { uniqueId } from 'lodash'; +import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; +import { + DynamicRowArrayModel, + DynamicRowArrayModelConfig +} from '../ds-dynamic-form-ui/models/ds-dynamic-row-array-model'; +import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model'; +import { DynamicFormControlLayout } from '@ng-dynamic-forms/core'; +import { setLayout } from './parser.utils'; +import { AuthorityOptions } from '../../../../core/integration/models/authority-options.model'; + +export abstract class FieldParser { + + protected fieldId: string; + + constructor(protected configData: FormFieldModel, protected initFormValues, protected readOnly: boolean) { + } + + public abstract modelFactory(fieldValue?: FormFieldMetadataValueObject): any; + + public parse() { + if (((this.getInitValueCount() > 1 && !this.configData.repeatable) || (this.configData.repeatable)) + && (this.configData.input.type !== 'list') + && (this.configData.input.type !== 'tag') + && (this.configData.input.type !== 'group') + ) { + let arrayCounter = 0; + let fieldArrayCounter = 0; + + const config = { + id: uniqueId() + '_array', + initialCount: this.getInitArrayIndex(), + notRepeteable: !this.configData.repeatable, + groupFactory: () => { + let model; + if ((arrayCounter === 0)) { + model = this.modelFactory(); + arrayCounter++; + } else { + const fieldArrayOfValueLenght = this.getInitValueCount(arrayCounter - 1); + let fieldValue = null; + if (fieldArrayOfValueLenght > 0) { + fieldValue = this.getInitFieldValue(arrayCounter - 1, fieldArrayCounter++); + if (fieldArrayCounter === fieldArrayOfValueLenght) { + fieldArrayCounter = 0; + arrayCounter++; + } + } + model = this.modelFactory(fieldValue); + } + setLayout(model, 'element', 'host', 'col'); + if (model.hasLanguages) { + setLayout(model, 'grid', 'control', 'col'); + } + return [model]; + } + } as DynamicRowArrayModelConfig; + + const layout: DynamicFormControlLayout = { + grid: { + group: 'dsgridgroup form-row' + } + }; + + return new DynamicRowArrayModel(config, layout); + + } else { + const model = this.modelFactory(this.getInitFieldValue()); + if (model.hasLanguages) { + setLayout(model, 'grid', 'control', 'col'); + } + return model; + } + } + + protected getInitValueCount(index = 0, fieldId?): number { + const fieldIds = fieldId || this.getFieldId(); + if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length === 1 && this.initFormValues.hasOwnProperty(fieldIds[0])) { + return this.initFormValues[fieldIds[0]].length; + } else if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length > 1) { + const values = []; + fieldIds.forEach((id) => { + if (this.initFormValues.hasOwnProperty(id)) { + values.push(this.initFormValues[id].length); + } + }); + return values[index]; + } else { + return 0; + } + } + + protected getInitGroupValues(): FormFieldMetadataValueObject[] { + const fieldIds = this.getFieldId(); + if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length === 1 && this.initFormValues.hasOwnProperty(fieldIds[0])) { + return this.initFormValues[fieldIds[0]]; + } + } + + protected getInitFieldValues(fieldId): FormFieldMetadataValueObject[] { + if (isNotEmpty(this.initFormValues) && isNotNull(fieldId) && this.initFormValues.hasOwnProperty(fieldId)) { + return this.initFormValues[fieldId]; + } + } + + protected getInitFieldValue(outerIndex = 0, innerIndex = 0, fieldId?): FormFieldMetadataValueObject { + const fieldIds = fieldId || this.getFieldId(); + if (isNotEmpty(this.initFormValues) + && isNotNull(fieldIds) + && fieldIds.length === 1 + && this.initFormValues.hasOwnProperty(fieldIds[outerIndex]) + && this.initFormValues[fieldIds[outerIndex]].length > innerIndex) { + return this.initFormValues[fieldIds[outerIndex]][innerIndex]; + } else if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length > 1) { + const values: FormFieldMetadataValueObject[] = []; + fieldIds.forEach((id) => { + if (this.initFormValues.hasOwnProperty(id)) { + const valueObj: FormFieldMetadataValueObject = Object.create({}); + valueObj.metadata = id; + valueObj.value = this.initFormValues[id][innerIndex]; + values.push(valueObj); + } + }); + return values[outerIndex]; + } else { + return null; + } + } + + protected getInitArrayIndex() { + const fieldIds: any = this.getFieldId(); + if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length === 1 && this.initFormValues.hasOwnProperty(fieldIds)) { + return this.initFormValues[fieldIds].length; + } else if (isNotEmpty(this.initFormValues) && isNotNull(fieldIds) && fieldIds.length > 1) { + let counter = 0; + fieldIds.forEach((id) => { + if (this.initFormValues.hasOwnProperty(id)) { + counter = counter + this.initFormValues[id].length; + } + }); + return (counter === 0) ? 1 : counter; + } else { + return 1; + } + } + + protected getFieldId(): string[] { + if (Array.isArray(this.configData.selectableMetadata)) { + if (this.configData.selectableMetadata.length === 1) { + return [this.configData.selectableMetadata[0].metadata]; + } else { + const ids = []; + this.configData.selectableMetadata.forEach((entry) => ids.push(entry.metadata)); + return ids; + } + } else { + return null; + } + } + + protected initModel(id?: string, label = true, labelEmpty = false, setErrors = true) { + + const controlModel = Object.create(null); + + // Sets input ID + this.fieldId = id ? id : this.getFieldId()[0]; + + // Sets input name (with the original field's id value) + controlModel.name = this.fieldId; + + // input ID doesn't allow dots, so replace them + controlModel.id = (this.fieldId).replace(/\./g, '_'); + + // Set read only option + controlModel.readOnly = this.readOnly; + controlModel.disabled = this.readOnly; + + if (label) { + controlModel.label = (labelEmpty) ? ' ' : this.configData.label; + } + + controlModel.placeholder = this.configData.label; + + if (this.configData.mandatory && setErrors) { + this.setErrors(controlModel); + } + + // Available Languages + if (this.configData.languageCodes && this.configData.languageCodes.length > 0) { + (controlModel as DsDynamicInputModel).languageCodes = this.configData.languageCodes; + } + + return controlModel; + } + + protected setErrors(controlModel) { + controlModel.required = true; + controlModel.validators = Object.assign({}, controlModel.validators, {required: null}); + controlModel.errorMessages = Object.assign( + {}, + controlModel.errorMessages, + {required: this.configData.mandatoryMessage}); + } + + protected setOptions(controlModel) { + // Checks if field has multiple values and sets options available + if (isNotUndefined(this.configData.selectableMetadata) && this.configData.selectableMetadata.length > 1) { + controlModel.options = []; + this.configData.selectableMetadata.forEach((option, key) => { + if (key === 0) { + controlModel.value = option.metadata; + } + controlModel.options.push({label: option.label, value: option.metadata}); + }); + } + } + + public setAuthorityOptions(controlModel, authorityUuid) { + if (isNotEmpty(this.configData.selectableMetadata[0].authority)) { + controlModel.authorityOptions = new AuthorityOptions( + this.configData.selectableMetadata[0].authority, + this.configData.selectableMetadata[0].metadata, + authorityUuid, + this.configData.selectableMetadata[0].closed + ) + } + } + + public setValues(modelConfig: DsDynamicInputModelConfig, fieldValue: any, forceValueAsObj: boolean = false, groupModel?: boolean) { + if (isNotEmpty(fieldValue)) { + if (groupModel) { + // Array, values is an array + modelConfig.value = this.getInitGroupValues(); + if (Array.isArray(modelConfig.value) && modelConfig.value.length > 0 && modelConfig.value[0].language) { + // Array Item has language, ex. AuthorityModel + modelConfig.language = modelConfig.value[0].language; + } + return; + } + + if (typeof fieldValue === 'object') { + modelConfig.language = fieldValue.language; + if (hasValue(fieldValue.language)) { + // Instance of FormFieldLanguageValueObject + modelConfig.value = fieldValue.value; + } else if (hasValue(fieldValue.metadata)) { + // Is a combobox field's value + modelConfig.value = fieldValue.value; + } else { + // Instance of FormFieldMetadataValueObject + modelConfig.value = fieldValue; + } + } else { + if (forceValueAsObj) { + // If value isn't an instance of FormFieldMetadataValueObject instantiate it + modelConfig.value = new FormFieldMetadataValueObject(fieldValue); + } else { + if (typeof fieldValue === 'string') { + // Case only string + modelConfig.value = fieldValue; + } + } + } + } + + return modelConfig; + } + +} diff --git a/src/app/shared/form/builder/parsers/group-field-parser.ts b/src/app/shared/form/builder/parsers/group-field-parser.ts new file mode 100644 index 0000000000..5ed0dfc18b --- /dev/null +++ b/src/app/shared/form/builder/parsers/group-field-parser.ts @@ -0,0 +1,67 @@ +import { FieldParser } from './field-parser'; +import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; +import { FormFieldModel } from '../models/form-field.model'; +import { + DynamicGroupModel, + DynamicGroupModelConfig, PLACEHOLDER_PARENT_METADATA +} from '../ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.model'; +import { isNotEmpty } from '../../../empty.util'; +import { FormRowModel } from '../../../../core/shared/config/config-submission-forms.model'; + +export class GroupFieldParser extends FieldParser { + + constructor(protected configData: FormFieldModel, + protected initFormValues, + protected readOnly: boolean, + protected authorityUuid: string) { + super(configData, initFormValues, readOnly); + } + + public modelFactory(fieldValue: FormFieldMetadataValueObject) { + const modelConfiguration: DynamicGroupModelConfig = this.initModel(); + + modelConfiguration.scopeUUID = this.authorityUuid; + if (this.configData && this.configData.rows && this.configData.rows.length > 0) { + modelConfiguration.formConfiguration = this.configData.rows; + modelConfiguration.relationFields = []; + this.configData.rows.forEach((row: FormRowModel) => { + row.fields.forEach((field: FormFieldModel) => { + if (field.selectableMetadata[0].metadata === this.configData.selectableMetadata[0].metadata) { + if (!field.mandatory) { + // throw new Error(`Configuration not valid: Main field ${this.configData.selectableMetadata[0].metadata} may be mandatory`); + } + modelConfiguration.mandatoryField = this.configData.selectableMetadata[0].metadata; + } else { + modelConfiguration.relationFields.push(field.selectableMetadata[0].metadata); + } + }) + }); + } else { + throw new Error(`Configuration not valid: ${modelConfiguration.name}`); + } + + if (isNotEmpty(this.getInitGroupValues())) { + modelConfiguration.value = []; + const mandatoryFieldEntries: FormFieldMetadataValueObject[] = this.getInitFieldValues(modelConfiguration.mandatoryField); + mandatoryFieldEntries.forEach((entry, index) => { + const item = Object.create(null); + const listFields = modelConfiguration.relationFields.concat(modelConfiguration.mandatoryField); + listFields.forEach((fieldId) => { + const value = this.getInitFieldValue(0, index, [fieldId]); + item[fieldId] = isNotEmpty(value) ? value : PLACEHOLDER_PARENT_METADATA; + }); + modelConfiguration.value.push(item); + }) + } + const cls = { + element: { + container: 'mb-3' + } + }; + + const model = new DynamicGroupModel(modelConfiguration, cls); + model.name = this.getFieldId()[0]; + return model; + } + +} diff --git a/src/app/shared/form/builder/parsers/list-field-parser.ts b/src/app/shared/form/builder/parsers/list-field-parser.ts new file mode 100644 index 0000000000..a502918a02 --- /dev/null +++ b/src/app/shared/form/builder/parsers/list-field-parser.ts @@ -0,0 +1,52 @@ +import { FieldParser } from './field-parser'; +import { FormFieldModel } from '../models/form-field.model'; +import { isNotEmpty } from '../../../empty.util'; +import { IntegrationSearchOptions } from '../../../../core/integration/models/integration-options.model'; +import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; +import { DynamicListCheckboxGroupModel } from '../ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model'; +import { DynamicListRadioGroupModel } from '../ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model'; + +export class ListFieldParser extends FieldParser { + searchOptions: IntegrationSearchOptions; + + constructor(protected configData: FormFieldModel, + protected initFormValues, + protected readOnly: boolean, + protected authorityUuid: string) { + super(configData, initFormValues, readOnly); + } + + public modelFactory(fieldValue: FormFieldMetadataValueObject): any { + const listModelConfig = this.initModel(); + listModelConfig.repeatable = this.configData.repeatable; + + if (this.configData.selectableMetadata[0].authority + && this.configData.selectableMetadata[0].authority.length > 0) { + + if (isNotEmpty(this.getInitGroupValues())) { + listModelConfig.value = []; + this.getInitGroupValues().forEach((value: any) => { + if (value instanceof FormFieldMetadataValueObject) { + listModelConfig.value.push(value); + } else { + const valueObj = new FormFieldMetadataValueObject(value); + listModelConfig.value.push(valueObj); + } + }); + } + this.setAuthorityOptions(listModelConfig, this.authorityUuid); + } + + let listModel; + if (listModelConfig.repeatable) { + listModelConfig.group = []; + listModel = new DynamicListCheckboxGroupModel(listModelConfig); + } else { + listModelConfig.options = []; + listModel = new DynamicListRadioGroupModel(listModelConfig); + } + + return listModel; + } + +} diff --git a/src/app/shared/form/builder/parsers/lookup-field-parser.ts b/src/app/shared/form/builder/parsers/lookup-field-parser.ts new file mode 100644 index 0000000000..9cec80c5ad --- /dev/null +++ b/src/app/shared/form/builder/parsers/lookup-field-parser.ts @@ -0,0 +1,30 @@ +import { FieldParser } from './field-parser'; +import { FormFieldModel } from '../models/form-field.model'; +import { AuthorityValueModel } from '../../../../core/integration/models/authority-value.model'; +import { isNotEmpty } from '../../../empty.util'; +import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; +import { DynamicLookupModel, DynamicLookupModelConfig } from '../ds-dynamic-form-ui/models/lookup/dynamic-lookup.model'; + +export class LookupFieldParser extends FieldParser { + + constructor(protected configData: FormFieldModel, + protected initFormValues, + protected readOnly: boolean, + protected authorityUuid: string) { + super(configData, initFormValues, readOnly); + } + + public modelFactory(fieldValue: any): any { + if (this.configData.selectableMetadata[0].authority) { + const lookupModelConfig: DynamicLookupModelConfig = this.initModel(); + + this.setAuthorityOptions(lookupModelConfig, this.authorityUuid); + lookupModelConfig.maxOptions = 10; + + this.setValues(lookupModelConfig, fieldValue, true); + + const lookupModel = new DynamicLookupModel(lookupModelConfig); + return lookupModel; + } + } +} diff --git a/src/app/shared/form/builder/parsers/lookup-name-field-parser.ts b/src/app/shared/form/builder/parsers/lookup-name-field-parser.ts new file mode 100644 index 0000000000..8ae8d78b2b --- /dev/null +++ b/src/app/shared/form/builder/parsers/lookup-name-field-parser.ts @@ -0,0 +1,22 @@ +import { FormFieldModel } from '../models/form-field.model'; +import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; +import { LookupFieldParser } from './lookup-field-parser'; + +export class LookupNameFieldParser extends LookupFieldParser { + + constructor(protected configData: FormFieldModel, + protected initFormValues, + protected readOnly: boolean, + protected authorityUuid: string) { + super(configData, initFormValues, readOnly, authorityUuid); + } + + public modelFactory(fieldValue: FormFieldMetadataValueObject | any): any { + const lookupModel = super.modelFactory(fieldValue); + lookupModel.separator = ','; + lookupModel.placeholder = 'form.last-name'; + lookupModel.placeholder2 = 'form.first-name'; + return lookupModel; + + } +} diff --git a/src/app/shared/form/builder/parsers/name-field-parser.ts b/src/app/shared/form/builder/parsers/name-field-parser.ts new file mode 100644 index 0000000000..dbc9d45f86 --- /dev/null +++ b/src/app/shared/form/builder/parsers/name-field-parser.ts @@ -0,0 +1,10 @@ +import { FormFieldModel } from '../models/form-field.model'; + +import { ConcatFieldParser } from './concat-field-parser'; + +export class NameFieldParser extends ConcatFieldParser { + + constructor(protected configData: FormFieldModel, protected initFormValues, protected readOnly: boolean) { + super(configData, initFormValues, readOnly, ',', 'form.last-name', 'form.first-name'); + } +} diff --git a/src/app/shared/form/builder/parsers/onebox-field-parser.ts b/src/app/shared/form/builder/parsers/onebox-field-parser.ts new file mode 100644 index 0000000000..3b1bfc935c --- /dev/null +++ b/src/app/shared/form/builder/parsers/onebox-field-parser.ts @@ -0,0 +1,95 @@ +import { DynamicSelectModel, DynamicSelectModelConfig } from '@ng-dynamic-forms/core'; + +import { FieldParser } from './field-parser'; +import { FormFieldModel } from '../models/form-field.model'; +import { + COMBOBOX_GROUP_SUFFIX, + COMBOBOX_METADATA_SUFFIX, + COMBOBOX_VALUE_SUFFIX, + DsDynamicComboboxModelConfig, + DynamicComboboxModel +} from '../ds-dynamic-form-ui/models/ds-dynamic-combobox.model'; +import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; +import { isNotEmpty } from '../../../empty.util'; +import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model'; +import { + DsDynamicTypeaheadModelConfig, + DynamicTypeaheadModel +} from '../ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.model'; + +export class OneboxFieldParser extends FieldParser { + + constructor(protected configData: FormFieldModel, + protected initFormValues, + protected readOnly: boolean, + protected authorityUuid: string) { + super(configData, initFormValues, readOnly); + } + + public modelFactory(fieldValue: FormFieldMetadataValueObject): any { + if (this.configData.selectableMetadata.length > 1) { + // Case ComboBox + const clsGroup = { + element: { + control: 'form-row', + } + }; + + const clsSelect = { + element: { + control: 'input-group-addon ds-form-input-addon', + }, + grid: { + host: 'col-sm-4 pr-0' + } + }; + + const clsInput = { + element: { + control: 'ds-form-input-value', + }, + grid: { + host: 'col-sm-8 pl-0' + } + }; + + const newId = this.configData.selectableMetadata[0].metadata + .split('.') + .slice(0, this.configData.selectableMetadata[0].metadata.split('.').length - 1) + .join('.'); + + const inputSelectGroup: DsDynamicComboboxModelConfig = Object.create(null); + inputSelectGroup.id = newId.replace(/\./g, '_') + COMBOBOX_GROUP_SUFFIX; + inputSelectGroup.group = []; + inputSelectGroup.legend = this.configData.label; + + const selectModelConfig: DynamicSelectModelConfig = this.initModel(newId + COMBOBOX_METADATA_SUFFIX); + this.setOptions(selectModelConfig); + if (isNotEmpty(fieldValue)) { + selectModelConfig.value = fieldValue.metadata; + } + selectModelConfig.disabled = true; + inputSelectGroup.group.push(new DynamicSelectModel(selectModelConfig, clsSelect)); + + const inputModelConfig: DsDynamicInputModelConfig = this.initModel(newId + COMBOBOX_VALUE_SUFFIX, true, true); + this.setValues(inputModelConfig, fieldValue); + + inputSelectGroup.readOnly = selectModelConfig.disabled && inputModelConfig.readOnly; + inputSelectGroup.group.push(new DsDynamicInputModel(inputModelConfig, clsInput)); + + return new DynamicComboboxModel(inputSelectGroup, clsGroup); + } else if (this.configData.selectableMetadata[0].authority) { + const typeaheadModelConfig: DsDynamicTypeaheadModelConfig = this.initModel(); + this.setAuthorityOptions(typeaheadModelConfig, this.authorityUuid); + this.setValues(typeaheadModelConfig, fieldValue, true); + typeaheadModelConfig.minChars = 3; + const typeaheadModel = new DynamicTypeaheadModel(typeaheadModelConfig); + return typeaheadModel; + } else { + const inputModelConfig: DsDynamicInputModelConfig = this.initModel(); + this.setValues(inputModelConfig, fieldValue); + const inputModel = new DsDynamicInputModel(inputModelConfig); + return inputModel; + } + } +} diff --git a/src/app/shared/form/builder/parsers/parser.utils.ts b/src/app/shared/form/builder/parsers/parser.utils.ts new file mode 100644 index 0000000000..734ec4b83a --- /dev/null +++ b/src/app/shared/form/builder/parsers/parser.utils.ts @@ -0,0 +1,17 @@ +import { isNull, isUndefined } from '../../../empty.util'; +import { DynamicFormControlLayout, DynamicFormControlLayoutConfig } from '@ng-dynamic-forms/core'; + +export function setLayout(model: any, controlLayout: string, controlLayoutConfig: string, style: string) { + if (isNull(model.layout)) { + model.layout = {} as DynamicFormControlLayout; + model.layout[controlLayout] = {} as DynamicFormControlLayoutConfig; + model.layout[controlLayout][controlLayoutConfig] = style; + } else if (isUndefined(model.layout[controlLayout])) { + model.layout[controlLayout] = {} as DynamicFormControlLayoutConfig; + model.layout[controlLayout][controlLayoutConfig] = style; + } else if (isUndefined(model.layout[controlLayout][controlLayoutConfig])) { + model.layout[controlLayout][controlLayoutConfig] = style; + } else { + model.layout[controlLayout][controlLayoutConfig] = model.layout[controlLayout][controlLayoutConfig].concat(` ${style}`); + } +} diff --git a/src/app/shared/form/builder/parsers/row-parser.ts b/src/app/shared/form/builder/parsers/row-parser.ts new file mode 100644 index 0000000000..b082cc060a --- /dev/null +++ b/src/app/shared/form/builder/parsers/row-parser.ts @@ -0,0 +1,162 @@ +import { DynamicFormArrayModel, DynamicFormControlModel, DynamicFormGroupModelConfig } from '@ng-dynamic-forms/core'; +import { uniqueId } from 'lodash'; + +import { DateFieldParser } from './date-field-parser'; +import { DropdownFieldParser } from './dropdown-field-parser'; +import { ListFieldParser } from './list-field-parser'; +import { OneboxFieldParser } from './onebox-field-parser'; +import { NameFieldParser } from './name-field-parser'; +import { SeriesFieldParser } from './series-field-parser'; +import { TagFieldParser } from './tag-field-parser'; +import { TextareaFieldParser } from './textarea-field-parser'; +import { GroupFieldParser } from './group-field-parser'; +import { IntegrationSearchOptions } from '../../../../core/integration/models/integration-options.model'; +import { DynamicGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.model'; +import { DynamicRowGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-group-model'; +import { isEmpty } from '../../../empty.util'; +import { LookupFieldParser } from './lookup-field-parser'; +import { LookupNameFieldParser } from './lookup-name-field-parser'; +import { DsDynamicInputModel } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model'; +import { setLayout } from './parser.utils'; +import { FormFieldModel } from '../models/form-field.model'; + +export const ROW_ID_PREFIX = 'df-row-group-config-'; + +export class RowParser { + protected authorityOptions: IntegrationSearchOptions; + + constructor(protected rowData, + protected scopeUUID, + protected initFormValues: any, + protected submissionScope, + protected readOnly: boolean) { + this.authorityOptions = new IntegrationSearchOptions(scopeUUID); + } + + public parse(): DynamicRowGroupModel { + let fieldModel: any = null; + let parsedResult = null; + const config: DynamicFormGroupModelConfig = { + id: uniqueId(ROW_ID_PREFIX), + group: [], + }; + + const scopedFields: FormFieldModel[] = this.filterScopedFields(this.rowData.fields); + + const layoutGridClass = ' col-sm-' + Math.trunc(12 / scopedFields.length) + ' d-flex flex-column justify-content-start'; + + // Iterate over row's fields + scopedFields.forEach((fieldData: FormFieldModel) => { + + switch (fieldData.input.type) { + case 'date': + fieldModel = (new DateFieldParser(fieldData, this.initFormValues, this.readOnly).parse()); + break; + + case 'dropdown': + fieldModel = (new DropdownFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse()); + break; + + case 'list': + fieldModel = (new ListFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse()); + break; + + case 'lookup': + fieldModel = (new LookupFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse()); + break; + + case 'onebox': + fieldModel = (new OneboxFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse()); + break; + + case 'lookup-name': + fieldModel = (new LookupNameFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse()); + break; + + case 'name': + fieldModel = (new NameFieldParser(fieldData, this.initFormValues, this.readOnly).parse()); + break; + + case 'series': + fieldModel = (new SeriesFieldParser(fieldData, this.initFormValues, this.readOnly).parse()); + break; + + case 'tag': + fieldModel = (new TagFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse()); + break; + + case 'textarea': + fieldModel = (new TextareaFieldParser(fieldData, this.initFormValues, this.readOnly).parse()); + break; + + case 'group': + fieldModel = new GroupFieldParser(fieldData, this.initFormValues, this.readOnly, this.authorityOptions.uuid).parse(); + break; + + case 'twobox': + // group.push(new TwoboxFieldParser(fieldData).parse()); + break; + + default: + throw new Error(`unknown form control model type defined on JSON object with label "${fieldData.label}"`); + } + + if (fieldModel) { + if (fieldModel instanceof DynamicFormArrayModel || fieldModel instanceof DynamicGroupModel) { + if (this.rowData.fields.length > 1) { + setLayout(fieldModel, 'grid', 'host', layoutGridClass); + config.group.push(fieldModel); + // if (isEmpty(parsedResult)) { + // parsedResult = []; + // } + // parsedResult.push(fieldModel); + } else { + parsedResult = fieldModel; + } + return; + } else { + if (fieldModel instanceof Array) { + fieldModel.forEach((model) => { + parsedResult = model; + return; + }) + } else { + setLayout(fieldModel, 'grid', 'host', layoutGridClass); + config.group.push(fieldModel); + } + } + fieldModel = null; + } + }); + + if (config && !isEmpty(config.group)) { + const clsGroup = { + element: { + control: 'form-row', + } + }; + const groupModel = new DynamicRowGroupModel(config, clsGroup); + if (Array.isArray(parsedResult)) { + parsedResult.push(groupModel) + } else { + parsedResult = groupModel; + } + } + return parsedResult; + } + + checksFieldScope(fieldScope) { + return (isEmpty(fieldScope) || isEmpty(this.submissionScope) || fieldScope === this.submissionScope); + } + + filterScopedFields(fields: FormFieldModel[]): FormFieldModel[] { + const filteredFields: FormFieldModel[] = []; + fields.forEach((field: FormFieldModel) => { + // Whether field scope doesn't match the submission scope, skip it + if (this.checksFieldScope(field.scope)) { + filteredFields.push(field); + } + }); + return filteredFields; + } +} diff --git a/src/app/shared/form/builder/parsers/series-field-parser.ts b/src/app/shared/form/builder/parsers/series-field-parser.ts new file mode 100644 index 0000000000..dd0b406e8e --- /dev/null +++ b/src/app/shared/form/builder/parsers/series-field-parser.ts @@ -0,0 +1,10 @@ +import { FormFieldModel } from '../models/form-field.model'; + +import { ConcatFieldParser } from './concat-field-parser'; + +export class SeriesFieldParser extends ConcatFieldParser { + + constructor(protected configData: FormFieldModel, protected initFormValues, protected readOnly: boolean) { + super(configData, initFormValues, readOnly, ';'); + } +} diff --git a/src/app/shared/form/builder/parsers/tag-field-parser.ts b/src/app/shared/form/builder/parsers/tag-field-parser.ts new file mode 100644 index 0000000000..fc153337e6 --- /dev/null +++ b/src/app/shared/form/builder/parsers/tag-field-parser.ts @@ -0,0 +1,31 @@ +import { FieldParser } from './field-parser'; +import { FormFieldModel } from '../models/form-field.model'; +import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; +import { isNotEmpty } from '../../../empty.util'; +import { DynamicTagModel, DynamicTagModelConfig } from '../ds-dynamic-form-ui/models/tag/dynamic-tag.model'; + +export class TagFieldParser extends FieldParser { + + constructor(protected configData: FormFieldModel, + protected initFormValues, + protected readOnly: boolean, + protected authorityUuid: string) { + super(configData, initFormValues, readOnly); + } + + public modelFactory(fieldValue: FormFieldMetadataValueObject): any { + const tagModelConfig: DynamicTagModelConfig = this.initModel(); + if (this.configData.selectableMetadata[0].authority + && this.configData.selectableMetadata[0].authority.length > 0) { + this.setAuthorityOptions(tagModelConfig, this.authorityUuid); + } + + tagModelConfig.minChars = 3; + this.setValues(tagModelConfig, fieldValue, null, true); + + const tagModel = new DynamicTagModel(tagModelConfig); + + return tagModel; + } + +} diff --git a/src/app/shared/form/builder/parsers/textarea-field-parser.ts b/src/app/shared/form/builder/parsers/textarea-field-parser.ts new file mode 100644 index 0000000000..4596bb18ce --- /dev/null +++ b/src/app/shared/form/builder/parsers/textarea-field-parser.ts @@ -0,0 +1,32 @@ +import { FieldParser } from './field-parser'; +import { + DynamicFormControlLayout, DynamicTextAreaModel, DynamicTextAreaModelConfig +} from '@ng-dynamic-forms/core'; +import { FormFieldModel } from '../models/form-field.model'; +import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; +import { isNotEmpty } from '../../../empty.util'; +import { + DsDynamicTextAreaModel, + DsDynamicTextAreaModelConfig +} from '../ds-dynamic-form-ui/models/ds-dynamic-textarea.model'; + +export class TextareaFieldParser extends FieldParser { + + public modelFactory(fieldValue: FormFieldMetadataValueObject | any): any { + const textAreaModelConfig: DsDynamicTextAreaModelConfig = this.initModel(); + + let layout: DynamicFormControlLayout; + + layout = { + element: { + label: 'col-form-label' + } + }; + + textAreaModelConfig.rows = 10; + this.setValues(textAreaModelConfig, fieldValue); + const textAreaModel = new DsDynamicTextAreaModel(textAreaModelConfig, layout); + + return textAreaModel; + } +} diff --git a/src/app/shared/form/builder/parsers/twobox-field-parser.ts b/src/app/shared/form/builder/parsers/twobox-field-parser.ts new file mode 100644 index 0000000000..1614ccf331 --- /dev/null +++ b/src/app/shared/form/builder/parsers/twobox-field-parser.ts @@ -0,0 +1,10 @@ +import { FieldParser } from './field-parser'; +import { FormFieldModel } from '../models/form-field.model'; + +// @TODO to be implemented +export class TwoboxFieldParser extends FieldParser { + + public modelFactory(): any { + return null; + } +} diff --git a/src/app/shared/form/form.actions.ts b/src/app/shared/form/form.actions.ts new file mode 100644 index 0000000000..681add077e --- /dev/null +++ b/src/app/shared/form/form.actions.ts @@ -0,0 +1,126 @@ +import { Action } from '@ngrx/store'; + +import { type } from '../ngrx/type'; + +/** + * For each action type in an action group, make a simple + * enum object for all of this group's action types. + * + * The 'type' utility function coerces strings into string + * literal types and runs a simple check to guarantee all + * action types in the application are unique. + */ +export const FormActionTypes = { + FORM_INIT: type('dspace/form/FORM_INIT'), + FORM_CHANGE: type('dspace/form/FORM_CHANGE'), + FORM_REMOVE: type('dspace/form/FORM_REMOVE'), + FORM_STATUS_CHANGE: type('dspace/form/FORM_STATUS_CHANGE'), + FORM_ADD_ERROR: type('dspace/form/ADD_ERROR'), + FORM_REMOVE_ERROR: type('dspace/form/REMOVE_ERROR'), + CLEAR_ERRORS: type('dspace/form/CLEAR_ERRORS'), +}; + +/* tslint:disable:max-classes-per-file */ +export class FormInitAction implements Action { + type = FormActionTypes.FORM_INIT; + payload: { + formId: string; + formData: any; + valid: boolean; + }; + + /** + * Create a new FormInitAction + * + * @param formId + * the Form's ID + * @param formData + * the FormGroup Object + * @param valid + * the Form validation status + */ + constructor(formId: string, formData: any, valid: boolean) { + this.payload = {formId, formData, valid}; + } +} + +export class FormChangeAction implements Action { + type = FormActionTypes.FORM_CHANGE; + payload: { + formId: string; + formData: any; + }; + + /** + * Create a new FormInitAction + * + * @param formId + * the Form's ID + * @param formData + * the FormGroup Object + */ + constructor(formId: string, formData: any) { + this.payload = {formId, formData}; + } +} + +export class FormRemoveAction implements Action { + type = FormActionTypes.FORM_REMOVE; + payload: { + formId: string; + }; + + /** + * Create a new FormRemoveAction + * + * @param formId + * the Form's ID + */ + constructor(formId: string) { + this.payload = {formId}; + } +} + +export class FormStatusChangeAction implements Action { + type = FormActionTypes.FORM_STATUS_CHANGE; + payload: { + formId: string; + valid: boolean; + }; + + /** + * Create a new FormInitAction + * + * @param formId + * the Form's ID + * @param valid + * the Form validation status + */ + constructor(formId: string, valid: boolean) { + this.payload = {formId, valid}; + } +} + +export class FormAddError implements Action { + type = FormActionTypes.FORM_ADD_ERROR; + payload: { + formId: string, + fieldId: string, + errorMessage: string, + }; + + constructor(formId: string, fieldId: string, errorMessage: string) { + this.payload = {formId, fieldId, errorMessage}; + } +} + +/* tslint:enable:max-classes-per-file */ + +/** + * Export a type alias of all actions in this action group + * so that reducers can easily compose action types + */ +export type FormAction = FormInitAction + | FormChangeAction + | FormStatusChangeAction + | FormAddError diff --git a/src/app/shared/form/form.component.html b/src/app/shared/form/form.component.html new file mode 100644 index 0000000000..e760c0111e --- /dev/null +++ b/src/app/shared/form/form.component.html @@ -0,0 +1,61 @@ +
+
+ + + + + + +
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+ +
+
+
+ +
+ + +
+
+
+ +
+ +
diff --git a/src/app/shared/form/form.component.scss b/src/app/shared/form/form.component.scss new file mode 100644 index 0000000000..0f8145f262 --- /dev/null +++ b/src/app/shared/form/form.component.scss @@ -0,0 +1,23 @@ +@import "../../../styles/_variables.scss"; + +.ds-form-input-addon { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right: 0; +} + +.ds-form-input-btn { + border: $input-btn-border-width solid $input-border-color; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-left: 0; +} + +.ds-form-input-btn:focus { + box-shadow: none !important; +} + +.ds-form-input-value { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} diff --git a/src/app/shared/form/form.component.spec.ts b/src/app/shared/form/form.component.spec.ts new file mode 100644 index 0000000000..a0489a5ab8 --- /dev/null +++ b/src/app/shared/form/form.component.spec.ts @@ -0,0 +1,175 @@ +// Load the implementations that should be tested +import { CommonModule } from '@angular/common'; + +import { + Component, + CUSTOM_ELEMENTS_SCHEMA, + DebugElement +} from '@angular/core'; + +import { + async, + ComponentFixture, + inject, + TestBed, +} from '@angular/core/testing'; + +import { StoreModule } from '@ngrx/store'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import Spy = jasmine.Spy; + +import { FormComponent } from './form.component'; +import { FormService } from './form.service'; +import { DynamicFormControlModel, DynamicFormValidationService, DynamicInputModel } from '@ng-dynamic-forms/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { FormBuilderService } from './builder/form-builder.service'; +import { SubmissionFormsConfigService } from '../../core/config/submission-forms-config.service'; +import { ResponseCacheService } from '../../core/cache/response-cache.service'; +import { RequestService } from '../../core/data/request.service'; +import { ObjectCacheService } from '../../core/cache/object-cache.service'; +import { Observable } from 'rxjs/Observable'; + +function createTestComponent(html: string, type: { new(...args: any[]): T }): ComponentFixture { + TestBed.overrideComponent(type, { + set: { template: html } + }); + const fixture = TestBed.createComponent(type); + + fixture.detectChanges(); + return fixture as ComponentFixture; +} + +export const TEST_FORM_MODEL = [ + + new DynamicInputModel( + { + id: 'dc_title', + label: 'Title', + placeholder: 'Title', + validators: { + required: null + }, + errorMessages: { + required: 'You must enter a main title for this item.' + } + } + ), + + new DynamicInputModel( + { + id: 'dc_title_alternative', + label: 'Other Titles', + placeholder: 'Other Titles', + } + ), + + new DynamicInputModel( + { + id: 'dc_publisher', + label: 'Publisher', + placeholder: 'Publisher', + } + ), + + new DynamicInputModel( + { + id: 'dc_identifier_citation', + label: 'Citation', + placeholder: 'Citation', + } + ), + + new DynamicInputModel( + { + id: 'dc_identifier_issn', + label: 'Identifiers', + placeholder: 'Identifiers', + } + ), +]; + +export const TEST_FORM_GROUP = { + dc_title: new FormControl(), + dc_title_alternative: new FormControl(), + dc_publisher: new FormControl(), + dc_identifier_citation: new FormControl(), + dc_identifier_issn: new FormControl() +} + +describe('Form component', () => { + + let testComp: TestComponent; + let testFixture: ComponentFixture; + let html; + const formServiceStub = { + getFormData: (formId) => Observable.of([]) + } + const formBuilderServiceStub = { + createFormGroup: (formModel) => new FormGroup(TEST_FORM_GROUP) + } + const submissionFormsConfigServiceStub = { } + + // async beforeEach + beforeEach(async(() => { + + TestBed.configureTestingModule({ + imports: [ + BrowserModule, + CommonModule, + FormsModule, + ReactiveFormsModule, + StoreModule.forRoot({}), + NgbModule.forRoot(), + ], + declarations: [ + FormComponent, + TestComponent, + ], // declare the test component + providers: [ + FormComponent, + { provide: FormService, useValue: formServiceStub }, + { provide: FormBuilderService, useValue: formBuilderServiceStub }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }); + + })); + + // synchronous beforeEach + beforeEach(() => { + html = ` + `; + + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + + }); + + it('should create Form Component', inject([FormComponent], (app: FormComponent) => { + expect(app).toBeDefined(); + })); + +}); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + + public formId; + public formModel: DynamicFormControlModel[]; + + constructor() { + this.formId = 'testForm'; + this.formModel = TEST_FORM_MODEL; + } + +} diff --git a/src/app/shared/form/form.component.ts b/src/app/shared/form/form.component.ts new file mode 100644 index 0000000000..47258315e2 --- /dev/null +++ b/src/app/shared/form/form.component.ts @@ -0,0 +1,274 @@ +import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { AbstractControl, FormArray, FormControl, FormGroup } from '@angular/forms'; + +import { + DynamicFormArrayModel, + DynamicFormControlEvent, + DynamicFormControlModel, + DynamicFormGroupModel, +} from '@ng-dynamic-forms/core'; +import { Store } from '@ngrx/store'; + +import { AppState } from '../../app.reducer'; +import { FormChangeAction, FormInitAction, FormRemoveAction, FormStatusChangeAction } from './form.actions'; +import { FormBuilderService } from './builder/form-builder.service'; +import { Observable } from 'rxjs/Observable'; +import { Subscription } from 'rxjs/Subscription'; +import { hasValue, isNotNull, isNull } from '../empty.util'; +import { FormService } from './form.service'; +import { formObjectFromIdSelector } from './selectors'; +import { FormEntry, FormError } from './form.reducers'; +import { isEmpty } from 'lodash'; + +/** + * The default form component. + */ +@Component({ + exportAs: 'formComponent', + selector: 'ds-form', + styleUrls: ['form.component.scss'], + templateUrl: 'form.component.html', +}) +export class FormComponent implements OnDestroy, OnInit { + + private formValid: boolean; + + /** + * A boolean that indicate if to display form's submit and cancel buttons + */ + @Input() displaySubmit = true; + + /** + * The form unique ID + */ + @Input() formId: string; + + /** + * An array of DynamicFormControlModel type + */ + @Input() formModel: DynamicFormControlModel[]; + @Input() parentFormModel: DynamicFormGroupModel | DynamicFormGroupModel[]; + @Input() formGroup: FormGroup; + + /* tslint:disable:no-output-rename */ + @Output('dfBlur') blur: EventEmitter = new EventEmitter(); + @Output('dfChange') change: EventEmitter = new EventEmitter(); + @Output('dfFocus') focus: EventEmitter = new EventEmitter(); + /* tslint:enable:no-output-rename */ + @Output() addArrayItem: EventEmitter = new EventEmitter(); + @Output() removeArrayItem: EventEmitter = new EventEmitter(); + + /** + * An event fired when form is valid and submitted . + * Event's payload equals to the form content. + */ + @Output() submit: EventEmitter> = new EventEmitter>(); + + /** + * An object of FormGroup type + */ + // public formGroup: FormGroup; + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + private subs: Subscription[] = []; + + constructor(private formService: FormService, + protected changeDetectorRef: ChangeDetectorRef, + private formBuilderService: FormBuilderService, + private store: Store) { + } + + /** + * Method provided by Angular. Invoked after the view has been initialized. + */ + + /*ngAfterViewChecked(): void { + this.subs.push(this.formGroup.valueChanges + .filter((formGroup) => this.formGroup.dirty) + .subscribe(() => { + // Dispatch a FormChangeAction if the user has changed the value in the UI + this.store.dispatch(new FormChangeAction(this.formId, this.formGroup.value)); + this.formGroup.markAsPristine(); + })); + }*/ + + private getFormGroup(): FormGroup { + if (!!this.parentFormModel) { + return this.formGroup.parent as FormGroup; + } + + return this.formGroup; + } + + private getFormGroupValue() { + return this.getFormGroup().value; + } + + private getFormGroupValidStatus() { + return this.getFormGroup().valid; + } + + /** + * Method provided by Angular. Invoked after the constructor + */ + ngOnInit() { + if (!this.formGroup) { + this.formGroup = this.formBuilderService.createFormGroup(this.formModel); + } else { + this.formModel.forEach((model) => { + this.formBuilderService.addFormGroupControl(this.formGroup, this.parentFormModel, model); + }); + } + + this.store.dispatch(new FormInitAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel), this.getFormGroupValidStatus())); + + // TODO: take a look to the following method: + // this.keepSync(); + + this.formValid = this.getFormGroupValidStatus(); + + this.subs.push(this.formGroup.statusChanges + .filter((currentStatus) => this.formValid !== this.getFormGroupValidStatus()) + .subscribe((currentStatus) => { + // Dispatch a FormStatusChangeAction if the form status has changed + this.store.dispatch(new FormStatusChangeAction(this.formId, this.getFormGroupValidStatus())); + this.formValid = this.getFormGroupValidStatus(); + })); + + this.subs.push( + this.store.select(formObjectFromIdSelector(this.formId)) + .filter((formState: FormEntry) => !!formState && !isEmpty(formState.errors)) + .map((formState) => formState.errors) + .distinctUntilChanged() + .delay(100) // this terrible delay is here to prevent the detection change error + .subscribe((errors: FormError[]) => { + const {formGroup, formModel} = this; + + errors.forEach((error: FormError) => { + const {fieldId} = error; + let field: AbstractControl; + if (!!this.parentFormModel) { + field = this.formBuilderService.getFormControlById(fieldId, formGroup.parent as FormGroup, formModel); + } else { + field = this.formBuilderService.getFormControlById(fieldId, formGroup, formModel); + } + + if (field) { + const model: DynamicFormControlModel = this.formBuilderService.findById(fieldId, formModel); + this.formService.addErrorToField(field, model, error.message); + } + }); + + this.changeDetectorRef.detectChanges(); + }) + ); + } + + /** + * Method provided by Angular. Invoked when the instance is destroyed + */ + ngOnDestroy() { + this.store.dispatch(new FormRemoveAction(this.formId)); + this.subs + .filter((sub) => hasValue(sub)) + .forEach((sub) => sub.unsubscribe()); + } + + /** + * Method to check if the form status is valid or not + */ + public isValid(): Observable { + return this.formService.isValid(this.formId) + } + + /** + * Method to keep synchronized form controls values with form state + */ + private keepSync() { + this.subs.push(this.formService.getFormData(this.formId) + .subscribe((stateFormData) => { + if (!Object.is(stateFormData, this.formGroup.value) && this.formGroup) { + this.formGroup.setValue(stateFormData); + } + })); + } + + onBlur(event) { + this.blur.emit(event); + } + + onFocus(event) { + this.focus.emit(event); + } + + onChange(event) { + const action: FormChangeAction = new FormChangeAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel)); + + this.store.dispatch(action); + this.formGroup.markAsPristine(); + + this.change.emit(event); + const control: FormControl = event.control; + + control.setErrors(null); + } + + /** + * Method called on submit. + * Emit a new submit Event whether the form is valid, mark fields with error otherwise + */ + onSubmit() { + if (this.getFormGroupValidStatus()) { + this.submit.emit(this.formService.getFormData(this.formId)); + } else { + this.formService.validateAllFormFields(this.formGroup); + } + } + + /** + * Method to reset form fields + */ + reset() { + this.formGroup.reset(); + } + + isItemReadOnly(arrayContext: DynamicFormArrayModel, index: number): boolean { + const context = arrayContext.groups[index]; + const model = context.group[0] as any; + return model.readOnly; + } + + removeItem($event, arrayContext: DynamicFormArrayModel, index: number) { + const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray; + this.removeArrayItem.emit(this.getEvent($event, arrayContext, index, 'remove')); + this.formBuilderService.removeFormArrayGroup(index, formArrayControl, arrayContext); + this.store.dispatch(new FormChangeAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel))); + } + + insertItem($event, arrayContext: DynamicFormArrayModel, index: number) { + const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray; + this.formBuilderService.insertFormArrayGroup(index, formArrayControl, arrayContext); + this.addArrayItem.emit(this.getEvent($event, arrayContext, index, 'add')); + this.store.dispatch(new FormChangeAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel))); + } + + protected getEvent($event: any, arrayContext: DynamicFormArrayModel, index: number, type: string): DynamicFormControlEvent { + const context = arrayContext.groups[index]; + const itemGroupModel = context.context; + let group = this.formGroup.get(itemGroupModel.id) as FormGroup; + if (isNull(group)) { + for (const key of Object.keys(this.formGroup.controls)) { + group = this.formGroup.controls[key].get(itemGroupModel.id) as FormGroup; + if (isNotNull(group)) { + break; + } + } + } + const model = context.group[0] as DynamicFormControlModel; + const control = group.controls[index] as FormControl; + return {$event, context, control, group, model, type}; + } +} diff --git a/src/app/shared/form/form.effects.ts b/src/app/shared/form/form.effects.ts new file mode 100644 index 0000000000..078f658300 --- /dev/null +++ b/src/app/shared/form/form.effects.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@angular/core'; +import { Actions, Effect } from '@ngrx/effects'; + +@Injectable() +export class FormEffects { + + constructor(private actions$: Actions) { + + } + +} diff --git a/src/app/shared/form/form.reducers.ts b/src/app/shared/form/form.reducers.ts new file mode 100644 index 0000000000..04d7d28d85 --- /dev/null +++ b/src/app/shared/form/form.reducers.ts @@ -0,0 +1,179 @@ +import { + FormAction, FormActionTypes, FormAddError, FormChangeAction, FormInitAction, FormRemoveAction, + FormStatusChangeAction +} from './form.actions'; +import { hasValue } from '../empty.util'; +import { uniqWith, isEqual } from 'lodash'; + +export interface FormError { + message: string; + fieldId: string; +} + +export interface FormEntry { + data: any; + valid: boolean; + errors: FormError[]; +} + +export interface FormState { + [formId: string]: FormEntry; +} + +const initialState: FormState = Object.create(null); + +export function formReducer(state = initialState, action: FormAction): FormState { + switch (action.type) { + + case FormActionTypes.FORM_INIT: { + return initForm(state, action as FormInitAction); + } + + case FormActionTypes.FORM_CHANGE: { + return changeDataForm(state, action as FormChangeAction); + } + + case FormActionTypes.FORM_REMOVE: { + return removeForm(state, action as FormRemoveAction); + } + + case FormActionTypes.FORM_STATUS_CHANGE: { + return changeStatusForm(state, action as FormStatusChangeAction); + } + + case FormActionTypes.FORM_ADD_ERROR: { + return addFormErrors(state, action as FormAddError) + } + + default: { + return state; + } + } +} + +function addFormErrors(state: FormState, action: FormAddError) { + const formId = action.payload.formId; + if (hasValue(state[formId])) { + const error: FormError = { + fieldId: action.payload.fieldId, + message: action.payload.errorMessage + }; + + return Object.assign({}, state, { + [ formId ]: { + data: state[formId].data, + valid: state[formId].valid, + errors: state[formId].errors ? uniqWith(state[formId].errors.concat(error), isEqual) : [].concat(error), + } + }); + } else { + return state; + } +} + +/** + * Init form state. + * + * @param state + * the current state + * @param action + * an FormInitAction + * @return FormState + * the new state, with the form initialized. + */ +function initForm(state: FormState, action: FormInitAction): FormState { + if (!hasValue(state[ action.payload.formId ])) { + return Object.assign({}, state, { + [ action.payload.formId ]: { + data: action.payload.formData, + valid: action.payload.valid + } + }); + } else { + const newState = Object.assign({}, state); + newState[ action.payload.formId ] = Object.assign({}, newState[ action.payload.formId ], { + data: action.payload.formData, + valid: action.payload.valid + } + ); + return newState; + } +} + +/** + * Set form data. + * + * @param state + * the current state + * @param action + * an FormChangeAction + * @return FormState + * the new state, with the data changed. + */ +function changeDataForm(state: FormState, action: FormChangeAction): FormState { + if (!hasValue(state[ action.payload.formId ])) { + return Object.assign({}, state, { + [ action.payload.formId ]: { + data: action.payload.formData, + valid: state[ action.payload.formId ].valid + } + }); + } else { + const newState = Object.assign({}, state); + newState[ action.payload.formId ] = Object.assign({}, newState[ action.payload.formId ], { + data: action.payload.formData, + valid: state[ action.payload.formId ].valid + } + ); + return newState; + } +} + +/** + * Set form status. + * + * @param state + * the current state + * @param action + * an FormStatusChangeAction + * @return FormState + * the new state, with the status changed. + */ +function changeStatusForm(state: FormState, action: FormStatusChangeAction): FormState { + if (!hasValue(state[ action.payload.formId ])) { + return Object.assign({}, state, { + [ action.payload.formId ]: { + data: state[ action.payload.formId ].data, + valid: action.payload.valid + } + }); + } else { + const newState = Object.assign({}, state); + newState[ action.payload.formId ] = Object.assign({}, newState[ action.payload.formId ], { + data: state[ action.payload.formId ].data, + valid: action.payload.valid + } + ); + return newState; + } +} + +/** + * Remove form state. + * + * @param state + * the current state + * @param action + * an FormRemoveAction + * @return FormState + * the new state, with the form initialized. + */ +function removeForm(state: FormState, action: FormRemoveAction): FormState { + if (hasValue(state[ action.payload.formId ])) { + const newState = Object.assign({}, state); + delete newState[ action.payload.formId ]; + return newState; + } else { + return state; + } +} diff --git a/src/app/shared/form/form.service.ts b/src/app/shared/form/form.service.ts new file mode 100644 index 0000000000..9d7b18acbc --- /dev/null +++ b/src/app/shared/form/form.service.ts @@ -0,0 +1,118 @@ +import { Injectable } from '@angular/core'; +import { AbstractControl, FormControl, FormGroup } from '@angular/forms'; +import { Observable } from 'rxjs/Observable'; +import { Store } from '@ngrx/store'; + +import { AppState } from '../../app.reducer'; +import { formObjectFromIdSelector } from './selectors'; +import { FormBuilderService } from './builder/form-builder.service'; +import { DynamicFormControlModel, DynamicFormGroupModel } from '@ng-dynamic-forms/core'; +import { isNotEmpty, isNotUndefined } from '../empty.util'; +import { find, uniqueId } from 'lodash'; +import { FormChangeAction } from './form.actions'; + +@Injectable() +export class FormService { + + constructor(private formBuilderService: FormBuilderService, + private store: Store) { + } + + /** + * Method to retrieve form's status from state + */ + public isValid(formId: string): Observable { + return this.store.select(formObjectFromIdSelector(formId)) + .filter((state) => isNotUndefined(state)) + .map((state) => state.valid) + .distinctUntilChanged(); + } + + /** + * Method to retrieve form's data from state + */ + public getFormData(formId: string): Observable { + return this.store.select(formObjectFromIdSelector(formId)) + .filter((state) => isNotUndefined(state)) + .map((state) => state.data) + .distinctUntilChanged(); + } + + /** + * Method to retrieve form's data from state + */ + public isFormInitialized(formId: string): Observable { + return this.store.select(formObjectFromIdSelector(formId)) + .distinctUntilChanged() + .map((state) => isNotUndefined(state)); + } + + public getUniqueId(formId): string { + return uniqueId() + '_' + formId; + } + + /** + * Method to validate form's fields + */ + public validateAllFormFields(formGroup: FormGroup) { + Object.keys(formGroup.controls).forEach((field) => { + const control = formGroup.get(field); + if (control instanceof FormControl) { + control.markAsTouched({onlySelf: true}); + } else if (control instanceof FormGroup) { + this.validateAllFormFields(control); + } + }); + } + + public setValue(formGroup: FormGroup, fieldModel: DynamicFormControlModel, fieldId: string, value: any) { + if (isNotEmpty(fieldModel)) { + const path = this.formBuilderService.getPath(fieldModel); + const fieldControl = formGroup.get(path); + fieldControl.markAsDirty(); + fieldControl.setValue(value); + } + } + + public addErrorToField(field: AbstractControl, model: DynamicFormControlModel, message: string) { + const errorFound = !!(find(field.errors, (err) => err === message)); + + // search for the same error in the formControl.errors property + if (!errorFound) { + const errorKey = uniqueId('error-'); // create a single key for the error + const error = {}; // create the error object + + error[errorKey] = message; // assign message + + // if form control model has errorMessages object, create it + if (!model.errorMessages) { + model.errorMessages = {}; + } + + // put the error in the form control model + model.errorMessages[errorKey] = message; + + // Use correct error messages from the model + const lastArray = message.split('.'); + if (lastArray && lastArray.length > 0) { + const last = lastArray[lastArray.length - 1]; + const modelMsg = model.errorMessages[last]; + if (modelMsg && modelMsg.length > 0) { + model.errorMessages[errorKey] = modelMsg; + } + } + + // add the error in the form control + field.setErrors(error); + + // formGroup.markAsDirty(); + field.markAsTouched(); + } + } + + public resetForm(formGroup: FormGroup, groupModel: DynamicFormControlModel[], formId: string) { + this.formBuilderService.clearAllModelsValue(groupModel); + formGroup.reset(); + this.store.dispatch(new FormChangeAction(formId, formGroup.value)); + } +} diff --git a/src/app/shared/form/selectors.ts b/src/app/shared/form/selectors.ts new file mode 100644 index 0000000000..a4f27945ef --- /dev/null +++ b/src/app/shared/form/selectors.ts @@ -0,0 +1,10 @@ +import { createSelector, MemoizedSelector, Selector } from '@ngrx/store'; + +import { AppState } from '../../app.reducer'; +import { FormEntry, FormState } from './form.reducers'; + +export const formStateSelector = (state: AppState) => state.forms; + +export function formObjectFromIdSelector(formId: string): MemoizedSelector { + return createSelector(formStateSelector, (forms: FormState) => forms[formId]); +} diff --git a/src/app/shared/number-picker/number-picker.component.html b/src/app/shared/number-picker/number-picker.component.html new file mode 100644 index 0000000000..5eb45a0bfb --- /dev/null +++ b/src/app/shared/number-picker/number-picker.component.html @@ -0,0 +1,33 @@ +
+ + + +
diff --git a/src/app/shared/number-picker/number-picker.component.scss b/src/app/shared/number-picker/number-picker.component.scss new file mode 100644 index 0000000000..33ed45b295 --- /dev/null +++ b/src/app/shared/number-picker/number-picker.component.scss @@ -0,0 +1,44 @@ +.ngb-tp { + display: flex; + align-items: center; +} +.ngb-tp-hour, .ngb-tp-minute, .ngb-tp-second, .ngb-tp-meridian { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-around; +} +.ngb-tp-spacer { + width: 1em; + text-align: center; +} +.chevron::before { + border-style: solid; + border-width: 0.29em 0.29em 0 0; + content: ''; + display: inline-block; + height: 0.69em; + left: 0.05em; + position: relative; + top: 0.15em; + transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); + -ms-transform: rotate(-45deg); + vertical-align: middle; + width: 0.71em; +} +.chevron.bottom:before { + top: -.3em; + -webkit-transform: rotate(135deg); + -ms-transform: rotate(135deg); + transform: rotate(135deg); +} +input { + text-align: center; + display: inline-block; + max-width: 80px !important; +} + +//.error { +// border-color: red; +//} diff --git a/src/app/shared/number-picker/number-picker.component.ts b/src/app/shared/number-picker/number-picker.component.ts new file mode 100644 index 0000000000..67b5107b79 --- /dev/null +++ b/src/app/shared/number-picker/number-picker.component.ts @@ -0,0 +1,147 @@ +import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, SimpleChanges, } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; + +@Component({ + selector: 'ds-number-picker', + styleUrls: ['./number-picker.component.scss'], + templateUrl: './number-picker.component.html', + providers: [ + {provide: NG_VALUE_ACCESSOR, useExisting: NumberPickerComponent, multi: true} + ], +}) + +export class NumberPickerComponent implements OnInit, ControlValueAccessor { + @Output() + selected = new EventEmitter(); + @Output() + remove = new EventEmitter(); + @Output() + change = new EventEmitter(); + + @Input() + step: number; + @Input() + min: number; + @Input() + max: number; + @Input() + size: number; + @Input() + placeholder: string; + @Input() + name: string; + @Input() + disabled: boolean; + @Input() + invalid: boolean; + @Input() + value: number; + lastValue: number; + + constructor(private fb: FormBuilder, private cd: ChangeDetectorRef) { + } + + ngOnInit() { + // this.lastValue = this.value; + this.step = this.step || 1; + this.min = this.min || 0; + this.max = this.max || 100; + this.disabled = this.disabled || false; + this.invalid = this.invalid || false; + this.cd.detectChanges(); + } + + ngOnChanges(changes: SimpleChanges) { + if (this.value) { + if (changes.max) { + // When the user select a month with < # of days + this.value = this.value > this.max ? this.max : this.value; + } + + } else if (changes.value && changes.value.currentValue === null) { + // When the user delete the inserted value + this.value = null; + } else if (changes.invalid) { + this.invalid = changes.invalid.currentValue; + } + } + + increment(reverse?: boolean) { + // First after init + if (!this.value) { + this.value = this.lastValue; + } else { + this.lastValue = this.value; + + let newValue = this.value; + if (reverse) { + newValue -= this.step; + } else { + newValue += this.step; + } + + if (newValue >= this.min && newValue <= this.max) { + this.value = newValue; + } else { + if (newValue > this.max) { + this.value = this.min; + } else { + this.value = this.max; + } + } + } + + this.emitChange(); + } + + decrement() { + this.increment(true); + } + + update(event) { + try { + const i = Number.parseInt(event.target.value); + + if (i >= this.min && i <= this.max) { + this.value = i; + this.emitChange(); + } else if (event.target.value === null || event.target.value === '') { + this.value = null; + this.emitChange(); + } else { + this.value = this.lastValue; + this.emitChange(); + } + } catch (e) { + this.value = this.lastValue; + this.emitChange(); + } + } + + onFocus() { + if (this.value) { + this.lastValue = this.value; + } + } + + writeValue(value) { + if (this.lastValue) { + this.lastValue = this.value; + this.value = value; + } else { + // First init + this.lastValue = value; + } + } + + registerOnChange(fn) { + // this.change = fn; + } + + registerOnTouched(fn) { + } + + emitChange() { + this.change.emit({field: this.name, value: this.value}); + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index c78c218fa9..81503f2245 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -3,15 +3,20 @@ import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbDatepickerModule, NgbModule, NgbTimepickerModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; import { NgxPaginationModule } from 'ngx-pagination'; +import { FileUploadModule } from 'ng2-file-upload'; + +import { InfiniteScrollModule } from 'ngx-infinite-scroll'; + import { EnumKeysPipe } from './utils/enum-keys-pipe'; import { FileSizePipe } from './utils/file-size-pipe'; import { SafeUrlPipe } from './utils/safe-url-pipe'; +import { ConsolePipe } from './utils/console.pipe'; import { CollectionListElementComponent } from './object-list/collection-list-element/collection-list-element.component'; import { CommunityListElementComponent } from './object-list/community-list-element/community-list-element.component'; @@ -20,11 +25,11 @@ import { SearchResultListElementComponent } from './object-list/search-result-li import { WrapperListElementComponent } from './object-list/wrapper-list-element/wrapper-list-element.component'; import { ObjectListComponent } from './object-list/object-list.component'; -import { CollectionGridElementComponent} from './object-grid/collection-grid-element/collection-grid-element.component' -import { CommunityGridElementComponent} from './object-grid/community-grid-element/community-grid-element.component' -import { ItemGridElementComponent} from './object-grid/item-grid-element/item-grid-element.component' -import { AbstractListableElementComponent} from './object-collection/shared/object-collection-element/abstract-listable-element.component' -import { WrapperGridElementComponent} from './object-grid/wrapper-grid-element/wrapper-grid-element.component' +import { CollectionGridElementComponent} from './object-grid/collection-grid-element/collection-grid-element.component'; +import { CommunityGridElementComponent} from './object-grid/community-grid-element/community-grid-element.component'; +import { ItemGridElementComponent} from './object-grid/item-grid-element/item-grid-element.component'; +import { AbstractListableElementComponent} from './object-collection/shared/object-collection-element/abstract-listable-element.component'; +import { WrapperGridElementComponent} from './object-grid/wrapper-grid-element/wrapper-grid-element.component'; import { ObjectGridComponent } from './object-grid/object-grid.component'; import { ObjectCollectionComponent } from './object-collection/object-collection.component'; import { ComcolPageContentComponent } from './comcol-page-content/comcol-page-content.component'; @@ -32,7 +37,6 @@ import { ComcolPageHeaderComponent } from './comcol-page-header/comcol-page-head import { ComcolPageLogoComponent } from './comcol-page-logo/comcol-page-logo.component'; import { ErrorComponent } from './error/error.component'; import { LoadingComponent } from './loading/loading.component'; - import { PaginationComponent } from './pagination/pagination.component'; import { ThumbnailComponent } from '../thumbnail/thumbnail.component'; import { SearchFormComponent } from './search-form/search-form.component'; @@ -40,21 +44,47 @@ import { SearchResultGridElementComponent } from './object-grid/search-result-gr import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component'; import { GridThumbnailComponent } from './object-grid/grid-thumbnail/grid-thumbnail.component'; import { VarDirective } from './utils/var.directive'; +import { FormComponent } from './form/form.component'; +import { DsDynamicTypeaheadComponent } from './form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component'; +import { DsDynamicScrollableDropdownComponent } from './form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component'; +import { DsDynamicFormControlComponent } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component'; +import { DsDynamicFormComponent } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form.component'; +import { DynamicFormsCoreModule } from '@ng-dynamic-forms/core'; +import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap'; +import { TextMaskModule } from 'angular2-text-mask'; import { DragClickDirective } from './utils/drag-click.directive'; import { TruncatePipe } from './utils/truncate.pipe'; import { TruncatableComponent } from './truncatable/truncatable.component'; import { TruncatableService } from './truncatable/truncatable.service'; import { TruncatablePartComponent } from './truncatable/truncatable-part/truncatable-part.component'; +import { UploaderComponent } from './uploader/uploader.component'; +import { ChipsComponent } from './chips/chips.component'; +import { DsDynamicTagComponent } from './form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component'; +import { DsDynamicListComponent } from './form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component'; +import { DsDynamicGroupComponent } from './form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.components'; +import { SortablejsModule } from 'angular-sortablejs'; +import { NumberPickerComponent } from './number-picker/number-picker.component'; +import { DsDatePickerComponent } from './form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component'; +import { DsDynamicLookupComponent } from './form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here CommonModule, + SortablejsModule, + DynamicFormsCoreModule, + DynamicFormsNGBootstrapUIModule, + FileUploadModule, FormsModule, + InfiniteScrollModule, NgbModule, + NgbDatepickerModule, + NgbTimepickerModule, + NgbTypeaheadModule, NgxPaginationModule, ReactiveFormsModule, RouterModule, - TranslateModule + TranslateModule, + TextMaskModule ]; const PIPES = [ @@ -62,16 +92,29 @@ const PIPES = [ EnumKeysPipe, FileSizePipe, SafeUrlPipe, - TruncatePipe + TruncatePipe, + ConsolePipe ]; const COMPONENTS = [ // put shared components here + ChipsComponent, ComcolPageContentComponent, ComcolPageHeaderComponent, ComcolPageLogoComponent, + DsDynamicFormComponent, + DsDynamicFormControlComponent, + DsDynamicListComponent, + DsDynamicLookupComponent, + DsDynamicScrollableDropdownComponent, + DsDynamicTagComponent, + DsDynamicTypeaheadComponent, + DsDynamicGroupComponent, + DsDatePickerComponent, ErrorComponent, + FormComponent, LoadingComponent, + NumberPickerComponent, ObjectListComponent, AbstractListableElementComponent, WrapperListElementComponent, @@ -82,6 +125,7 @@ const COMPONENTS = [ SearchFormComponent, ThumbnailComponent, GridThumbnailComponent, + UploaderComponent, WrapperListElementComponent, ViewModeSwitchComponent, TruncatableComponent, diff --git a/src/app/shared/uploader/uploader-options.model.ts b/src/app/shared/uploader/uploader-options.model.ts new file mode 100644 index 0000000000..0bd6412b17 --- /dev/null +++ b/src/app/shared/uploader/uploader-options.model.ts @@ -0,0 +1,13 @@ + +export class UploaderOptions { + /** + * URL of the REST endpoint for file upload. + */ + url: string; + + authToken: string; + + disableMultipart = false; + + itemAlias: string; +} diff --git a/src/app/shared/uploader/uploader.component.html b/src/app/shared/uploader/uploader.component.html new file mode 100644 index 0000000000..f01b1f7a78 --- /dev/null +++ b/src/app/shared/uploader/uploader.component.html @@ -0,0 +1,51 @@ +
+
+
+
+

{{dropOverDocumentMsg | translate}}

+
+
+
+
+
+

+ {{dropMsg | translate}} {{'uploader.or' | translate}} + + +

+
+
+
+ {{'uploader.queue-lenght' | translate}}: {{ uploader?.queue?.length }} | {{ uploader?.queue[0]?.file.name }} +
+ +
+ {{ uploader.progress }}% + {{'uploader.processing' | translate}}... +
+
+
+
+
+
+
+
+
diff --git a/src/app/shared/uploader/uploader.component.scss b/src/app/shared/uploader/uploader.component.scss new file mode 100644 index 0000000000..f0b54bfe15 --- /dev/null +++ b/src/app/shared/uploader/uploader.component.scss @@ -0,0 +1,40 @@ +@import '../../../styles/_variables.scss'; + +.ds-base-drop-zone { + border: 2px dashed $gray-600; +} + +/* Default class applied to drop zones on over */ +.ds-base-drop-zone-file-over { + border: 2px dashed map-get($theme-colors, primary); +} + +.ds-base-drop-zone p { + height: 42px; +} + +.ds-document-drop-zone { + top: 0; + left: 0; + z-index: -1; +} + +.ds-document-drop-zone-active { + z-index: 1025 !important; +} + +.ds-document-drop-zone-inner { + background-color: rgba($white, 0.7); + z-index: 1021; + top: 0; + left: 0; +} + +.ds-document-drop-zone-inner-content { + border: 4px dashed map-get($theme-colors, primary); + z-index: 1021; +} + +.ds-document-drop-zone-inner-content p { + font-size: ($font-size-lg * 2.5); +} diff --git a/src/app/shared/uploader/uploader.component.ts b/src/app/shared/uploader/uploader.component.ts new file mode 100644 index 0000000000..476ba510e0 --- /dev/null +++ b/src/app/shared/uploader/uploader.component.ts @@ -0,0 +1,168 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + HostListener, + Input, + Output, + ViewEncapsulation, +} from '@angular/core' + +import { FileUploader } from 'ng2-file-upload'; +import { Observable } from 'rxjs/Observable'; +import { uniqueId } from 'lodash'; +import { ScrollToConfigOptions, ScrollToService } from '@nicky-lenaers/ngx-scroll-to'; + +import { UploaderOptions } from './uploader-options.model'; +import { isNotEmpty, isUndefined } from '../empty.util'; +import { UploaderService } from './uploader.service'; + +@Component({ + selector: 'ds-uploader', + templateUrl: 'uploader.component.html', + styleUrls: ['uploader.component.scss'], + changeDetection: ChangeDetectionStrategy.Default, + encapsulation: ViewEncapsulation.Emulated +}) + +export class UploaderComponent { + + /** + * The message to show when drag files on the drop zone + */ + @Input() dropMsg: string; + + /** + * The message to show when drag files on the window document + */ + @Input() dropOverDocumentMsg: string; + + /** + * The message to show when drag files on the window document + */ + @Input() enableDragOverDocument: boolean; + + /** + * The function to call before an upload + */ + @Input() onBeforeUpload: () => void; + + /** + * Configuration for the ng2-file-upload component. + */ + @Input() uploadFilesOptions: UploaderOptions; + + /** + * The function to call when upload is completed + */ + @Output() onCompleteItem: EventEmitter = new EventEmitter(); + + public uploader: FileUploader; + public uploaderId: string; + public isOverBaseDropZone = Observable.of(false); + public isOverDocumentDropZone = Observable.of(false); + + @HostListener('window:dragover', ['$event']) + onDragOver(event: any) { + + if (this.enableDragOverDocument && this.uploaderService.isAllowedDragOverPage()) { + // Show drop area on the page + event.preventDefault(); + if ((event.target as any).tagName !== 'HTML') { + this.isOverDocumentDropZone = Observable.of(true); + } + } + } + + constructor(private cdr: ChangeDetectorRef, private scrollToService: ScrollToService, private uploaderService: UploaderService) { + } + + /** + * Method provided by Angular. Invoked after the constructor. + */ + ngOnInit() { + this.uploaderId = 'ds-drag-and-drop-uploader' + uniqueId(); + this.checkConfig(this.uploadFilesOptions); + this.uploader = new FileUploader({ + url: this.uploadFilesOptions.url, + authToken: this.uploadFilesOptions.authToken, + disableMultipart: this.uploadFilesOptions.disableMultipart, + itemAlias: this.uploadFilesOptions.itemAlias, + removeAfterUpload: true, + autoUpload: true + }); + + if (isUndefined(this.enableDragOverDocument)) { + this.enableDragOverDocument = false; + } + if (isUndefined(this.dropMsg)) { + this.dropMsg = 'uploader.drag-message'; + } + if (isUndefined(this.dropOverDocumentMsg)) { + this.dropOverDocumentMsg = 'uploader.drag-message'; + } + } + + ngAfterViewInit() { + // Maybe to remove: needed to avoid CORS issue with our temp upload server + this.uploader.onAfterAddingFile = ((item) => { + item.withCredentials = false; + }); + this.uploader.onBeforeUploadItem = () => { + this.onBeforeUpload(); + this.isOverDocumentDropZone = Observable.of(false); + + // Move page target to the uploader + const config: ScrollToConfigOptions = { + target: this.uploaderId + }; + this.scrollToService.scrollTo(config); + }; + this.uploader.onCompleteItem = (item: any, response: any, status: any, headers: any) => { + if (isNotEmpty(response)) { + const responsePath = JSON.parse(response); + this.onCompleteItem.emit(responsePath); + } + }; + this.uploader.onProgressAll = () => this.onProgress(); + this.uploader.onProgressItem = () => this.onProgress(); + } + + /** + * Called when files are dragged on the base drop area. + */ + public fileOverBase(isOver: boolean): void { + this.isOverBaseDropZone = Observable.of(isOver); + } + + /** + * Called when files are dragged on the window document drop area. + */ + public fileOverDocument(isOver: boolean) { + if (!isOver) { + this.isOverDocumentDropZone = Observable.of(isOver); + } + } + + private onProgress() { + this.cdr.detectChanges(); + } + + /** + * Ensure options passed contains the required properties. + * + * @param fileUploadOptions + * The upload-files options object. + */ + private checkConfig(fileUploadOptions: any) { + const required = ['url', 'authToken', 'disableMultipart', 'itemAlias']; + const missing = required.filter((prop) => { + return !((prop in fileUploadOptions) && fileUploadOptions[prop] !== ''); + }); + if (0 < missing.length) { + throw new Error('UploadFiles: Argument is missing the following required properties: ' + missing.join(', ')); + } + } + +} diff --git a/src/app/shared/uploader/uploader.service.ts b/src/app/shared/uploader/uploader.service.ts new file mode 100644 index 0000000000..548de34f9c --- /dev/null +++ b/src/app/shared/uploader/uploader.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; + +@Injectable() +export class UploaderService { + private _overrideDragOverPage = false; + + public overrideDragOverPage() { + this._overrideDragOverPage = true; + } + + public allowDragOverPage() { + this._overrideDragOverPage = false; + } + + public isAllowedDragOverPage(): boolean { + return !this._overrideDragOverPage; + } +} diff --git a/src/app/shared/utils/console.pipe.ts b/src/app/shared/utils/console.pipe.ts new file mode 100644 index 0000000000..fc672a84ae --- /dev/null +++ b/src/app/shared/utils/console.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'dsConsole' +}) +export class ConsolePipe implements PipeTransform { + transform(value: any): string { + console.log(value); + return ''; + } +} diff --git a/yarn.lock b/yarn.lock index bb571e8d6c..77cd9b1e71 100644 --- a/yarn.lock +++ b/yarn.lock @@ -81,6 +81,14 @@ version "1.0.0" resolved "https://registry.yarnpkg.com/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-1.0.0.tgz#8f2ae70db2fe1dcbf5e0acb49dc2b1bbba2be8d2" +"@ng-dynamic-forms/core@5.4.7": + version "5.4.7" + resolved "https://registry.yarnpkg.com/@ng-dynamic-forms/core/-/core-5.4.7.tgz#203dffe4bb31a3599e906990ad9dc2b35714e37a" + +"@ng-dynamic-forms/ui-ng-bootstrap@5.4.7": + version "5.4.7" + resolved "https://registry.yarnpkg.com/@ng-dynamic-forms/ui-ng-bootstrap/-/ui-ng-bootstrap-5.4.7.tgz#66d037a226da96fe84c4dbac98e4dba859c551f8" + "@ngrx/effects@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@ngrx/effects/-/effects-5.1.0.tgz#cef84576b2d0333f19188aedfe156fd301bff70a" @@ -122,6 +130,10 @@ version "2.0.1" resolved "https://registry.yarnpkg.com/@ngx-translate/http-loader/-/http-loader-2.0.1.tgz#aa67788e64bfa8652691a77b022b3b4031209113" +"@nicky-lenaers/ngx-scroll-to@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@nicky-lenaers/ngx-scroll-to/-/ngx-scroll-to-0.6.0.tgz#6d2922f5765a472e3c86499d9e53df5ca210e637" + "@types/acorn@^4.0.3": version "4.0.3" resolved "https://registry.yarnpkg.com/@types/acorn/-/acorn-4.0.3.tgz#d1f3e738dde52536f9aad3d3380d14e448820afd" @@ -421,12 +433,22 @@ angular-idle-preload@2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/angular-idle-preload/-/angular-idle-preload-2.0.4.tgz#7b177c0f52918c090e5c345480b922297cd59a0d" +angular-sortablejs@^2.5.0: + version "2.5.2" + resolved "https://registry.yarnpkg.com/angular-sortablejs/-/angular-sortablejs-2.5.2.tgz#ffd651e47cc93a191db4c023f617db3789fd9af5" + angular2-template-loader@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/angular2-template-loader/-/angular2-template-loader-0.6.2.tgz#c0d44e90fff0fac95e8b23f043acda7fd1c51d7c" dependencies: loader-utils "^0.2.15" +angular2-text-mask@8.0.4: + version "8.0.4" + resolved "https://registry.yarnpkg.com/angular2-text-mask/-/angular2-text-mask-8.0.4.tgz#07e485746cfb9f27e710b27b2785eac4cc4871fc" + dependencies: + text-mask-core "^5.0.0" + angulartics2@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/angulartics2/-/angulartics2-5.2.0.tgz#5bac82d4b6acf798b7db906488861e70b49fe04c" @@ -5587,12 +5609,20 @@ netmask@~1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35" +ng2-file-upload@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ng2-file-upload/-/ng2-file-upload-1.2.1.tgz#5563c5dfd6f43fbfbe815c206e343464a0a6a197" + ngrx-store-freeze@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/ngrx-store-freeze/-/ngrx-store-freeze-0.2.1.tgz#04fb29db33cafda0f2d6ea32adeaac4891b1b27b" dependencies: deep-freeze-strict "^1.1.1" +ngx-infinite-scroll@0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/ngx-infinite-scroll/-/ngx-infinite-scroll-0.8.2.tgz#9cc615c01fbb6307599453c9d9cfb5c1db4fd3e8" + ngx-pagination@3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/ngx-pagination/-/ngx-pagination-3.0.3.tgz#314145263613738d8c544da36cd8dacc5aa89a6f" @@ -8151,6 +8181,10 @@ sort-keys@^1.0.0: dependencies: is-plain-obj "^1.0.0" +sortablejs@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.7.0.tgz#80a2b2370abd568e1cec8c271131ef30a904fa28" + source-list-map@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" @@ -8590,6 +8624,14 @@ term-size@^1.2.0: dependencies: execa "^0.7.0" +text-mask-core@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/text-mask-core/-/text-mask-core-5.0.1.tgz#86db742bdfe3b4c383bb51a3b4ca342c86110639" + +text-mask-core@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/text-mask-core/-/text-mask-core-5.1.1.tgz#a7f65634e11236818fd36a92668e17bf9368f357" + throttleit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" From a1146505643084af922da757a50b661c7df7013a Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 9 May 2018 19:23:09 +0200 Subject: [PATCH 02/41] fixes and adjustments --- src/app/core/core.module.ts | 12 +++++++++++ .../data/config-response-parsing.service.ts | 2 +- .../ds-dynamic-form-control.component.html | 20 ++++++++++++------- .../ds-dynamic-form-control.component.ts | 4 ++-- .../date-picker.component.html} | 0 .../date-picker.component.scss} | 0 .../date-picker.component.ts} | 6 +++--- .../date-picker.model.ts} | 0 .../dynamic-group.component.html | 0 .../dynamic-group.component.scss | 0 .../dynamic-group.components.ts | 1 - .../dynamic-group.model.ts | 0 .../form/builder/form-builder.service.ts | 8 ++++---- .../form/builder/parsers/date-field-parser.ts | 4 ++-- .../builder/parsers/group-field-parser.ts | 2 +- .../shared/form/builder/parsers/row-parser.ts | 2 +- src/app/shared/form/form.component.html | 1 + src/app/shared/form/form.component.ts | 4 +++- src/app/shared/shared.module.ts | 4 ++-- webpack/webpack.server.js | 4 ++++ 20 files changed, 49 insertions(+), 25 deletions(-) rename src/app/shared/form/builder/ds-dynamic-form-ui/models/{ds-date-picker/ds-date-picker.component.html => date-picker/date-picker.component.html} (100%) rename src/app/shared/form/builder/ds-dynamic-form-ui/models/{ds-date-picker/ds-date-picker.component.scss => date-picker/date-picker.component.scss} (100%) rename src/app/shared/form/builder/ds-dynamic-form-ui/models/{ds-date-picker/ds-date-picker.component.ts => date-picker/date-picker.component.ts} (96%) rename src/app/shared/form/builder/ds-dynamic-form-ui/models/{ds-date-picker/ds-date-picker.model.ts => date-picker/date-picker.model.ts} (100%) rename src/app/shared/form/builder/ds-dynamic-form-ui/models/{ds-dynamic-group => dynamic-group}/dynamic-group.component.html (100%) rename src/app/shared/form/builder/ds-dynamic-form-ui/models/{ds-dynamic-group => dynamic-group}/dynamic-group.component.scss (100%) rename src/app/shared/form/builder/ds-dynamic-form-ui/models/{ds-dynamic-group => dynamic-group}/dynamic-group.components.ts (99%) rename src/app/shared/form/builder/ds-dynamic-form-ui/models/{ds-dynamic-group => dynamic-group}/dynamic-group.model.ts (100%) diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 3ae9fd5407..e57f1b45c6 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -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'; @@ -21,6 +22,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'; @@ -39,6 +42,8 @@ import { RouteService } from '../shared/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 { HALEndpointService } from './shared/hal-endpoint.service'; import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service'; @@ -66,6 +71,11 @@ const PROVIDERS = [ CollectionDataService, DSOResponseParsingService, DSpaceRESTv2Service, + DynamicFormLayoutService, + DynamicFormService, + DynamicFormValidationService, + FormBuilderService, + FormService, HALEndpointService, HostWindowService, ItemDataService, @@ -89,6 +99,8 @@ const PROVIDERS = [ SubmissionDefinitionsConfigService, SubmissionFormsConfigService, SubmissionSectionsConfigService, + AuthorityService, + IntegrationResponseParsingService, UploaderService, UUIDService, { provide: NativeWindowService, useFactory: NativeWindowFactory } diff --git a/src/app/core/data/config-response-parsing.service.ts b/src/app/core/data/config-response-parsing.service.ts index 033c9ddc68..dfbbfc50c7 100644 --- a/src/app/core/data/config-response-parsing.service.ts +++ b/src/app/core/data/config-response-parsing.service.ts @@ -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(data.payload, request.href); return new ConfigSuccessResponse(configDefinition[Object.keys(configDefinition)[0]], data.statusCode, this.processPageInfo(data.payload)); } else { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html index de30440dc2..f26238cc1f 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html @@ -93,8 +93,10 @@ [ngClass]="getClass('element', 'control', checkboxModel)">
-
+
+ +
-
+
+ +
-
- + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts index dc60697c1f..cd6557b560 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts @@ -34,8 +34,8 @@ import { import { DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD } from './models/typeahead/dynamic-typeahead.model'; import { DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; import { DYNAMIC_FORM_CONTROL_TYPE_TAG } from './models/tag/dynamic-tag.model'; -import { DYNAMIC_FORM_CONTROL_TYPE_RELATION } from './models/ds-dynamic-group/dynamic-group.model'; -import { DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER } from './models/ds-date-picker/ds-date-picker.model'; +import { DYNAMIC_FORM_CONTROL_TYPE_RELATION } 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'; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html similarity index 100% rename from src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component.html rename to src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.scss similarity index 100% rename from src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component.scss rename to src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.scss diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts similarity index 96% rename from src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component.ts rename to src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts index 6b661c65c9..663e17c0b1 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts @@ -1,13 +1,13 @@ import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; import { FormGroup } from '@angular/forms'; -import { DynamicDsDatePickerModel } from './ds-date-picker.model'; +import { DynamicDsDatePickerModel } from './date-picker.model'; export const DS_DATE_PICKER_SEPARATOR = '-'; @Component({ selector: 'ds-date-picker', - styleUrls: ['./ds-date-picker.component.scss'], - templateUrl: './ds-date-picker.component.html', + styleUrls: ['./date-picker.component.scss'], + templateUrl: './date-picker.component.html', }) export class DsDatePickerComponent implements OnInit { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts similarity index 100% rename from src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.model.ts rename to src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.html similarity index 100% rename from src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.component.html rename to src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.html diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.scss similarity index 100% rename from src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.component.scss rename to src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.scss diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.components.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.components.ts similarity index 99% rename from src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.components.ts rename to src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.components.ts index f74de35110..7c209966c2 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.components.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.components.ts @@ -63,7 +63,6 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit { } ngOnInit() { - console.log(this.model.hasErrorMessages); const config = {rows: this.model.formConfiguration} as SubmissionFormsModel; if (isNotEmpty(this.model.value)) { this.formCollapsed = Observable.of(true); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model.ts similarity index 100% rename from src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.model.ts rename to src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model.ts diff --git a/src/app/shared/form/builder/form-builder.service.ts b/src/app/shared/form/builder/form-builder.service.ts index 8e61f80af4..78509f76de 100644 --- a/src/app/shared/form/builder/form-builder.service.ts +++ b/src/app/shared/form/builder/form-builder.service.ts @@ -12,12 +12,12 @@ import { } from '@ng-dynamic-forms/core'; import { mergeWith } from 'lodash'; -import { isEmpty, isNotEmpty, isNotNull, isNull } from '../../empty.util'; +import { isEmpty, isNotEmpty, isNotNull, isNotUndefined, isNull } from '../../empty.util'; import { DynamicComboboxModel } from './ds-dynamic-form-ui/models/ds-dynamic-combobox.model'; import { SubmissionFormsModel } from '../../../core/shared/config/config-submission-forms.model'; import { DynamicConcatModel } from './ds-dynamic-form-ui/models/ds-dynamic-concat.model'; import { DynamicListCheckboxGroupModel } from './ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model'; -import { DynamicGroupModel } from './ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.model'; +import { DynamicGroupModel } from './ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model'; import { DynamicTagModel } from './ds-dynamic-form-ui/models/tag/dynamic-tag.model'; import { DynamicListRadioGroupModel } from './ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model'; import { RowParser } from './parsers/row-parser'; @@ -135,7 +135,7 @@ export class FormBuilderService extends DynamicFormService { controlId = controlModel.name; } - const controlValue = (controlModel as any).value || null; + const controlValue = isNotUndefined((controlModel as any).value) ? (controlModel as any).value : null; if (controlId && iterateResult.hasOwnProperty(controlId) && isNotNull(iterateResult[controlId])) { iterateResult[controlId].push(controlValue); } else { @@ -151,7 +151,7 @@ export class FormBuilderService extends DynamicFormService { return result; } - modelFromConfiguration(json: string | SubmissionFormsModel, scopeUUID: string, initFormValues: any, submissionScope?: string, readOnly = false): DynamicFormControlModel[] | never { + 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; diff --git a/src/app/shared/form/builder/parsers/date-field-parser.ts b/src/app/shared/form/builder/parsers/date-field-parser.ts index b4b7f62a9e..57e4a4cacb 100644 --- a/src/app/shared/form/builder/parsers/date-field-parser.ts +++ b/src/app/shared/form/builder/parsers/date-field-parser.ts @@ -1,9 +1,9 @@ import { FieldParser } from './field-parser'; import { DynamicDatePickerModelConfig } from '@ng-dynamic-forms/core'; import { FormFieldModel } from '../models/form-field.model'; -import { DynamicDsDatePickerModel } from '../ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.model'; +import { 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/ds-date-picker/ds-date-picker.component'; +import { DS_DATE_PICKER_SEPARATOR } from '../ds-dynamic-form-ui/models/date-picker/date-picker.component'; export class DateFieldParser extends FieldParser { diff --git a/src/app/shared/form/builder/parsers/group-field-parser.ts b/src/app/shared/form/builder/parsers/group-field-parser.ts index 5ed0dfc18b..cd897bb265 100644 --- a/src/app/shared/form/builder/parsers/group-field-parser.ts +++ b/src/app/shared/form/builder/parsers/group-field-parser.ts @@ -4,7 +4,7 @@ import { FormFieldModel } from '../models/form-field.model'; import { DynamicGroupModel, DynamicGroupModelConfig, PLACEHOLDER_PARENT_METADATA -} from '../ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.model'; +} 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'; diff --git a/src/app/shared/form/builder/parsers/row-parser.ts b/src/app/shared/form/builder/parsers/row-parser.ts index b082cc060a..833ccfb6bd 100644 --- a/src/app/shared/form/builder/parsers/row-parser.ts +++ b/src/app/shared/form/builder/parsers/row-parser.ts @@ -11,7 +11,7 @@ import { TagFieldParser } from './tag-field-parser'; import { TextareaFieldParser } from './textarea-field-parser'; import { GroupFieldParser } from './group-field-parser'; import { IntegrationSearchOptions } from '../../../../core/integration/models/integration-options.model'; -import { DynamicGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.model'; +import { DynamicGroupModel } from '../ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model'; import { DynamicRowGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-group-model'; import { isEmpty } from '../../../empty.util'; import { LookupFieldParser } from './lookup-field-parser'; diff --git a/src/app/shared/form/form.component.html b/src/app/shared/form/form.component.html index e760c0111e..0539a57000 100644 --- a/src/app/shared/form/form.component.html +++ b/src/app/shared/form/form.component.html @@ -5,6 +5,7 @@ [formId]="formId" [formGroup]="formGroup" [formModel]="formModel" + [formLayout]="formLayout" (dfBlur)="onBlur($event)" (dfChange)="onChange($event)" (dfFocus)="onFocus($event)"> diff --git a/src/app/shared/form/form.component.ts b/src/app/shared/form/form.component.ts index 47258315e2..99712bde9c 100644 --- a/src/app/shared/form/form.component.ts +++ b/src/app/shared/form/form.component.ts @@ -5,7 +5,7 @@ import { DynamicFormArrayModel, DynamicFormControlEvent, DynamicFormControlModel, - DynamicFormGroupModel, + DynamicFormGroupModel, DynamicFormLayout, } from '@ng-dynamic-forms/core'; import { Store } from '@ngrx/store'; @@ -49,6 +49,7 @@ export class FormComponent implements OnDestroy, OnInit { @Input() formModel: DynamicFormControlModel[]; @Input() parentFormModel: DynamicFormGroupModel | DynamicFormGroupModel[]; @Input() formGroup: FormGroup; + @Input() formLayout: DynamicFormLayout = null; /* tslint:disable:no-output-rename */ @Output('dfBlur') blur: EventEmitter = new EventEmitter(); @@ -205,6 +206,7 @@ export class FormComponent implements OnDestroy, OnInit { } onChange(event) { + console.log(event, this.formGroup); const action: FormChangeAction = new FormChangeAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel)); this.store.dispatch(action); diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 81503f2245..d784df9df3 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -61,10 +61,10 @@ import { UploaderComponent } from './uploader/uploader.component'; import { ChipsComponent } from './chips/chips.component'; import { DsDynamicTagComponent } from './form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component'; import { DsDynamicListComponent } from './form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component'; -import { DsDynamicGroupComponent } from './form/builder/ds-dynamic-form-ui/models/ds-dynamic-group/dynamic-group.components'; +import { DsDynamicGroupComponent } from './form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.components'; import { SortablejsModule } from 'angular-sortablejs'; import { NumberPickerComponent } from './number-picker/number-picker.component'; -import { DsDatePickerComponent } from './form/builder/ds-dynamic-form-ui/models/ds-date-picker/ds-date-picker.component'; +import { DsDatePickerComponent } from './form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component'; import { DsDynamicLookupComponent } from './form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component'; const MODULES = [ diff --git a/webpack/webpack.server.js b/webpack/webpack.server.js index 20f188dc79..ce0c52602a 100644 --- a/webpack/webpack.server.js +++ b/webpack/webpack.server.js @@ -17,6 +17,10 @@ module.exports = { whitelist: [ /@angular/, /@ng/, + /angular2-text-mask/, + /ng2-file-upload/, + /angular-sortablejs/, + /sortablejs/, /ngx/] })], } From 876dba289232e99d394864ebbdb2a0282d6cce33 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Thu, 10 May 2018 17:24:09 +0200 Subject: [PATCH 03/41] Added tests --- src/app/shared/chips/chips.component.spec.ts | 79 ++++++++ src/app/shared/form/form.actions.ts | 7 +- src/app/shared/form/form.component.spec.ts | 64 ++++--- src/app/shared/form/form.reducer.spec.ts | 174 ++++++++++++++++++ src/app/shared/form/form.reducers.ts | 6 +- src/app/shared/form/form.service.spec.ts | 73 ++++++++ src/app/shared/form/form.service.ts | 2 +- .../uploader/uploader.component.spec.ts | 91 +++++++++ 8 files changed, 457 insertions(+), 39 deletions(-) create mode 100644 src/app/shared/chips/chips.component.spec.ts create mode 100644 src/app/shared/form/form.reducer.spec.ts create mode 100644 src/app/shared/form/form.service.spec.ts create mode 100644 src/app/shared/uploader/uploader.component.spec.ts diff --git a/src/app/shared/chips/chips.component.spec.ts b/src/app/shared/chips/chips.component.spec.ts new file mode 100644 index 0000000000..add22b5d42 --- /dev/null +++ b/src/app/shared/chips/chips.component.spec.ts @@ -0,0 +1,79 @@ +// 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 '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'; + +function createTestComponent(html: string, type: { new(...args: any[]): T }): ComponentFixture { + TestBed.overrideComponent(type, { + set: {template: html} + }); + const fixture = TestBed.createComponent(type); + + fixture.detectChanges(); + return fixture as ComponentFixture; +} + +describe('Chips component', () => { + + let testComp: TestComponent; + let testFixture: ComponentFixture; + let html; + + // 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] + }); + + })); + + // synchronous beforeEach + beforeEach(() => { + html = ` + `; + + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + it('should create Chips Component', inject([ChipsComponent], (app: ChipsComponent) => { + + expect(app).toBeDefined(); + })); + +}); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + + public chips = new Chips([]); + +} diff --git a/src/app/shared/form/form.actions.ts b/src/app/shared/form/form.actions.ts index 681add077e..b1d5f2b3e3 100644 --- a/src/app/shared/form/form.actions.ts +++ b/src/app/shared/form/form.actions.ts @@ -15,9 +15,9 @@ export const FormActionTypes = { FORM_CHANGE: type('dspace/form/FORM_CHANGE'), FORM_REMOVE: type('dspace/form/FORM_REMOVE'), FORM_STATUS_CHANGE: type('dspace/form/FORM_STATUS_CHANGE'), - FORM_ADD_ERROR: type('dspace/form/ADD_ERROR'), - FORM_REMOVE_ERROR: type('dspace/form/REMOVE_ERROR'), - CLEAR_ERRORS: type('dspace/form/CLEAR_ERRORS'), + FORM_ADD_ERROR: type('dspace/form/FORM_ADD_ERROR'), + FORM_REMOVE_ERROR: type('dspace/form/FORM_REMOVE_ERROR'), + FORM_CLEAR_ERRORS: type('dspace/form/FORM_CLEAR_ERRORS'), }; /* tslint:disable:max-classes-per-file */ @@ -122,5 +122,6 @@ export class FormAddError implements Action { */ export type FormAction = FormInitAction | FormChangeAction + | FormRemoveAction | FormStatusChangeAction | FormAddError diff --git a/src/app/shared/form/form.component.spec.ts b/src/app/shared/form/form.component.spec.ts index a0489a5ab8..efc6383603 100644 --- a/src/app/shared/form/form.component.spec.ts +++ b/src/app/shared/form/form.component.spec.ts @@ -1,40 +1,25 @@ // 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 { CommonModule } from '@angular/common'; +import { BrowserModule } from '@angular/platform-browser'; +import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { - Component, - CUSTOM_ELEMENTS_SCHEMA, - DebugElement -} from '@angular/core'; - -import { - async, - ComponentFixture, - inject, - TestBed, -} from '@angular/core/testing'; - -import { StoreModule } from '@ngrx/store'; - +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/of'; +import { DynamicFormControlModel, DynamicFormValidationService, DynamicInputModel } from '@ng-dynamic-forms/core'; +import { Store } from '@ngrx/store'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; - -import Spy = jasmine.Spy; +import { TranslateModule } from '@ngx-translate/core'; import { FormComponent } from './form.component'; import { FormService } from './form.service'; -import { DynamicFormControlModel, DynamicFormValidationService, DynamicInputModel } from '@ng-dynamic-forms/core'; -import { BrowserModule } from '@angular/platform-browser'; -import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormBuilderService } from './builder/form-builder.service'; -import { SubmissionFormsConfigService } from '../../core/config/submission-forms-config.service'; -import { ResponseCacheService } from '../../core/cache/response-cache.service'; -import { RequestService } from '../../core/data/request.service'; -import { ObjectCacheService } from '../../core/cache/object-cache.service'; -import { Observable } from 'rxjs/Observable'; +import { FormState } from './form.reducers'; function createTestComponent(html: string, type: { new(...args: any[]): T }): ComponentFixture { TestBed.overrideComponent(type, { - set: { template: html } + set: {template: html} }); const fixture = TestBed.createComponent(type); @@ -110,7 +95,14 @@ describe('Form component', () => { const formBuilderServiceStub = { createFormGroup: (formModel) => new FormGroup(TEST_FORM_GROUP) } - const submissionFormsConfigServiceStub = { } + const submissionFormsConfigServiceStub = {}; + + const store: Store = jasmine.createSpyObj('store', { + /* tslint:disable:no-empty */ + dispatch: {}, + /* tslint:enable:no-empty */ + select: Observable.of({}) + }); // async beforeEach beforeEach(async(() => { @@ -121,17 +113,22 @@ describe('Form component', () => { CommonModule, FormsModule, ReactiveFormsModule, - StoreModule.forRoot({}), NgbModule.forRoot(), + TranslateModule.forRoot() ], declarations: [ FormComponent, TestComponent, ], // declare the test component providers: [ + ChangeDetectorRef, + DynamicFormValidationService, + FormBuilderService, FormComponent, - { provide: FormService, useValue: formServiceStub }, - { provide: FormBuilderService, useValue: formBuilderServiceStub }, + FormService, + { + provide: Store, useValue: store + } ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }); @@ -141,17 +138,17 @@ describe('Form component', () => { // synchronous beforeEach beforeEach(() => { html = ` - `; + [displaySubmit]="displaySubmit">`; testFixture = createTestComponent(html, TestComponent) as ComponentFixture; testComp = testFixture.componentInstance; - }); it('should create Form Component', inject([FormComponent], (app: FormComponent) => { + expect(app).toBeDefined(); })); @@ -166,6 +163,7 @@ class TestComponent { public formId; public formModel: DynamicFormControlModel[]; + public displaySubmit = false; constructor() { this.formId = 'testForm'; diff --git a/src/app/shared/form/form.reducer.spec.ts b/src/app/shared/form/form.reducer.spec.ts new file mode 100644 index 0000000000..64141a15b6 --- /dev/null +++ b/src/app/shared/form/form.reducer.spec.ts @@ -0,0 +1,174 @@ +import { formReducer } from './form.reducers'; +import { + FormAddError, + FormChangeAction, + FormInitAction, + FormRemoveAction, + FormStatusChangeAction +} from './form.actions'; + +describe('formReducer', () => { + + it('should set init state of the form', () => { + const state = { + testForm: { + data: { + 'dc.contributor.author': null, + 'dc.title': null, + 'dc.date.issued': null, + 'dc.description': null + }, + valid: false, + errors: [] + } + }; + const formId = 'testForm'; + const formData = { + 'dc.contributor.author': null, + 'dc.title': null, + 'dc.date.issued': null, + 'dc.description': null + }; + const valid = false; + const action = new FormInitAction(formId, formData, valid); + const newState = formReducer({}, action); + + expect(newState).toEqual(state); + }); + + it('should change form data on form change', () => { + const initState = { + testForm: { + data: { + 'dc.contributor.author': null, + 'dc.title': null, + 'dc.date.issued': null, + 'dc.description': null + }, + valid: false, + errors: [] + } + }; + const state = { + testForm: { + data: { + 'dc.contributor.author': null, + 'dc.title': ['test'], + 'dc.date.issued': null, + 'dc.description': null + }, + valid: false, + errors: [] + } + }; + const formId = 'testForm'; + const formData = { + 'dc.contributor.author': null, + 'dc.title': ['test'], + 'dc.date.issued': null, + 'dc.description': null + }; + + const action = new FormChangeAction(formId, formData); + const newState = formReducer(initState, action); + + expect(newState).toEqual(state); + }); + + it('should change form status on form status change', () => { + const initState = { + testForm: { + data: { + 'dc.contributor.author': null, + 'dc.title': ['test'], + 'dc.date.issued': null, + 'dc.description': null + }, + valid: false, + errors: [] + } + }; + const state = { + testForm: { + data: { + 'dc.contributor.author': null, + 'dc.title': ['test'], + 'dc.date.issued': null, + 'dc.description': null + }, + valid: true, + errors: [] + } + }; + const formId = 'testForm'; + + const action = new FormStatusChangeAction(formId, true); + const newState = formReducer(initState, action); + + expect(newState).toEqual(state); + }); + + it('should add error to form state', () => { + const initState = { + testForm: { + data: { + 'dc.contributor.author': null, + 'dc.title': ['test'], + 'dc.date.issued': null, + 'dc.description': null + }, + valid: true, + errors: [] + } + }; + + const state = { + testForm: { + data: { + 'dc.contributor.author': null, + 'dc.title': ['test'], + 'dc.date.issued': null, + 'dc.description': null + }, + valid: true, + errors: [ + { + fieldId: 'dc.title', + message: 'Not valid' + } + ] + } + }; + + const formId = 'testForm'; + const fieldId = 'dc.title'; + const message = 'Not valid'; + + const action = new FormAddError(formId, fieldId, message); + const newState = formReducer(initState, action); + + expect(newState).toEqual(state); + }); + + it('should remove form state', () => { + const initState = { + testForm: { + data: { + 'dc.contributor.author': null, + 'dc.title': ['test'], + 'dc.date.issued': null, + 'dc.description': null + }, + valid: true, + errors: [] + } + }; + + const formId = 'testForm'; + + const action = new FormRemoveAction(formId); + const newState = formReducer(initState, action); + + expect(newState).toEqual({}); + }); +}); diff --git a/src/app/shared/form/form.reducers.ts b/src/app/shared/form/form.reducers.ts index 04d7d28d85..d06db331eb 100644 --- a/src/app/shared/form/form.reducers.ts +++ b/src/app/shared/form/form.reducers.ts @@ -86,14 +86,16 @@ function initForm(state: FormState, action: FormInitAction): FormState { return Object.assign({}, state, { [ action.payload.formId ]: { data: action.payload.formData, - valid: action.payload.valid + valid: action.payload.valid, + errors: [] } }); } else { const newState = Object.assign({}, state); newState[ action.payload.formId ] = Object.assign({}, newState[ action.payload.formId ], { data: action.payload.formData, - valid: action.payload.valid + valid: action.payload.valid, + errors: [] } ); return newState; diff --git a/src/app/shared/form/form.service.spec.ts b/src/app/shared/form/form.service.spec.ts new file mode 100644 index 0000000000..b72bbbd0d6 --- /dev/null +++ b/src/app/shared/form/form.service.spec.ts @@ -0,0 +1,73 @@ +import { Store, StoreModule } from '@ngrx/store'; +import { async, inject, TestBed } from '@angular/core/testing'; +import 'rxjs/add/observable/of'; +import { FormService } from './form.service'; +import { FormBuilderService } from './builder/form-builder.service'; +import { AppState } from '../../app.reducer'; +import { DynamicPathable } from '@ng-dynamic-forms/core/src/model/misc/dynamic-form-control-path.model'; +import { DynamicFormControlModel } from '@ng-dynamic-forms/core'; +import { formReducer } from './form.reducers'; + +describe('FormService', () => { + const formId = 'testForm'; + let service: FormService; + const formData = { + 'dc.contributor.author': null, + 'dc.title': ['test'], + 'dc.date.issued': null, + 'dc.description': null + }; + const formState = { + testForm: { + data: formData, + valid: true, + errors: [] + } + }; + + const formBuilderServiceStub: any = { + getPath: (model: DynamicPathable) => [], + /* tslint:disable:no-empty */ + clearAllModelsValue: (groupModel: DynamicFormControlModel[]) => { + } + /* tslint:enable:no-empty */ + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + StoreModule.forRoot({formReducer}) + ], + providers: [ + {provide: FormBuilderService, useValue: formBuilderServiceStub}, + ] + }).compileComponents(); + })); + + beforeEach(inject([Store], (store: Store) => { + store + .subscribe((state) => { + state.forms = formState; + }); + service = new FormService(formBuilderServiceStub, store); + })); + + it('should check whether form state is init', () => { + service.isFormInitialized(formId).subscribe((init) => { + expect(init).toBe(true); + }); + }); + + it('should return form status when isValid is called', () => { + service.isValid(formId).subscribe((status) => { + expect(status).toBe(true); + }); + }); + + it('should return form data when getFormData is called', () => { + service.getFormData(formId).subscribe((data) => { + expect(data).toBe(formData); + }); + }); + +}); diff --git a/src/app/shared/form/form.service.ts b/src/app/shared/form/form.service.ts index 9d7b18acbc..2adef27179 100644 --- a/src/app/shared/form/form.service.ts +++ b/src/app/shared/form/form.service.ts @@ -31,7 +31,7 @@ export class FormService { /** * Method to retrieve form's data from state */ - public getFormData(formId: string): Observable { + public getFormData(formId: string): Observable { return this.store.select(formObjectFromIdSelector(formId)) .filter((state) => isNotUndefined(state)) .map((state) => state.data) diff --git a/src/app/shared/uploader/uploader.component.spec.ts b/src/app/shared/uploader/uploader.component.spec.ts new file mode 100644 index 0000000000..f1e12e7693 --- /dev/null +++ b/src/app/shared/uploader/uploader.component.spec.ts @@ -0,0 +1,91 @@ +// 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 { ScrollToService } from '@nicky-lenaers/ngx-scroll-to'; + +import { UploaderService } from './uploader.service'; +import { UploaderOptions } from './uploader-options.model'; +import { UploaderComponent } from './uploader.component'; +import { FileUploadModule } from 'ng2-file-upload'; +import { TranslateModule } from '@ngx-translate/core'; + +function createTestComponent(html: string, type: { new(...args: any[]): T }): ComponentFixture { + TestBed.overrideComponent(type, { + set: {template: html} + }); + const fixture = TestBed.createComponent(type); + + fixture.detectChanges(); + return fixture as ComponentFixture; +} + +describe('Chips component', () => { + + let testComp: TestComponent; + let testFixture: ComponentFixture; + let html; + + // async beforeEach + beforeEach(async(() => { + + TestBed.configureTestingModule({ + imports: [ + FileUploadModule, + TranslateModule.forRoot() + ], + declarations: [ + UploaderComponent, + TestComponent, + ], // declare the test component + providers: [ + ChangeDetectorRef, + ScrollToService, + UploaderComponent, + UploaderService + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }); + + })); + + // synchronous beforeEach + beforeEach(() => { + html = ` + `; + + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + it('should create Uploader Component', inject([UploaderComponent], (app: UploaderComponent) => { + + expect(app).toBeDefined(); + })); + +}); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + public uploadFilesOptions: UploaderOptions = { + url: 'http://test', + authToken: null, + disableMultipart: false, + itemAlias: null + }; + + /* tslint:disable:no-empty */ + public onBeforeUpload = () => { + }; + + onCompleteItem(event) { + } + + /* tslint:enable:no-empty */ +} From 5ec01031d9e9f57d203351f73c78abd9c4ebf879 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Mon, 11 Jun 2018 11:13:05 +0200 Subject: [PATCH 04/41] updated with the latest changes --- .../ds-dynamic-form-control.component.html | 2 +- .../ds-dynamic-form-control.component.ts | 2 - .../date-picker/date-picker.component.html | 80 ++++++++--------- .../date-picker/date-picker.component.ts | 30 +++---- .../dynamic-group.component.html | 11 ++- .../dynamic-group/dynamic-group.components.ts | 40 +++++++-- .../models/list/dynamic-list.component.ts | 10 +-- .../lookup/dynamic-lookup.component.html | 61 ++++++------- .../lookup/dynamic-lookup.component.scss | 4 + ...dynamic-scrollable-dropdown.component.html | 4 +- .../typeahead/dynamic-typeahead.component.ts | 11 ++- .../form/builder/form-builder.service.ts | 84 ++++++++++++++--- .../models/form-field-metadata-value.model.ts | 12 ++- .../form/builder/parsers/date-field-parser.ts | 18 ++-- .../form/builder/parsers/field-parser.ts | 25 +++--- .../builder/parsers/onebox-field-parser.ts | 1 - src/app/shared/form/form.actions.ts | 25 ++++++ src/app/shared/form/form.component.ts | 18 ++-- src/app/shared/form/form.reducers.ts | 89 +++++++++++++------ src/app/shared/form/form.service.ts | 2 +- .../number-picker.component.html | 14 +-- .../number-picker.component.scss | 23 +---- .../number-picker/number-picker.component.ts | 51 +++++------ 23 files changed, 375 insertions(+), 242 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html index f26238cc1f..5614e23c4a 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html @@ -127,8 +127,8 @@ [showWeekdays]="getAdditional('showWeekdays', true)" [showWeekNumbers]="getAdditional('showWeekNumbers', false)" [startDate]="model.focusedDate" + (dateSelect)="onValueChange($event)" (blur)="onBlur($event)" - (change)="onValueChange($event)" (focus)="onFocus($event)">
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts index cd6557b560..a38c35eb9c 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts @@ -12,7 +12,6 @@ import { import { FormGroup } from '@angular/forms'; import { DynamicDatePickerModel, - DynamicFormArrayGroupModel, DynamicFormControlComponent, DynamicFormControlEvent, DynamicFormControlModel, @@ -172,5 +171,4 @@ export class DsDynamicFormControlComponent extends DynamicFormControlComponent i this.onValueChange(event); } } - } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html index 81733ff999..96ee0dd38e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.html @@ -1,46 +1,46 @@
-
- -
+ -
- -
+ -
- -
+
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts index 663e17c0b1..6d50d4375b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts @@ -1,6 +1,7 @@ import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { DynamicDsDatePickerModel } from './date-picker.model'; +import { hasValue, isNotEmpty } from '../../../../../empty.util'; export const DS_DATE_PICKER_SEPARATOR = '-'; @@ -20,12 +21,10 @@ export class DsDatePickerComponent implements OnInit { // @Input() // maxDate; - @Output() - selected = new EventEmitter(); - @Output() - remove = new EventEmitter(); - @Output() - change = new EventEmitter(); + @Output() selected = new EventEmitter(); + @Output() remove = new EventEmitter(); + @Output() change = new EventEmitter(); + @Output() focus = new EventEmitter(); initialYear: number; initialMonth: number; @@ -48,9 +47,8 @@ export class DsDatePickerComponent implements OnInit { disabledMonth = true; disabledDay = true; - invalid = false; - ngOnInit() {// TODO Manage fields when not setted + ngOnInit() { const now = new Date(); this.initialYear = now.getFullYear(); this.initialMonth = now.getMonth() + 1; @@ -76,15 +74,6 @@ export class DsDatePickerComponent implements OnInit { this.maxYear = this.initialYear + 100; - // Invalid state for year - this.group.get(this.model.id).statusChanges.subscribe((state) => { - if (state === 'INVALID' || this.model.malformedDate) { - this.invalid = true; - } else { - this.invalid = false; - this.model.malformedDate = false; - } - }); } onChange(event) { @@ -157,8 +146,13 @@ export class DsDatePickerComponent implements OnInit { : this.day.toString(); value += DS_DATE_PICKER_SEPARATOR + dd; } + this.model.valueUpdates.next(value); - this.change.emit(event); + this.change.emit(value); + } + + onFocus(event) { + this.focus.emit(event); } getLastDay(date: Date) { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.html index 916026a85b..c628eaf8e5 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.html @@ -18,10 +18,11 @@
+ [displaySubmit]="false" + (dfChange)="onChange($event)">
@@ -54,7 +55,11 @@
- +
{ + this.formCollapsed = (isNotEmpty(value) && !(value.length === 1 && isObjectEmpty(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.chips = new Chips(this.model.value, 'value', this.model.mandatoryField); + this.chips = new Chips(this.model.value, 'value', this.model.mandatoryField, this.EnvConfig.submission.metadata.icons); this.subs.push( this.chips.chipsItems .subscribe((subItems: any[]) => { @@ -118,20 +130,32 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit { return res; } + onChange(event: DynamicFormControlEvent) { + return + } + onChipSelected(event) { this.expandForm(); this.selectedChipItem = this.chips.getChipByIndex(event); this.formModel.forEach((row) => { const modelRow = row as DynamicFormGroupModel; modelRow.group.forEach((model: DynamicInputModel) => { - const value = this.selectedChipItem.item[model.name] === PLACEHOLDER_PARENT_METADATA ? null : this.selectedChipItem.item[model.name]; - if (model instanceof DynamicLookupModel) { - (model as DynamicLookupModel).valueUpdates.next(value); - } else if (model instanceof DynamicInputModel) { - model.valueUpdates.next(value); + 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 as any).value = value; + model.valueUpdates.next(value); } + // if (model instanceof DynamicLookupModel) { + // (model as DynamicLookupModel).valueUpdates.next(value); + // } else if (model instanceof DynamicInputModel) { + // model.valueUpdates.next(value); + // } else { + // (model as any).value = value; + // } }); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts index a3baa3de88..91bd0275e7 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts @@ -6,12 +6,11 @@ import { AuthorityService } from '../../../../../../core/integration/authority.s import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model'; import { hasValue, isNotEmpty } from '../../../../../empty.util'; import { DynamicListCheckboxGroupModel } from './dynamic-list-checkbox-group.model'; -import { ConfigData } from '../../../../../../core/config/config-data'; -import { ConfigAuthorityModel } from '../../../../../../core/shared/config/config-authority.model'; import { FormBuilderService } from '../../../form-builder.service'; import { DynamicCheckboxModel } from '@ng-dynamic-forms/core'; import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model'; import { DynamicListRadioGroupModel } from './dynamic-list-radio-group.model'; +import { IntegrationData } from '../../../../../../core/integration/integration-data'; export interface ListItem { id: string, @@ -26,7 +25,6 @@ export interface ListItem { templateUrl: './dynamic-list.component.html' }) -// TODO Fare questo componente da zero export class DsDynamicListComponent implements OnInit { @Input() bindId = true; @Input() group: FormGroup; @@ -91,13 +89,13 @@ export class DsDynamicListComponent implements OnInit { protected setOptionsFromAuthority() { if (this.model.authorityOptions.name && this.model.authorityOptions.name.length > 0) { const listGroup = this.group.controls[this.model.id] as FormGroup; - this.authorityService.getEntriesByName(this.searchOptions).subscribe((authorities: ConfigData) => { + this.authorityService.getEntriesByName(this.searchOptions).subscribe((authorities: IntegrationData) => { let groupCounter = 0; let itemsPerGroup = 0; let tempList: ListItem[] = []; - this.authorityList = authorities.payload as ConfigAuthorityModel[]; + this.authorityList = authorities.payload as AuthorityValueModel[]; // Make a list of available options (checkbox/radio) and split in groups of 'model.groupLength' - (authorities.payload as ConfigAuthorityModel[]).forEach((option, key) => { + (authorities.payload as AuthorityValueModel[]).forEach((option, key) => { const value = option.id || option.value; const checked: boolean = isNotEmpty(findKey( this.model.value, diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html index 972538efad..14f468b0f8 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html @@ -1,7 +1,6 @@ -
+ (openChange)="openChange($event);">
@@ -19,26 +18,25 @@ [placeholder]="model.placeholder" [readonly]="model.readOnly" (change)="$event.preventDefault()" - (blur)="onBlurEvent($event); sdRef.close();" - (focus)="onFocusEvent($event); sdRef.close();" - (click)="$event.stopPropagation(); sdRef.close();" + (blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();" + (focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();" + (click)="$event.stopPropagation(); $event.stopPropagation(); sdRef.close();" (input)="onInput($event)">
- -
@@ -59,9 +57,9 @@ [placeholder]="model.placeholder | translate" [readonly]="model.readOnly" (change)="$event.preventDefault()" - (blur)="onBlurEvent($event); sdRef.close();" - (focus)="onFocusEvent($event); sdRef.close();" - (click)="$event.stopPropagation(); sdRef.close();" + (blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();" + (focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();" + (click)="$event.stopPropagation(); $event.stopPropagation(); sdRef.close();" (input)="onInput($event)">
@@ -78,24 +76,23 @@ [placeholder]="model.placeholder2 | translate" [readonly]="model.readOnly" (change)="$event.preventDefault()" - (blur)="onBlurEvent($event); sdRef.close();" - (focus)="onFocusEvent($event); sdRef.close();" + (blur)="onBlurEvent($event); $event.stopPropagation(); sdRef.close();" + (focus)="onFocusEvent($event); $event.stopPropagation(); sdRef.close();" (click)="$event.stopPropagation(); sdRef.close();" (input)="onInput($event)">
- -
@@ -115,11 +112,11 @@ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.scss index a333fc881f..a697772020 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.scss +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.scss @@ -1,5 +1,9 @@ @import "../../../../../../../styles/variables"; +.dropdown-toggle::after { + display:none +} + /* enable absolute positioning */ .spinner-addon { position: relative; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html index 1ea1b62cd0..aed4e16bca 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html @@ -30,11 +30,11 @@ (scrolled)="onScroll()" [scrollWindow]="false"> - + -

Loading...

+

{{'form.loading' | translate}}

diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts index a261eef8f7..1d31fc0968 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts @@ -30,6 +30,7 @@ export class DsDynamicTypeaheadComponent implements OnInit { searchFailed = false; hideSearchingWhenUnsubscribed = new Observable(() => () => this.changeSearchingStatus(false)); currentValue: any; + inputValue: any; formatter = (x: { display: string }) => { return (typeof x === 'object') ? x.display : x @@ -88,9 +89,8 @@ export class DsDynamicTypeaheadComponent implements OnInit { onInput(event) { if (!this.model.authorityOptions.closed && isNotEmpty(event.target.value)) { const valueObj = new FormFieldMetadataValueObject(event.target.value); - this.currentValue = valueObj; - this.model.valueUpdates.next(valueObj as any); - this.change.emit(valueObj); + this.inputValue = valueObj; + this.model.valueUpdates.next(this.inputValue); } if (event.data) { // this.group.markAsDirty(); @@ -98,6 +98,10 @@ export class DsDynamicTypeaheadComponent implements OnInit { } onBlurEvent(event: Event) { + if (!this.model.authorityOptions.closed && isNotEmpty(this.inputValue)) { + this.change.emit(this.inputValue); + this.inputValue = null; + } this.blur.emit(event); } @@ -114,6 +118,7 @@ export class DsDynamicTypeaheadComponent implements OnInit { } onSelectItem(event: NgbTypeaheadSelectItemEvent) { + this.inputValue = null; this.currentValue = event.item; this.model.valueUpdates.next(event.item); this.change.emit(event.item); diff --git a/src/app/shared/form/builder/form-builder.service.ts b/src/app/shared/form/builder/form-builder.service.ts index 78509f76de..2b0cddaaa4 100644 --- a/src/app/shared/form/builder/form-builder.service.ts +++ b/src/app/shared/form/builder/form-builder.service.ts @@ -10,20 +10,26 @@ import { DynamicPathable, JSONUtils, } from '@ng-dynamic-forms/core'; -import { mergeWith } from 'lodash'; +import { mergeWith, isObject } from 'lodash'; import { isEmpty, isNotEmpty, isNotNull, isNotUndefined, isNull } from '../../empty.util'; import { DynamicComboboxModel } from './ds-dynamic-form-ui/models/ds-dynamic-combobox.model'; import { SubmissionFormsModel } from '../../../core/shared/config/config-submission-forms.model'; import { DynamicConcatModel } from './ds-dynamic-form-ui/models/ds-dynamic-concat.model'; import { DynamicListCheckboxGroupModel } from './ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model'; -import { DynamicGroupModel } from './ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model'; -import { DynamicTagModel } from './ds-dynamic-form-ui/models/tag/dynamic-tag.model'; +import { + DYNAMIC_FORM_CONTROL_TYPE_RELATION, + DynamicGroupModel +} from './ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model'; +import { DYNAMIC_FORM_CONTROL_TYPE_TAG, DynamicTagModel } from './ds-dynamic-form-ui/models/tag/dynamic-tag.model'; import { DynamicListRadioGroupModel } from './ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model'; import { RowParser } from './parsers/row-parser'; import { DynamicRowArrayModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-array-model'; import { DynamicRowGroupModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-group-model'; +import { DsDynamicInputModel } from './ds-dynamic-form-ui/models/ds-dynamic-input.model'; +import { FormFieldMetadataValueObject } from './models/form-field-metadata-value.model'; +import { AuthorityValueModel } from '../../../core/integration/models/authority-value.model'; @Injectable() export class FormBuilderService extends DynamicFormService { @@ -95,11 +101,25 @@ export class FormBuilderService extends DynamicFormService { } }; - const iterateControlModels = (findGroupModel: DynamicFormControlModel[]): void => { + const normalizeValue = (controlModel, controlValue, controlModelIndex) => { + const controlLanguage = (controlModel as DsDynamicInputModel).hasLanguages ? (controlModel as DsDynamicInputModel).language : null; + if (((isObject(controlValue) && controlValue.id) || controlValue instanceof AuthorityValueModel)) { + return new FormFieldMetadataValueObject(controlValue.value, controlLanguage, controlValue.id, controlValue.display, controlModelIndex); + } else if (!(controlValue instanceof FormFieldMetadataValueObject)) { + return new FormFieldMetadataValueObject(controlValue, controlLanguage, null, null, controlModelIndex); + } else { + const place = controlModelIndex || controlValue.place; + return Object.assign(new FormFieldMetadataValueObject(), controlValue, {place}); + } + }; + + const iterateControlModels = (findGroupModel: DynamicFormControlModel[], controlModelIndex: number = 0): void => { let iterateResult = Object.create({}); // Iterate over all group's controls for (const controlModel of findGroupModel) { + /* tslint:disable-next-line:no-shadowed-variable */ + // for (const {controlModel, controlModelIndex} of findGroupModel.map((controlModel, controlModelIndex) => ({ controlModel, controlModelIndex }))) { if (controlModel instanceof DynamicRowGroupModel && !this.isCustomGroup(controlModel)) { iterateResult = mergeWith(iterateResult, iterateControlModels((controlModel as DynamicFormGroupModel).group), customizer); @@ -113,7 +133,7 @@ export class FormBuilderService extends DynamicFormService { if (controlModel instanceof DynamicRowArrayModel) { for (const arrayItemModel of controlModel.groups) { - iterateResult = mergeWith(iterateResult, iterateControlModels(arrayItemModel.group), customizer); + iterateResult = mergeWith(iterateResult, iterateControlModels(arrayItemModel.group, arrayItemModel.index), customizer); } continue; } @@ -121,7 +141,7 @@ export class FormBuilderService extends DynamicFormService { if (controlModel instanceof DynamicFormArrayModel) { iterateResult[controlModel.name] = []; for (const arrayItemModel of controlModel.groups) { - iterateResult[controlModel.name].push(iterateControlModels(arrayItemModel.group)); + iterateResult[controlModel.name].push(iterateControlModels(arrayItemModel.group, arrayItemModel.index)); } continue; } @@ -135,12 +155,37 @@ export class FormBuilderService extends DynamicFormService { controlId = controlModel.name; } - const controlValue = isNotUndefined((controlModel as any).value) ? (controlModel as any).value : null; - if (controlId && iterateResult.hasOwnProperty(controlId) && isNotNull(iterateResult[controlId])) { - iterateResult[controlId].push(controlValue); - } else { - iterateResult[controlId] = isNotEmpty(controlValue) ? (Array.isArray(controlValue) ? controlValue : [controlValue]) : null; + if (controlModel instanceof DynamicGroupModel) { + const values = (controlModel as any).value; + values.forEach((groupValue, groupIndex) => { + const newGroupValue = Object.create({}); + Object.keys(groupValue) + .forEach((key) => { + const normValue = normalizeValue(controlModel, groupValue[key], groupIndex); + if (iterateResult.hasOwnProperty(key)) { + iterateResult[key].push(normValue); + } else { + iterateResult[key] = [normValue]; + } + // newGroupValue[key] = normalizeValue(controlModel, groupValue[key], groupIndex); + }); + // controlArrayValue.push(newGroupValue); + }) + } 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; @@ -182,16 +227,29 @@ export class FormBuilderService extends DynamicFormService { || model.parent instanceof DynamicGroupModel); } + isComboboxGroup(model: DynamicFormControlModel) { + return model && model instanceof DynamicComboboxModel; + } + isCustomGroup(model: DynamicFormControlModel) { return model && (model instanceof DynamicConcatModel || model instanceof DynamicComboboxModel - || model instanceof DynamicListCheckboxGroupModel + || this.isListGroup(model)); + } + + isListGroup(model: DynamicFormControlModel) { + return model && + (model instanceof DynamicListCheckboxGroupModel || model instanceof DynamicListRadioGroupModel); } isModelInAuthorityGroup(model: DynamicFormControlModel) { - return (model instanceof DynamicListCheckboxGroupModel || model instanceof DynamicTagModel); + return model && (this.isListGroup(model) || model.type === DYNAMIC_FORM_CONTROL_TYPE_TAG); + } + + isRelationGroup(model: DynamicFormControlModel) { + return model && model.type === DYNAMIC_FORM_CONTROL_TYPE_RELATION; } getFormControlById(id: string, formGroup: FormGroup, groupModel: DynamicFormControlModel[], index = 0) { diff --git a/src/app/shared/form/builder/models/form-field-metadata-value.model.ts b/src/app/shared/form/builder/models/form-field-metadata-value.model.ts index 7379d62de6..ebcdc9264c 100644 --- a/src/app/shared/form/builder/models/form-field-metadata-value.model.ts +++ b/src/app/shared/form/builder/models/form-field-metadata-value.model.ts @@ -2,7 +2,7 @@ import { isNotEmpty } from '../../../empty.util'; export class FormFieldMetadataValueObject { metadata?: string; - value: string; + value: any; display: string; language: any; authority: string; @@ -11,14 +11,14 @@ export class FormFieldMetadataValueObject { closed: boolean; label: string; - constructor(value: string, + constructor(value: any = null, language: any = null, authority: string = null, display: string = null, + place: number = 0, confidence: number = -1, - place: number = -1, metadata: string = null) { - this.value = value; + this.value = isNotNull(value) ? ((typeof value === 'string') ? value.trim() : value) : null; this.language = language; this.authority = authority; this.display = display || value; @@ -39,4 +39,8 @@ export class FormFieldMetadataValueObject { hasAuthority(): boolean { return isNotEmpty(this.authority); } + + hasValue(): boolean { + return isNotEmpty(this.value); + } } diff --git a/src/app/shared/form/builder/parsers/date-field-parser.ts b/src/app/shared/form/builder/parsers/date-field-parser.ts index 57e4a4cacb..6ad244f298 100644 --- a/src/app/shared/form/builder/parsers/date-field-parser.ts +++ b/src/app/shared/form/builder/parsers/date-field-parser.ts @@ -4,20 +4,19 @@ import { FormFieldModel } from '../models/form-field.model'; 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(): any { + public modelFactory(fieldValue: FormFieldMetadataValueObject): any { const inputDateModelConfig: DynamicDatePickerModelConfig = this.initModel(); inputDateModelConfig.toggleIcon = 'fa fa-calendar'; - - const dateModel = new DynamicDsDatePickerModel(inputDateModelConfig); - + this.setValues(inputDateModelConfig as any, fieldValue); // Init Data and validity check - if (isNotEmpty(this.getInitFieldValue())) { + if (isNotEmpty(inputDateModelConfig.value)) { let malformedData = false; - const value = this.getInitFieldValue().toString(); + const value = inputDateModelConfig.value.toString(); if (value.length >= 4) { const valuesArray = value.split(DS_DATE_PICKER_SEPARATOR); if (valuesArray.length < 4) { @@ -29,12 +28,8 @@ export class DateFieldParser extends FieldParser { } } - if (!malformedData) { - dateModel.valueUpdates.next(this.getInitFieldValue()); - } else { + if (malformedData) { // TODO Set error message - dateModel.malformedDate = true; - // TODO // const errorMessage = 'The stored date is not compliant'; // dateModel.validators = Object.assign({}, dateModel.validators, {malformedDate: null}); // dateModel.errorMessages = Object.assign({}, dateModel.errorMessages, {malformedDate: errorMessage}); @@ -44,6 +39,7 @@ export class DateFieldParser extends FieldParser { } } + const dateModel = new DynamicDsDatePickerModel(inputDateModelConfig); return dateModel; } } diff --git a/src/app/shared/form/builder/parsers/field-parser.ts b/src/app/shared/form/builder/parsers/field-parser.ts index 29a133171d..9845f1772d 100644 --- a/src/app/shared/form/builder/parsers/field-parser.ts +++ b/src/app/shared/form/builder/parsers/field-parser.ts @@ -118,9 +118,9 @@ export abstract class FieldParser { const values: FormFieldMetadataValueObject[] = []; fieldIds.forEach((id) => { if (this.initFormValues.hasOwnProperty(id)) { - const valueObj: FormFieldMetadataValueObject = Object.create({}); + const valueObj: FormFieldMetadataValueObject = Object.assign(new FormFieldMetadataValueObject(), this.initFormValues[id][innerIndex]); valueObj.metadata = id; - valueObj.value = this.initFormValues[id][innerIndex]; + // valueObj.value = this.initFormValues[id][innerIndex]; values.push(valueObj); } }); @@ -243,16 +243,21 @@ export abstract class FieldParser { if (typeof fieldValue === 'object') { modelConfig.language = fieldValue.language; - if (hasValue(fieldValue.language)) { - // Instance of FormFieldLanguageValueObject - modelConfig.value = fieldValue.value; - } else if (hasValue(fieldValue.metadata)) { - // Is a combobox field's value - modelConfig.value = fieldValue.value; - } else { - // Instance of FormFieldMetadataValueObject + 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 diff --git a/src/app/shared/form/builder/parsers/onebox-field-parser.ts b/src/app/shared/form/builder/parsers/onebox-field-parser.ts index 3b1bfc935c..c5a4f50e0c 100644 --- a/src/app/shared/form/builder/parsers/onebox-field-parser.ts +++ b/src/app/shared/form/builder/parsers/onebox-field-parser.ts @@ -68,7 +68,6 @@ export class OneboxFieldParser extends FieldParser { if (isNotEmpty(fieldValue)) { selectModelConfig.value = fieldValue.metadata; } - selectModelConfig.disabled = true; inputSelectGroup.group.push(new DynamicSelectModel(selectModelConfig, clsSelect)); const inputModelConfig: DsDynamicInputModelConfig = this.initModel(newId + COMBOBOX_VALUE_SUFFIX, true, true); diff --git a/src/app/shared/form/form.actions.ts b/src/app/shared/form/form.actions.ts index b1d5f2b3e3..7023d6bba1 100644 --- a/src/app/shared/form/form.actions.ts +++ b/src/app/shared/form/form.actions.ts @@ -114,6 +114,29 @@ export class FormAddError implements Action { } } +export class FormRemoveErrorAction implements Action { + type = FormActionTypes.FORM_REMOVE_ERROR; + payload: { + formId: string, + fieldId: string + }; + + constructor(formId: string, fieldId: string) { + this.payload = {formId, fieldId}; + } +} + +export class FormClearErrorsAction implements Action { + type = FormActionTypes.FORM_CLEAR_ERRORS; + payload: { + formId: string + }; + + constructor(formId: string) { + this.payload = {formId}; + } +} + /* tslint:enable:max-classes-per-file */ /** @@ -125,3 +148,5 @@ export type FormAction = FormInitAction | FormRemoveAction | FormStatusChangeAction | FormAddError + | FormClearErrorsAction + | FormRemoveErrorAction diff --git a/src/app/shared/form/form.component.ts b/src/app/shared/form/form.component.ts index 99712bde9c..981aed083e 100644 --- a/src/app/shared/form/form.component.ts +++ b/src/app/shared/form/form.component.ts @@ -10,7 +10,13 @@ import { import { Store } from '@ngrx/store'; import { AppState } from '../../app.reducer'; -import { FormChangeAction, FormInitAction, FormRemoveAction, FormStatusChangeAction } from './form.actions'; +import { + FormChangeAction, + FormInitAction, + FormRemoveAction, + FormRemoveErrorAction, + FormStatusChangeAction +} from './form.actions'; import { FormBuilderService } from './builder/form-builder.service'; import { Observable } from 'rxjs/Observable'; import { Subscription } from 'rxjs/Subscription'; @@ -144,7 +150,7 @@ export class FormComponent implements OnDestroy, OnInit { .filter((formState: FormEntry) => !!formState && !isEmpty(formState.errors)) .map((formState) => formState.errors) .distinctUntilChanged() - .delay(100) // this terrible delay is here to prevent the detection change error + // .delay(100) // this terrible delay is here to prevent the detection change error .subscribe((errors: FormError[]) => { const {formGroup, formModel} = this; @@ -172,10 +178,10 @@ export class FormComponent implements OnDestroy, OnInit { * Method provided by Angular. Invoked when the instance is destroyed */ ngOnDestroy() { - this.store.dispatch(new FormRemoveAction(this.formId)); this.subs .filter((sub) => hasValue(sub)) .forEach((sub) => sub.unsubscribe()); + this.store.dispatch(new FormRemoveAction(this.formId)); } /** @@ -206,7 +212,6 @@ export class FormComponent implements OnDestroy, OnInit { } onChange(event) { - console.log(event, this.formGroup); const action: FormChangeAction = new FormChangeAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel)); this.store.dispatch(action); @@ -215,7 +220,10 @@ export class FormComponent implements OnDestroy, OnInit { this.change.emit(event); const control: FormControl = event.control; - control.setErrors(null); + // control.setErrors(null); + if (control.valid) { + this.store.dispatch(new FormRemoveErrorAction(this.formId, event.model.id)); + } } /** diff --git a/src/app/shared/form/form.reducers.ts b/src/app/shared/form/form.reducers.ts index d06db331eb..c65f2405ec 100644 --- a/src/app/shared/form/form.reducers.ts +++ b/src/app/shared/form/form.reducers.ts @@ -1,9 +1,15 @@ import { - FormAction, FormActionTypes, FormAddError, FormChangeAction, FormInitAction, FormRemoveAction, + FormAction, + FormActionTypes, + FormAddError, + FormChangeAction, FormClearErrorsAction, + FormInitAction, + FormRemoveAction, + FormRemoveErrorAction, FormStatusChangeAction } from './form.actions'; import { hasValue } from '../empty.util'; -import { uniqWith, isEqual } from 'lodash'; +import { isEqual, uniqWith } from 'lodash'; export interface FormError { message: string; @@ -45,6 +51,14 @@ export function formReducer(state = initialState, action: FormAction): FormState return addFormErrors(state, action as FormAddError) } + case FormActionTypes.FORM_REMOVE_ERROR: { + return removeFormError(state, action as FormRemoveErrorAction) + } + + case FormActionTypes.FORM_CLEAR_ERRORS: { + return clearsFormErrors(state, action as FormClearErrorsAction) + } + default: { return state; } @@ -60,7 +74,7 @@ function addFormErrors(state: FormState, action: FormAddError) { }; return Object.assign({}, state, { - [ formId ]: { + [formId]: { data: state[formId].data, valid: state[formId].valid, errors: state[formId].errors ? uniqWith(state[formId].errors.concat(error), isEqual) : [].concat(error), @@ -71,6 +85,31 @@ function addFormErrors(state: FormState, action: FormAddError) { } } +function removeFormError(state: FormState, action: FormRemoveErrorAction) { + const formId = action.payload.formId; + const fieldId = action.payload.fieldId; + if (hasValue(state[formId])) { + const errors = state[formId].errors.filter((error) => error.fieldId !== fieldId); + const newState = Object.assign({}, state); + newState[formId] = Object.assign({}, state[formId], {errors}); + return newState; + } else { + return state; + } +} + +function clearsFormErrors(state: FormState, action: FormClearErrorsAction) { + const formId = action.payload.formId; + if (hasValue(state[formId])) { + const errors = []; + const newState = Object.assign({}, state); + newState[formId] = Object.assign({}, state[formId], {errors}); + return newState; + } else { + return state; + } +} + /** * Init form state. * @@ -82,22 +121,18 @@ function addFormErrors(state: FormState, action: FormAddError) { * the new state, with the form initialized. */ function initForm(state: FormState, action: FormInitAction): FormState { - if (!hasValue(state[ action.payload.formId ])) { + const formState = { + data: action.payload.formData, + valid: action.payload.valid, + errors: [] + }; + if (!hasValue(state[action.payload.formId])) { return Object.assign({}, state, { - [ action.payload.formId ]: { - data: action.payload.formData, - valid: action.payload.valid, - errors: [] - } + [action.payload.formId]: formState }); } else { const newState = Object.assign({}, state); - newState[ action.payload.formId ] = Object.assign({}, newState[ action.payload.formId ], { - data: action.payload.formData, - valid: action.payload.valid, - errors: [] - } - ); + newState[action.payload.formId] = Object.assign({}, newState[action.payload.formId], formState); return newState; } } @@ -113,18 +148,18 @@ function initForm(state: FormState, action: FormInitAction): FormState { * the new state, with the data changed. */ function changeDataForm(state: FormState, action: FormChangeAction): FormState { - if (!hasValue(state[ action.payload.formId ])) { + if (!hasValue(state[action.payload.formId])) { return Object.assign({}, state, { - [ action.payload.formId ]: { + [action.payload.formId]: { data: action.payload.formData, - valid: state[ action.payload.formId ].valid + valid: state[action.payload.formId].valid } }); } else { const newState = Object.assign({}, state); - newState[ action.payload.formId ] = Object.assign({}, newState[ action.payload.formId ], { + newState[action.payload.formId] = Object.assign({}, newState[action.payload.formId], { data: action.payload.formData, - valid: state[ action.payload.formId ].valid + valid: state[action.payload.formId].valid } ); return newState; @@ -142,17 +177,17 @@ function changeDataForm(state: FormState, action: FormChangeAction): FormState { * the new state, with the status changed. */ function changeStatusForm(state: FormState, action: FormStatusChangeAction): FormState { - if (!hasValue(state[ action.payload.formId ])) { + if (!hasValue(state[action.payload.formId])) { return Object.assign({}, state, { - [ action.payload.formId ]: { - data: state[ action.payload.formId ].data, + [action.payload.formId]: { + data: state[action.payload.formId].data, valid: action.payload.valid } }); } else { const newState = Object.assign({}, state); - newState[ action.payload.formId ] = Object.assign({}, newState[ action.payload.formId ], { - data: state[ action.payload.formId ].data, + newState[action.payload.formId] = Object.assign({}, newState[action.payload.formId], { + data: state[action.payload.formId].data, valid: action.payload.valid } ); @@ -171,9 +206,9 @@ function changeStatusForm(state: FormState, action: FormStatusChangeAction): For * the new state, with the form initialized. */ function removeForm(state: FormState, action: FormRemoveAction): FormState { - if (hasValue(state[ action.payload.formId ])) { + if (hasValue(state[action.payload.formId])) { const newState = Object.assign({}, state); - delete newState[ action.payload.formId ]; + delete newState[action.payload.formId]; return newState; } else { return state; diff --git a/src/app/shared/form/form.service.ts b/src/app/shared/form/form.service.ts index 2adef27179..f890d90012 100644 --- a/src/app/shared/form/form.service.ts +++ b/src/app/shared/form/form.service.ts @@ -84,7 +84,7 @@ export class FormService { error[errorKey] = message; // assign message - // if form control model has errorMessages object, create it + // if form control model has not errorMessages object, create it if (!model.errorMessages) { model.errorMessages = {}; } diff --git a/src/app/shared/number-picker/number-picker.component.html b/src/app/shared/number-picker/number-picker.component.html index 5eb45a0bfb..55e0c39078 100644 --- a/src/app/shared/number-picker/number-picker.component.html +++ b/src/app/shared/number-picker/number-picker.component.html @@ -1,7 +1,8 @@ -
+
@@ -28,7 +28,7 @@ type="button" tabindex="-1" [disabled]="disabled" - (click)="decrement()"> + (click)="toggleDown()"> Decrement diff --git a/src/app/shared/number-picker/number-picker.component.spec.ts b/src/app/shared/number-picker/number-picker.component.spec.ts new file mode 100644 index 0000000000..b7f696a42e --- /dev/null +++ b/src/app/shared/number-picker/number-picker.component.spec.ts @@ -0,0 +1,173 @@ +// 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 'rxjs/add/observable/of'; + +import { UploaderService } from '../uploader/uploader.service'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { By } from '@angular/platform-browser'; +import { NumberPickerComponent } from './number-picker.component'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +function createTestComponent(html: string, type: { new(...args: any[]): T }): ComponentFixture { + TestBed.overrideComponent(type, { + set: {template: html} + }); + const fixture = TestBed.createComponent(type); + + fixture.detectChanges(); + return fixture as ComponentFixture; +} + +describe('NumberPickerComponent test suite', () => { + + let testComp: TestComponent; + let numberPickerComp: NumberPickerComponent; + let testFixture: ComponentFixture; + let numberPickerFixture: ComponentFixture; + let html; + + // async beforeEach + beforeEach(async(() => { + + TestBed.configureTestingModule({ + imports: [ + FormsModule, + ReactiveFormsModule, + NgbModule.forRoot() + ], + declarations: [ + NumberPickerComponent, + TestComponent, + ], // declare the test component + providers: [ + ChangeDetectorRef, + NumberPickerComponent, + UploaderService + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }); + + })); + + // synchronous beforeEach + beforeEach(() => { + html = ` + `; + + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + it('should create NumberPickerComponent', inject([NumberPickerComponent], (app: NumberPickerComponent) => { + expect(app).toBeDefined(); + })); + + beforeEach(() => { + numberPickerFixture = TestBed.createComponent(NumberPickerComponent); + numberPickerComp = numberPickerFixture.componentInstance; // NumberPickerComponent test instance + numberPickerFixture.detectChanges(); + }); + + afterEach(() => { + numberPickerFixture.destroy(); + numberPickerComp = null; + }); + + it('should use default value when component\'s property is not passed', () => { + + expect(numberPickerComp.min).toBe(0); + expect(numberPickerComp.max).toBe(100); + expect(numberPickerComp.size).toBe(1); + expect(numberPickerComp.step).toBe(1); + }); + + it('should increase value', () => { + numberPickerComp.startValue = 0; + numberPickerComp.toggleUp(); + + expect(numberPickerComp.value).toBe(0); + + numberPickerComp.toggleUp(); + + expect(numberPickerComp.value).toBe(1); + }); + + it('should set min value when the value exceeds the max', () => { + numberPickerComp.value = 100; + numberPickerComp.toggleUp(); + + expect(numberPickerComp.value).toBe(0); + + }); + + it('should decrease value', () => { + numberPickerComp.startValue = 2; + numberPickerComp.toggleDown(); + + expect(numberPickerComp.value).toBe(2); + + numberPickerComp.toggleDown(); + + expect(numberPickerComp.value).toBe(1); + }); + + it('should set max value when the value is less than the min', () => { + numberPickerComp.value = 0; + numberPickerComp.toggleDown(); + + expect(numberPickerComp.value).toBe(100); + + }); + + it('should update value on input type', () => { + const de = numberPickerFixture.debugElement.query(By.css('input.form-control')); + const inputEl = de.nativeElement; + + inputEl.value = 99; + inputEl.dispatchEvent(new Event('change')); + + expect(numberPickerComp.value).toBe(99); + }); + + it('should not update value when input value is invalid', () => { + const de = numberPickerFixture.debugElement.query(By.css('input.form-control')); + const inputEl = de.nativeElement; + + inputEl.value = 101; + inputEl.dispatchEvent(new Event('change')); + + expect(numberPickerComp.value).toBe(undefined); + }); + +}); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + + public disabled = false; + public max = 100; + public min = 0; + public initValue = 0; + public showErrorMessages = false; + public size = 4; + public value; + +} diff --git a/src/app/shared/number-picker/number-picker.component.ts b/src/app/shared/number-picker/number-picker.component.ts index b4172b7182..56be174055 100644 --- a/src/app/shared/number-picker/number-picker.component.ts +++ b/src/app/shared/number-picker/number-picker.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, SimpleChanges, } from '@angular/core'; import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { isEmpty } from '../empty.util'; @Component({ selector: 'ds-number-picker', @@ -27,16 +28,17 @@ export class NumberPickerComponent implements OnInit, ControlValueAccessor { @Output() change = new EventEmitter(); @Output() focus = new EventEmitter(); - lastValue: number; + startValue: number; constructor(private fb: FormBuilder, private cd: ChangeDetectorRef) { } ngOnInit() { - // this.lastValue = this.value; + // this.startValue = this.value; this.step = this.step || 1; this.min = this.min || 0; this.max = this.max || 100; + this.size = this.size || 1; this.disabled = this.disabled || false; this.invalid = this.invalid || false; this.cd.detectChanges(); @@ -57,12 +59,13 @@ export class NumberPickerComponent implements OnInit, ControlValueAccessor { } } - increment(reverse?: boolean) { + private changeValue(reverse: boolean = false) { + // First after init - if (!this.value) { - this.value = this.lastValue; + if (isEmpty(this.value)) { + this.value = this.startValue; } else { - this.lastValue = this.value; + this.startValue = this.value; let newValue = this.value; if (reverse) { @@ -85,8 +88,12 @@ export class NumberPickerComponent implements OnInit, ControlValueAccessor { this.emitChange(); } - decrement() { - this.increment(true); + toggleDown() { + this.changeValue(true); + } + + toggleUp() { + this.changeValue(); } update(event) { @@ -109,18 +116,18 @@ export class NumberPickerComponent implements OnInit, ControlValueAccessor { onFocus(event) { if (this.value) { - this.lastValue = this.value; + this.startValue = this.value; } this.focus.emit(event); } writeValue(value) { - if (this.lastValue) { - this.lastValue = this.value; + if (this.startValue) { + this.startValue = this.value; this.value = value; } else { // First init - this.lastValue = value; + this.startValue = value || this.min; } } diff --git a/src/app/shared/pagination/pagination.component.spec.ts b/src/app/shared/pagination/pagination.component.spec.ts index 48767cf582..d5d0c4980c 100644 --- a/src/app/shared/pagination/pagination.component.spec.ts +++ b/src/app/shared/pagination/pagination.component.spec.ts @@ -300,7 +300,7 @@ describe('Pagination component', () => { it('should get parameters from route', () => { - activatedRouteStub = testFixture.debugElement.injector.get(ActivatedRoute) as any;; + activatedRouteStub = testFixture.debugElement.injector.get(ActivatedRoute) as any; activatedRouteStub.testParams = { pageId: 'test', page: 2, From d49cba9090634635664945240ce1a821a02d0e1e Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 26 Jun 2018 16:41:11 +0200 Subject: [PATCH 18/41] Added more tests --- .../form/builder/parsers/row-parser.spec.ts | 250 ++++++++++++- src/app/shared/form/form.component.spec.ts | 338 ++++++++++++++++-- src/app/shared/form/form.component.ts | 20 +- src/app/shared/testing/mock-store.ts | 9 +- 4 files changed, 561 insertions(+), 56 deletions(-) diff --git a/src/app/shared/form/builder/parsers/row-parser.spec.ts b/src/app/shared/form/builder/parsers/row-parser.spec.ts index b60cf8d01c..b7bb083de7 100644 --- a/src/app/shared/form/builder/parsers/row-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/row-parser.spec.ts @@ -9,6 +9,13 @@ describe('RowParser test suite', () => { let row1: FormRowModel; let row2: FormRowModel; let row3: FormRowModel; + let row4: FormRowModel; + let row5: FormRowModel; + let row6: FormRowModel; + let row7: FormRowModel; + let row8: FormRowModel; + let row9: FormRowModel; + let row10: FormRowModel; const scopeUUID = 'testScopeUUID'; const initFormValues = {}; @@ -77,9 +84,10 @@ describe('RowParser test suite', () => { } ], languageCodes: [] - } + } as FormFieldModel ] } as FormRowModel; + row3 = { fields: [ { @@ -96,7 +104,7 @@ describe('RowParser test suite', () => { } ], languageCodes: [] - }, + } as FormFieldModel, { input: {type: 'onebox'}, label: 'Other title', @@ -112,14 +120,248 @@ describe('RowParser test suite', () => { } ], languageCodes: [] - } + } as FormFieldModel ] } as FormRowModel; + row4 = { + fields: [ + { + 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, + { + input: {type: 'series'}, + label: 'Series/Report No.', + mandatory: 'false', + repeatable: false, + hints: 'Enter the series and number assigned to this item by your community.', + selectableMetadata: [ + { + metadata: 'series', + } + ], + languageCodes: [] + } as FormFieldModel + ] + } as FormRowModel; + + row5 = { + fields: [ + { + input: { + type: 'lookup-name' + }, + label: 'Author', + mandatory: 'false', + repeatable: false, + hints: 'Enter the name of the author.', + selectableMetadata: [ + { + metadata: 'author', + authority: 'RPAuthority', + closed: false + } + ], + languageCodes: [] + } as FormFieldModel + ] + } as FormRowModel; + + row6 = { + fields: [ + { + input: { + type: 'list' + }, + label: 'Type', + mandatory: 'false', + repeatable: true, + hints: 'Select the type.', + selectableMetadata: [ + { + metadata: 'type', + authority: 'type_programme', + closed: false + } + ], + languageCodes: [] + } as FormFieldModel + ] + } as FormRowModel; + + row7 = { + fields: [ + { + 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 + ] + } as FormRowModel; + + row8 = { + fields: [ + { + input: { + type: 'tag' + }, + label: 'Keywords', + mandatory: 'false', + repeatable: false, + hints: 'Local controlled vocabulary.', + selectableMetadata: [ + { + metadata: 'subject', + authority: 'JOURNALAuthority', + closed: false + } + ], + languageCodes: [] + } as FormFieldModel + ] + } as FormRowModel; + + row9 = { + fields: [ + { + input: { + type: 'textarea' + }, + label: 'Description', + mandatory: 'false', + repeatable: false, + hints: 'Enter a description.', + selectableMetadata: [ + { + metadata: 'description' + } + ], + languageCodes: [] + } as FormFieldModel + ] + } as FormRowModel; + + row10 = { + fields: [ + { + 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 + ] + } as FormRowModel; }); it('should init parser properly', () => { - const parser = new RowParser(row1, scopeUUID, initFormValues, submissionScope, readOnly); + let parser = new RowParser(row1, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(parser instanceof RowParser).toBe(true); + + parser = new RowParser(row2, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(parser instanceof RowParser).toBe(true); + + parser = new RowParser(row3, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(parser instanceof RowParser).toBe(true); + + parser = new RowParser(row4, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(parser instanceof RowParser).toBe(true); + + parser = new RowParser(row5, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(parser instanceof RowParser).toBe(true); + + parser = new RowParser(row6, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(parser instanceof RowParser).toBe(true); + + parser = new RowParser(row7, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(parser instanceof RowParser).toBe(true); + + parser = new RowParser(row8, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(parser instanceof RowParser).toBe(true); + + parser = new RowParser(row9, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(parser instanceof RowParser).toBe(true); + + parser = new RowParser(row10, scopeUUID, initFormValues, submissionScope, readOnly); expect(parser instanceof RowParser).toBe(true); }); diff --git a/src/app/shared/form/form.component.spec.ts b/src/app/shared/form/form.component.spec.ts index efc6383603..155e009fb1 100644 --- a/src/app/shared/form/form.component.spec.ts +++ b/src/app/shared/form/form.component.spec.ts @@ -1,13 +1,16 @@ -// 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 { CommonModule } from '@angular/common'; import { BrowserModule } from '@angular/platform-browser'; -import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { Observable } from 'rxjs/Observable'; +import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import 'rxjs/add/observable/of'; -import { DynamicFormControlModel, DynamicFormValidationService, DynamicInputModel } from '@ng-dynamic-forms/core'; +import { + DynamicFormArrayModel, + DynamicFormControlEvent, + DynamicFormControlModel, + DynamicFormValidationService, + DynamicInputModel +} from '@ng-dynamic-forms/core'; import { Store } from '@ngrx/store'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; @@ -16,6 +19,9 @@ import { FormComponent } from './form.component'; import { FormService } from './form.service'; import { FormBuilderService } from './builder/form-builder.service'; import { FormState } from './form.reducers'; +import { FormChangeAction, FormStatusChangeAction } from './form.actions'; +import { MockStore } from '../testing/mock-store'; +import { FormFieldMetadataValueObject } from './builder/models/form-field-metadata-value.model'; function createTestComponent(html: string, type: { new(...args: any[]): T }): ComponentFixture { TestBed.overrideComponent(type, { @@ -76,33 +82,47 @@ export const TEST_FORM_MODEL = [ ), ]; -export const TEST_FORM_GROUP = { - dc_title: new FormControl(), - dc_title_alternative: new FormControl(), - dc_publisher: new FormControl(), - dc_identifier_citation: new FormControl(), - dc_identifier_issn: new FormControl() -} +export const TEST_FORM_MODEL_WITH_ARRAY = [ + new DynamicFormArrayModel({ -describe('Form component', () => { + id: 'bootstrapFormArray', + initialCount: 1, + label: 'Form Array', + groupFactory: () => { + return [ + new DynamicInputModel({ + + id: 'bootstrapArrayGroupInput', + placeholder: 'example array group input', + readOnly: false + }) + ]; + } + }) +]; + +describe('FormComponent test suite', () => { let testComp: TestComponent; + let formComp: FormComponent; let testFixture: ComponentFixture; + let formFixture: ComponentFixture; + const formState: FormState = { + testForm: { + data: { + dc_title: null, + dc_title_alternative: null, + dc_publisher: null, + dc_identifier_citation: null, + dc_identifier_issn: null + }, + valid: false, + errors: [] + } + }; let html; - const formServiceStub = { - getFormData: (formId) => Observable.of([]) - } - const formBuilderServiceStub = { - createFormGroup: (formModel) => new FormGroup(TEST_FORM_GROUP) - } - const submissionFormsConfigServiceStub = {}; - const store: Store = jasmine.createSpyObj('store', { - /* tslint:disable:no-empty */ - dispatch: {}, - /* tslint:enable:no-empty */ - select: Observable.of({}) - }); + const store: MockStore = new MockStore(formState); // async beforeEach beforeEach(async(() => { @@ -135,23 +155,265 @@ describe('Form component', () => { })); - // synchronous beforeEach - beforeEach(() => { - html = ` - `; + describe('', () => { + // synchronous beforeEach + beforeEach(() => { + html = ` + `; - testFixture = createTestComponent(html, TestComponent) as ComponentFixture; - testComp = testFixture.componentInstance; + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + it('should create FormComponent', inject([FormComponent], (app: FormComponent) => { + + expect(app).toBeDefined(); + })); }); - it('should create Form Component', inject([FormComponent], (app: FormComponent) => { + describe('', () => { + beforeEach(() => { - expect(app).toBeDefined(); - })); + formFixture = TestBed.createComponent(FormComponent); + formComp = formFixture.componentInstance; // FormComponent test instance + formComp.formId = 'testForm'; + formComp.formModel = TEST_FORM_MODEL; + formComp.displaySubmit = false; + formFixture.detectChanges(); + spyOn(store, 'dispatch'); + }); + afterEach(() => { + formFixture.destroy(); + formComp = null; + }); + + it('should dispatch a FormStatusChangeAction when Form group status changes', () => { + const control = formComp.formGroup.get(['dc_title']); + control.setValue('Test Title'); + + expect(store.dispatch).toHaveBeenCalledWith(new FormStatusChangeAction('testForm', formComp.formGroup.valid)); + + }); + + it('should display form errors when errors are added to the state', () => { + const errors = [{ + fieldId: 'dc_title', + message: 'error.validation.required' + }]; + + formState.testForm.errors = errors; + store.nextState(formState); + formFixture.detectChanges(); + + expect((formComp as any).formErrors).toEqual(errors); + + }); + + it('should remove form errors when errors are empty in the state', () => { + (formComp as any).formErrors = [{ + fieldId: 'dc_title', + message: 'error.validation.required' + }]; + const errors = []; + + formState.testForm.errors = errors; + store.nextState(formState); + formFixture.detectChanges(); + + expect((formComp as any).formErrors).toEqual(errors); + + }); + + it('should dispatch FormChangeAction on form change', inject([FormBuilderService], (service: FormBuilderService) => { + const event = { + $event: new FormFieldMetadataValueObject('Test Title'), + context: null, + control: formComp.formGroup.get('dc_title'), + group: formComp.formGroup, + model: formComp.formModel[0], + type: 'change' + } as DynamicFormControlEvent; + + spyOn(formComp.change, 'emit'); + + formComp.onChange(event); + + expect(store.dispatch).toHaveBeenCalledWith(new FormChangeAction('testForm', service.getValueFromModel(formComp.formModel))); + expect(formComp.change.emit).toHaveBeenCalled(); + })); + + it('should emit change on form change', inject([FormBuilderService], (service: FormBuilderService) => { + const event = { + $event: new FormFieldMetadataValueObject('Test Title'), + context: null, + control: formComp.formGroup.get('dc_title'), + group: formComp.formGroup, + model: formComp.formModel[0], + type: 'change' + } as DynamicFormControlEvent; + + spyOn(formComp.change, 'emit'); + + formComp.onChange(event); + + expect(formComp.change.emit).toHaveBeenCalled(); + })); + + it('should not emit change Event on form change when emitChange is false', inject([FormBuilderService], (service: FormBuilderService) => { + const event = { + $event: new FormFieldMetadataValueObject('Test Title'), + context: null, + control: formComp.formGroup.get('dc_title'), + group: formComp.formGroup, + model: formComp.formModel[0], + type: 'change' + } as DynamicFormControlEvent; + + formComp.emitChange = false; + spyOn(formComp.change, 'emit'); + + formComp.onChange(event); + + expect(formComp.change.emit).not.toHaveBeenCalled(); + })); + + it('should emit blur Event on blur', () => { + const event = { + $event: new FocusEvent('blur'), + context: null, + control: formComp.formGroup.get('dc_title'), + group: formComp.formGroup, + model: formComp.formModel[0], + type: 'blur' + } as DynamicFormControlEvent; + + spyOn(formComp.blur, 'emit'); + + formComp.onBlur(event); + + expect(formComp.blur.emit).toHaveBeenCalled(); + }); + + it('should emit focus Event on focus', () => { + const event = { + $event: new FocusEvent('focus'), + context: null, + control: formComp.formGroup.get('dc_title'), + group: formComp.formGroup, + model: formComp.formModel[0], + type: 'focus' + } as DynamicFormControlEvent; + + spyOn(formComp.focus, 'emit'); + + formComp.onFocus(event); + + expect(formComp.focus.emit).toHaveBeenCalled(); + }); + + it('should return Observable of form status', () => { + + const control = formComp.formGroup.get(['dc_title']); + control.setValue('Test Title'); + formState.testForm.valid = true; + store.nextState(formState); + formFixture.detectChanges(); + + formComp.isValid().subscribe((valid) => { + expect(valid).toBe(true); + }); + }); + + it('should emit submit Event on form submit whether the form is valid', () => { + + const control = formComp.formGroup.get(['dc_title']); + control.setValue('Test Title'); + formState.testForm.valid = true; + spyOn(formComp.submit, 'emit'); + + store.nextState(formState); + formFixture.detectChanges(); + + formComp.onSubmit(); + expect(formComp.submit.emit).toHaveBeenCalled(); + }); + + it('should not emit submit Event on form submit whether the form is not valid', () => { + + spyOn((formComp as any).formService, 'validateAllFormFields'); + + store.nextState(formState); + formFixture.detectChanges(); + + formComp.onSubmit(); + expect((formComp as any).formService.validateAllFormFields).toHaveBeenCalled(); + }); + + it('should reset form group', () => { + + spyOn(formComp.formGroup, 'reset'); + + formComp.reset(); + + expect(formComp.formGroup.reset).toHaveBeenCalled(); + }); + }); + + describe('', () => { + beforeEach(() => { + + formFixture = TestBed.createComponent(FormComponent); + formComp = formFixture.componentInstance; // FormComponent test instance + formComp.formId = 'testFormArray'; + formComp.formModel = TEST_FORM_MODEL_WITH_ARRAY; + formComp.displaySubmit = false; + formFixture.detectChanges(); + spyOn(store, 'dispatch'); + }); + + afterEach(() => { + formFixture.destroy(); + formComp = null; + }); + + it('should return ReadOnly property from array item', inject([FormBuilderService], (service: FormBuilderService) => { + const readOnly = formComp.isItemReadOnly(formComp.formModel[0] as DynamicFormArrayModel, 0); + + expect(readOnly).toBe(false); + })); + + it('should dispatch FormChangeAction when an item has been added to an array', inject([FormBuilderService], (service: FormBuilderService) => { + formComp.insertItem(new Event('click'), formComp.formModel[0] as DynamicFormArrayModel, 1); + + expect(store.dispatch).toHaveBeenCalledWith(new FormChangeAction('testFormArray', service.getValueFromModel(formComp.formModel))); + })); + + it('should emit addArrayItem Event when an item has been added to an array', inject([FormBuilderService], (service: FormBuilderService) => { + spyOn(formComp.addArrayItem, 'emit'); + + formComp.insertItem(new Event('click'), formComp.formModel[0] as DynamicFormArrayModel, 1); + + expect(formComp.addArrayItem.emit).toHaveBeenCalled(); + })); + + it('should dispatch FormChangeAction when an item has been removed from an array', inject([FormBuilderService], (service: FormBuilderService) => { + formComp.removeItem(new Event('click'), formComp.formModel[0] as DynamicFormArrayModel, 1); + + expect(store.dispatch).toHaveBeenCalledWith(new FormChangeAction('testFormArray', service.getValueFromModel(formComp.formModel))); + })); + + it('should emit removeArrayItem Event when an item has been removed from an array', inject([FormBuilderService], (service: FormBuilderService) => { + spyOn(formComp.removeArrayItem, 'emit'); + + formComp.removeItem(new Event('click'), formComp.formModel[0] as DynamicFormArrayModel, 1); + + expect(formComp.removeArrayItem.emit).toHaveBeenCalled(); + })); + }) }); // declare a test component diff --git a/src/app/shared/form/form.component.ts b/src/app/shared/form/form.component.ts index 7a894ca3e5..922f2e40f8 100644 --- a/src/app/shared/form/form.component.ts +++ b/src/app/shared/form/form.component.ts @@ -21,7 +21,7 @@ import { import { FormBuilderService } from './builder/form-builder.service'; import { Observable } from 'rxjs/Observable'; import { Subscription } from 'rxjs/Subscription'; -import { hasValue, isNotNull, isNull } from '../empty.util'; +import { hasValue, isNotEmpty, isNotNull, isNull } from '../empty.util'; import { FormService } from './form.service'; import { formObjectFromIdSelector } from './selectors'; import { FormEntry, FormError } from './form.reducers'; @@ -154,7 +154,7 @@ export class FormComponent implements OnDestroy, OnInit { this.subs.push( this.store.select(formObjectFromIdSelector(this.formId)) - .filter((formState: FormEntry) => !!formState && !isEmpty(formState.errors)) + .filter((formState: FormEntry) => !!formState && (isNotEmpty(formState.errors) || isNotEmpty(this.formErrors))) .map((formState) => formState.errors) .distinctUntilChanged() // .delay(100) // this terrible delay is here to prevent the detection change error @@ -223,7 +223,7 @@ export class FormComponent implements OnDestroy, OnInit { /** * Method to keep synchronized form controls values with form state */ - private keepSync() { + private keepSync(): void { this.subs.push(this.formService.getFormData(this.formId) .subscribe((stateFormData) => { if (!Object.is(stateFormData, this.formGroup.value) && this.formGroup) { @@ -232,15 +232,15 @@ export class FormComponent implements OnDestroy, OnInit { })); } - onBlur(event) { + onBlur(event: DynamicFormControlEvent): void { this.blur.emit(event); } - onFocus(event) { + onFocus(event: DynamicFormControlEvent): void { this.focus.emit(event); } - onChange(event) { + onChange(event: DynamicFormControlEvent): void { const action: FormChangeAction = new FormChangeAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel)); this.store.dispatch(action); @@ -260,7 +260,7 @@ export class FormComponent implements OnDestroy, OnInit { * Method called on submit. * Emit a new submit Event whether the form is valid, mark fields with error otherwise */ - onSubmit() { + onSubmit(): void { if (this.getFormGroupValidStatus()) { this.submit.emit(this.formService.getFormData(this.formId)); } else { @@ -271,7 +271,7 @@ export class FormComponent implements OnDestroy, OnInit { /** * Method to reset form fields */ - reset() { + reset(): void { this.formGroup.reset(); } @@ -281,14 +281,14 @@ export class FormComponent implements OnDestroy, OnInit { return model.readOnly; } - removeItem($event, arrayContext: DynamicFormArrayModel, index: number) { + removeItem($event, arrayContext: DynamicFormArrayModel, index: number): void { const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray; this.removeArrayItem.emit(this.getEvent($event, arrayContext, index, 'remove')); this.formBuilderService.removeFormArrayGroup(index, formArrayControl, arrayContext); this.store.dispatch(new FormChangeAction(this.formId, this.formBuilderService.getValueFromModel(this.formModel))); } - insertItem($event, arrayContext: DynamicFormArrayModel, index: number) { + insertItem($event, arrayContext: DynamicFormArrayModel, index: number): void { const formArrayControl = this.formGroup.get(this.formBuilderService.getPath(arrayContext)) as FormArray; this.formBuilderService.insertFormArrayGroup(index, formArrayControl, arrayContext); this.addArrayItem.emit(this.getEvent($event, arrayContext, index, 'add')); diff --git a/src/app/shared/testing/mock-store.ts b/src/app/shared/testing/mock-store.ts index c619b5aa77..a3bca3c1b5 100644 --- a/src/app/shared/testing/mock-store.ts +++ b/src/app/shared/testing/mock-store.ts @@ -9,12 +9,13 @@ export class MockStore extends BehaviorSubject { } dispatch = (action: Action): void => { - console.info(); - } + // console.info(action); + }; select = (pathOrMapFn: any): Observable => { - return Observable.of(this.getValue()); - } + return this.asObservable() + .map((value) => pathOrMapFn.projector(value)) + }; nextState(_newState: T) { this.next(_newState); From 2ffbdf942c574d76fcc8b41e2781df7a4150a21f Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Tue, 26 Jun 2018 19:19:13 +0200 Subject: [PATCH 19/41] Added more tests --- .../shared/chips/models/chips-item.model.ts | 4 +- .../chips/models/chpis-item.model.spec.ts | 78 ++++++++++++ .../shared/chips/models/chpis.model.spec.ts | 115 ++++++++++++++++++ 3 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 src/app/shared/chips/models/chpis-item.model.spec.ts create mode 100644 src/app/shared/chips/models/chpis.model.spec.ts diff --git a/src/app/shared/chips/models/chips-item.model.ts b/src/app/shared/chips/models/chips-item.model.ts index 0efdd0984b..ed082a51eb 100644 --- a/src/app/shared/chips/models/chips-item.model.ts +++ b/src/app/shared/chips/models/chips-item.model.ts @@ -17,8 +17,8 @@ export class ChipsItem { private objToDisplay: string; constructor(item: any, - fieldToDisplay: string, - objToDisplay: string, + fieldToDisplay: string = 'display', + objToDisplay?: string, icons?: ChipsItemIcon[], editMode?: boolean) { diff --git a/src/app/shared/chips/models/chpis-item.model.spec.ts b/src/app/shared/chips/models/chpis-item.model.spec.ts new file mode 100644 index 0000000000..6e1e2dcc3b --- /dev/null +++ b/src/app/shared/chips/models/chpis-item.model.spec.ts @@ -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[] = [{style: 'fa fa-plus'}]; + item.updateIcons(icons); + + expect(item.icons).toEqual(icons); + }); + + it('should return true if has icons', () => { + const icons: ChipsItemIcon[] = [{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'); + }); +}); diff --git a/src/app/shared/chips/models/chpis.model.spec.ts b/src/app/shared/chips/models/chpis.model.spec.ts new file mode 100644 index 0000000000..661dc561ba --- /dev/null +++ b/src/app/shared/chips/models/chpis.model.spec.ts @@ -0,0 +1,115 @@ +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' + }, + ]; + chips = new Chips(items, 'value', 'toDisplay', {toDisplay: 'fa fa-plus'}); + + expect(chips.displayField).toBe('value'); + expect(chips.displayObj).toBe('toDisplay'); + expect(chips.iconsConfig).toEqual({toDisplay: 'fa fa-plus'}); + expect(chips.getChipsItems()).toEqual(items); + }); +}); From f6b665cb90ac6b62cd20eac01810a99b90eb1c72 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 27 Jun 2018 17:43:09 +0200 Subject: [PATCH 20/41] Added more tests --- .../dynamic-group.component.spec.ts | 302 ++++++++++++++---- .../dynamic-group/dynamic-group.components.ts | 11 +- .../dynamic-group/dynamic-group.model.ts | 2 - 3 files changed, 239 insertions(+), 76 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.spec.ts index 51b0838fd5..0ba55ef982 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.spec.ts @@ -8,8 +8,8 @@ import 'rxjs/add/observable/of'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { DsDynamicGroupComponent } from './dynamic-group.components'; -import { DynamicGroupModel } from './dynamic-group.model'; -import { FormRowModel } from '../../../../../../core/shared/config/config-submission-forms.model'; +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'; @@ -20,6 +20,11 @@ import { DynamicFormValidationService } from '@ng-dynamic-forms/core'; import { Store } from '@ngrx/store'; import { AppState } from '../../../../../../app.reducer'; import { Observable } from 'rxjs/Observable'; +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 { By } from '@angular/platform-browser'; function createTestComponent(html: string, type: { new(...args: any[]): T }): ComponentFixture { TestBed.overrideComponent(type, { @@ -31,13 +36,70 @@ function createTestComponent(html: string, type: { new(...args: any[]): T }): return fixture as ComponentFixture; } -describe('Dynamic Group component', () => { +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', () => { let testComp: TestComponent; + let groupComp: DsDynamicGroupComponent; let testFixture: ComponentFixture; + let groupFixture: ComponentFixture; + let modelValue: any; let html; + let control1: FormControl; + let model1: DsDynamicInputModel; + let control2: FormControl; + let model2: DsDynamicInputModel; - const mockStore: Store = jasmine.createSpyObj('store', { + const store: Store = jasmine.createSpyObj('store', { dispatch: {}, select: Observable.of(true) }); @@ -47,6 +109,7 @@ describe('Dynamic Group component', () => { TestBed.configureTestingModule({ imports: [ + BrowserAnimationsModule, FormsModule, ReactiveFormsModule, NgbModule.forRoot(), @@ -65,34 +128,181 @@ describe('Dynamic Group component', () => { FormComponent, FormService, {provide: GLOBAL_CONFIG, useValue: {} as GlobalConfig}, - {provide: Store, useValue: mockStore}, + {provide: Store, useValue: store}, ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }); })); - // synchronous beforeEach - beforeEach(() => { - html = ` - `; + describe('', () => { + // synchronous beforeEach + beforeEach(() => { + html = ` + `; - testFixture = createTestComponent(html, TestComponent) as ComponentFixture; - testComp = testFixture.componentInstance; + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + it('should create DsDynamicGroupComponent', inject([DsDynamicGroupComponent], (app: DsDynamicGroupComponent) => { + + expect(app).toBeDefined(); + })); }); - it('should create DsDynamicGroupComponent', inject([DsDynamicGroupComponent], (app: DsDynamicGroupComponent) => { + describe('when init model value is empty', () => { + beforeEach(inject([FormBuilderService], (service: FormBuilderService) => { - expect(app).toBeDefined(); - })); + 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 config = {rows: groupComp.model.formConfiguration} as SubmissionFormsModel; + const formModel = service.modelFromConfiguration(config, 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 de = groupFixture.debugElement.queryAll(By.css('button')); + 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 de = groupFixture.debugElement.queryAll(By.css('button')); + 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(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); + 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 config = {rows: groupComp.model.formConfiguration} as SubmissionFormsModel; + const formModel = service.modelFromConfiguration(config, 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 de = groupFixture.debugElement.queryAll(By.css('button')); + 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', inject([FormBuilderService], (service: FormBuilderService) => { + groupComp.onChipSelected(0); + groupFixture.detectChanges(); + + const de = groupFixture.debugElement.queryAll(By.css('button')); + 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 @@ -102,55 +312,9 @@ describe('Dynamic Group component', () => { }) class TestComponent { - group = new FormGroup({ - date: new FormControl(), - }); + group = FORM_GROUP_TEST_GROUP; - groupModelConfig = { - disabled: false, - errorMessages: {required: 'You must specify at least one author.'}, - 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], - id: 'date', - label: 'Date', - mandatoryField: 'dc.contributor.author', - name: 'date', - placeholder: 'Date', - readOnly: false, - relationFields: ['local.contributor.affiliation'], - required: true, - scopeUUID: '43fe1f8c-09a6-4fcf-9c78-5d4fed8f2c8f', - submissionScope: undefined, - validators: {required: null} - }; + groupModelConfig = FORM_GROUP_TEST_MODEL_CONFIG; model = new DynamicGroupModel(this.groupModelConfig); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.components.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.components.ts index 2d5f33eac0..acb146c98c 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.components.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.components.ts @@ -122,11 +122,12 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit { || 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); - } + // if (value instanceof FormFieldMetadataValueObject || value instanceof AuthorityValueModel) { + // model.valueUpdates.next(value.display); + // } else { + // model.valueUpdates.next(value); + // } + model.valueUpdates.next(value); }); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model.ts index 59e442b4e9..f8f4c80385 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model.ts @@ -13,11 +13,9 @@ export const PLACEHOLDER_PARENT_METADATA = '#PLACEHOLDER_PARENT_METADATA_VALUE#' export interface DynamicGroupModelConfig extends DsDynamicInputModelConfig { formConfiguration: FormRowModel[], mandatoryField: string, - name: string, relationFields: string[], scopeUUID: string, submissionScope: string; - value?: any; } /** From 8f40ea0ea5efeb1aec10082f46b557799859d93a Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Fri, 29 Jun 2018 18:58:53 +0200 Subject: [PATCH 21/41] Added more tests and fixes --- .../date-picker/date-picker.component.spec.ts | 222 +++++++++++-- .../date-picker/date-picker.component.ts | 6 +- .../dynamic-group.component.spec.ts | 19 +- .../lookup/dynamic-lookup.component.html | 2 +- .../lookup/dynamic-lookup.component.spec.ts | 300 ++++++++++++++++-- .../models/lookup/dynamic-lookup.component.ts | 6 - ...dynamic-scrollable-dropdown.component.html | 4 +- ...amic-scrollable-dropdown.component.spec.ts | 198 ++++++++++-- .../dynamic-scrollable-dropdown.component.ts | 8 +- .../models/tag/dynamic-tag.component.html | 6 +- .../models/tag/dynamic-tag.component.spec.ts | 255 +++++++++++++-- .../models/tag/dynamic-tag.component.ts | 22 +- .../dynamic-typeahead.component.html | 6 +- .../dynamic-typeahead.component.spec.ts | 183 +++++++++-- .../typeahead/dynamic-typeahead.component.ts | 9 +- .../shared/testing/authority-service-stub.ts | 13 +- 16 files changed, 1035 insertions(+), 224 deletions(-) diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts index f7d5fc1501..76cd0947bb 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts @@ -1,13 +1,17 @@ // 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 'rxjs/add/observable/of'; +import { FormControl, FormGroup } from '@angular/forms'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { SortablejsModule } from 'angular-sortablejs'; +import { DynamicFormValidationService } from '@ng-dynamic-forms/core'; + import { DsDatePickerComponent } from './date-picker.component'; -import { FormControl, FormGroup } from '@angular/forms'; import { DynamicDsDatePickerModel } from './date-picker.model'; +import { FormBuilderService } from '../../../form-builder.service'; + +import { FormComponent } from '../../../../form.component'; +import { FormService } from '../../../../form.service'; function createTestComponent(html: string, type: { new(...args: any[]): T }): ComponentFixture { TestBed.overrideComponent(type, { @@ -19,10 +23,28 @@ function createTestComponent(html: string, type: { new(...args: any[]): T }): return fixture as ComponentFixture; } -describe('Date Picker component', () => { +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; + let dateFixture: ComponentFixture; let html; // async beforeEach @@ -39,15 +61,20 @@ describe('Date Picker component', () => { providers: [ ChangeDetectorRef, DsDatePickerComponent, + DynamicFormValidationService, + FormBuilderService, + FormComponent, + FormService ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }); })); - // synchronous beforeEach - beforeEach(() => { - html = ` + describe('', () => { + // synchronous beforeEach + beforeEach(() => { + html = ` { (change)='onValueChange($event)' (focus)='onFocus($event)'>`; - testFixture = createTestComponent(html, TestComponent) as ComponentFixture; - testComp = testFixture.componentInstance; + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + it('should create DsDatePickerComponent', inject([DsDatePickerComponent], (app: DsDatePickerComponent) => { + + expect(app).toBeDefined(); + })); + }); - it('should create DsDatePickerComponent', inject([DsDatePickerComponent], (app: DsDatePickerComponent) => { + describe('', () => { + describe('when init model value is empty', () => { + beforeEach(() => { - expect(app).toBeDefined(); - })); + 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(); + }); + }); + }); }); @@ -75,23 +255,9 @@ describe('Date Picker component', () => { }) class TestComponent { - group = new FormGroup({ - date: new FormControl(), - }); + group = DATE_TEST_GROUP; - inputDateModelConfig = { - 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' - }; - - model = new DynamicDsDatePickerModel(this.inputDateModelConfig); + model = new DynamicDsDatePickerModel(DATE_TEST_MODEL_CONFIG); showErrorMessages = false; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts index 17a4e9e750..741d86fab9 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts @@ -31,9 +31,9 @@ export class DsDatePickerComponent implements OnInit { initialMonth: number; initialDay: number; - year: number; - month: number; - day: number; + year: any; + month: any; + day: any; minYear: 0; maxYear: number; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.spec.ts index 0ba55ef982..55f98df8ac 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.spec.ts @@ -3,7 +3,10 @@ import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/c 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'; @@ -16,15 +19,11 @@ import { FormService } from '../../../../form.service'; import { GlobalConfig } from '../../../../../../../config/global-config.interface'; import { GLOBAL_CONFIG } from '../../../../../../../config'; import { FormComponent } from '../../../../form.component'; -import { DynamicFormValidationService } from '@ng-dynamic-forms/core'; -import { Store } from '@ngrx/store'; import { AppState } from '../../../../../../app.reducer'; -import { Observable } from 'rxjs/Observable'; 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 { By } from '@angular/platform-browser'; function createTestComponent(html: string, type: { new(...args: any[]): T }): ComponentFixture { TestBed.overrideComponent(type, { @@ -202,7 +201,6 @@ describe('DsDynamicGroupComponent test suite', () => { }]; groupFixture.detectChanges(); - const de = groupFixture.debugElement.queryAll(By.css('button')); const buttons = groupFixture.debugElement.nativeElement.querySelectorAll('button'); const btnEl = buttons[0]; btnEl.click(); @@ -219,7 +217,6 @@ describe('DsDynamicGroupComponent test suite', () => { groupFixture.detectChanges(); - const de = groupFixture.debugElement.queryAll(By.css('button')); const buttons = groupFixture.debugElement.nativeElement.querySelectorAll('button'); const btnEl = buttons[2]; btnEl.click(); @@ -231,7 +228,7 @@ describe('DsDynamicGroupComponent test suite', () => { }); describe('when init model value is not empty', () => { - beforeEach(inject([FormBuilderService], (service: FormBuilderService) => { + beforeEach(() => { groupFixture = TestBed.createComponent(DsDynamicGroupComponent); groupComp = groupFixture.componentInstance; // FormComponent test instance @@ -246,7 +243,7 @@ describe('DsDynamicGroupComponent test suite', () => { groupComp.showErrorMessages = false; groupFixture.detectChanges(); - })); + }); afterEach(() => { groupFixture.destroy(); @@ -279,7 +276,6 @@ describe('DsDynamicGroupComponent test suite', () => { }]; groupFixture.detectChanges(); - const de = groupFixture.debugElement.queryAll(By.css('button')); const buttons = groupFixture.debugElement.nativeElement.querySelectorAll('button'); const btnEl = buttons[0]; btnEl.click(); @@ -290,18 +286,17 @@ describe('DsDynamicGroupComponent test suite', () => { expect(groupComp.formCollapsed).toEqual(Observable.of(true)); })); - it('should delete existing chips item', inject([FormBuilderService], (service: FormBuilderService) => { + it('should delete existing chips item', () => { groupComp.onChipSelected(0); groupFixture.detectChanges(); - const de = groupFixture.debugElement.queryAll(By.css('button')); 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)); - })); + }); }); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html index 710639ff88..bd18ef3a1f 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.html @@ -69,7 +69,7 @@ [attr.autoComplete]="model.autoComplete" [class.is-invalid]="showErrorMessages" [dynamicId]="bindId && model.id" - [name]="name2" + [name]="model.name + '_2'" [type]="model.inputType" [(ngModel)]="secondInputValue" [disabled]="firstInputValue.length === 0 || isInputDisabled()" diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts index b5e76f7b15..b2cce76be6 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts @@ -1,12 +1,12 @@ // 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, inject, TestBed, } from '@angular/core/testing'; +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 { 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'; @@ -14,6 +14,13 @@ 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'; function createTestComponent(html: string, type: { new(...args: any[]): T }): ComponentFixture { TestBed.overrideComponent(type, { @@ -25,15 +32,67 @@ function createTestComponent(html: string, type: { new(...args: any[]): T }): return fixture as ComponentFixture; } +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; + let lookupFixture: ComponentFixture; let html; + const authorityServiceStub = new AuthorityServiceStub(); + // async beforeEach beforeEach(async(() => { - const authorityServiceStub = new AuthorityServiceStub(); TestBed.configureTestingModule({ imports: [ @@ -52,6 +111,10 @@ describe('Dynamic Lookup component', () => { providers: [ ChangeDetectorRef, DsDynamicLookupComponent, + DynamicFormValidationService, + FormBuilderService, + FormComponent, + FormService, {provide: AuthorityService, useValue: authorityServiceStub}, ], schemas: [CUSTOM_ELEMENTS_SCHEMA] @@ -59,9 +122,10 @@ describe('Dynamic Lookup component', () => { })); - // synchronous beforeEach - beforeEach(() => { - html = ` + describe('', () => { + // synchronous beforeEach + beforeEach(() => { + html = ` { (change)="onValueChange($event)" (focus)="onFocus($event)">`; - testFixture = createTestComponent(html, TestComponent) as ComponentFixture; - testComp = testFixture.componentInstance; + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + it('should create DsDynamicLookupComponent', inject([DsDynamicLookupComponent], (app: DsDynamicLookupComponent) => { + expect(app).toBeDefined(); + })); }); - it('should create DsDynamicLookupComponent', inject([DsDynamicLookupComponent], (app: DsDynamicLookupComponent) => { + describe('when model is DynamicLookupModel', () => { - expect(app).toBeDefined(); - })); + 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 @@ -89,31 +341,9 @@ describe('Dynamic Lookup component', () => { }) class TestComponent { - group: FormGroup = new FormGroup({ - lookup: new FormControl(), - }); + group: FormGroup = LOOKUP_TEST_GROUP; - inputLookupModelConfig = { - 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 - }; + inputLookupModelConfig = LOOKUP_TEST_MODEL_CONFIG; model = new DynamicLookupModel(this.inputLookupModelConfig); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts index a0a9f3be31..4e88e9c78e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts @@ -32,7 +32,6 @@ export class DsDynamicLookupComponent implements OnDestroy, OnInit { public loading = false; public pageInfo: PageInfo; public optionsList: any; - public name2: string; protected searchOptions: IntegrationSearchOptions; protected sub: Subscription; @@ -50,11 +49,6 @@ export class DsDynamicLookupComponent implements OnDestroy, OnInit { this.model.maxOptions, 1); - // Switch Lookup/LookupName - if (this.isLookupName()) { - this.name2 = this.model.name + '2'; - } - this.setInputsValue(this.model.value); this.model.valueUpdates diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html index aed4e16bca..6a5e588610 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.html @@ -7,9 +7,9 @@ [readonly]="model.readOnly" [type]="model.inputType" [value]="formatItemForInput(model.value)" - (blur)="onBlurEvent($event)" + (blur)="onBlur($event)" (click)="$event.stopPropagation(); openDropdown(sdRef);" - (focus)="onFocusEvent($event)" + (focus)="onFocus($event)" (keypress)="$event.preventDefault()">