From 833de6465374f28dcadafcedd9d948e077dc01ee Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 16 Jan 2020 12:50:40 +0200 Subject: [PATCH] UI: RuleChain improvements. --- ui-ngx/src/app/core/utils.ts | 6 +- .../rulechain/rule-node-config.component.ts | 38 +++----- .../rulechain/rulechain-page.component.ts | 12 +-- .../components/file-input.component.html | 7 +- .../shared/components/file-input.component.ts | 56 +++++++++++- .../src/app/shared/models/rule-node.models.ts | 91 +++++++++++++++++-- 6 files changed, 161 insertions(+), 49 deletions(-) diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 21618ba623..e58e3dd400 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -356,7 +356,7 @@ function utf8ToBytes(input: string, units?: number): number[] { return bytes; } -export function deepClone(target: T): T { +export function deepClone(target: T, ignoreFields?: string[]): T { if (target === null) { return target; } @@ -371,7 +371,9 @@ export function deepClone(target: T): T { if (typeof target === 'object' && target !== {}) { const cp = { ...(target as { [key: string]: any }) } as { [key: string]: any }; Object.keys(cp).forEach(k => { - cp[k] = deepClone(cp[k]); + if (!ignoreFields || ignoreFields.indexOf(k) === -1) { + cp[k] = deepClone(cp[k]); + } }); return cp as T; } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts index 180c97708f..26c41289ae 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts @@ -16,39 +16,25 @@ import { AfterViewInit, - Component, ElementRef, - EventEmitter, forwardRef, + Component, + ComponentRef, + forwardRef, Input, - OnChanges, + OnDestroy, OnInit, - Output, - SimpleChanges, ViewChild, - Compiler, - Injector, ComponentRef, OnDestroy + ViewContainerRef } from '@angular/core'; -import { PageComponent } from '@shared/components/page.component'; -import { Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, NgForm, Validators } from '@angular/forms'; -import { FcRuleNode, FcRuleEdge } from './rulechain-page.models'; -import { RuleNodeType, LinkLabel, RuleNodeDefinition, RuleNodeConfiguration, IRuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; -import { EntityType } from '@shared/models/entity-type.models'; -import { Observable, of, Subscription } from 'rxjs'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; +import { + IRuleNodeConfigurationComponent, + RuleNodeConfiguration, + RuleNodeDefinition +} from '@shared/models/rule-node.models'; +import { Subscription } from 'rxjs'; import { RuleChainService } from '@core/http/rule-chain.service'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; -import { deepClone } from '@core/utils'; -import { EntityAlias } from '@shared/models/alias.models'; -import { TruncatePipe } from '@shared/pipe/truncate.pipe'; -import { MatChipList, MatAutocomplete, MatChipInputEvent, MatAutocompleteSelectedEvent } from '@angular/material'; import { TranslateService } from '@ngx-translate/core'; -import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; -import { catchError, map, mergeMap, share } from 'rxjs/operators'; -import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component'; -import { SharedModule } from '@shared/shared.module'; -import { WidgetComponentsModule } from '@home/components/widget/widget-components.module'; -import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; -import { ViewContainerRef } from '@angular/core'; import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component'; @Component({ diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index a4ef9a7d9d..cd9950af6b 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -341,9 +341,9 @@ export class RuleChainPageComponent extends PageComponent this.nextNodeID = 1; this.nextConnectorID = 1; - this.selectedObjects.length = 0; - this.ruleChainModel.nodes.length = 0; - this.ruleChainModel.edges.length = 0; + this.selectedObjects = []; + this.ruleChainModel.nodes = []; + this.ruleChainModel.edges = []; this.inputConnectorId = this.nextConnectorID++; this.ruleChainModel.nodes.push( @@ -535,7 +535,7 @@ export class RuleChainPageComponent extends PageComponent this.editingRuleNodeLink = null; this.isEditingRuleNode = true; this.editingRuleNodeIndex = this.ruleChainModel.nodes.indexOf(node); - this.editingRuleNode = deepClone(node); + this.editingRuleNode = deepClone(node, ['component']); setTimeout(() => { this.ruleNodeComponent.ruleNodeFormGroup.markAsPristine(); }, 0); @@ -576,7 +576,7 @@ export class RuleChainPageComponent extends PageComponent onRevertRuleNodeEdit() { this.ruleNodeComponent.ruleNodeFormGroup.markAsPristine(); const node = this.ruleChainModel.nodes[this.editingRuleNodeIndex]; - this.editingRuleNode = deepClone(node); + this.editingRuleNode = deepClone(node, ['component']); } onRevertRuleNodeLinkEdit() { @@ -593,7 +593,7 @@ export class RuleChainPageComponent extends PageComponent delete this.editingRuleNode.error; } this.ruleChainModel.nodes[this.editingRuleNodeIndex] = this.editingRuleNode; - this.editingRuleNode = deepClone(this.editingRuleNode); + this.editingRuleNode = deepClone(this.editingRuleNode, ['component']); this.onModelChanged(); this.updateRuleNodesHighlight(); } diff --git a/ui-ngx/src/app/shared/components/file-input.component.html b/ui-ngx/src/app/shared/components/file-input.component.html index 0446834e92..32c4bd1de2 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.html +++ b/ui-ngx/src/app/shared/components/file-input.component.html @@ -33,13 +33,14 @@
- - + +
-
import.no-file
+ +
{{ noFileText }}
{{ fileName }}
diff --git a/ui-ngx/src/app/shared/components/file-input.component.ts b/ui-ngx/src/app/shared/components/file-input.component.ts index c7275d6e46..82db88f12f 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.ts +++ b/ui-ngx/src/app/shared/components/file-input.component.ts @@ -14,7 +14,17 @@ /// limitations under the License. /// -import { AfterViewInit, Component, forwardRef, Input, OnDestroy, ViewChild } from '@angular/core'; +import { + AfterViewInit, + Component, + EventEmitter, + forwardRef, + Input, + OnChanges, + OnDestroy, + Output, SimpleChanges, + ViewChild +} from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -22,6 +32,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Subscription } from 'rxjs'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { FlowDirective } from '@flowjs/ngx-flow'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'tb-file-input', @@ -35,7 +46,7 @@ import { FlowDirective } from '@flowjs/ngx-flow'; } ] }) -export class FileInputComponent extends PageComponent implements AfterViewInit, OnDestroy, ControlValueAccessor { +export class FileInputComponent extends PageComponent implements AfterViewInit, OnDestroy, ControlValueAccessor, OnChanges { @Input() label: string; @@ -43,6 +54,12 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, @Input() accept = '*/*'; + @Input() + noFileText = 'import.no-file'; + + @Input() + inputId = 'select'; + @Input() allowedExtensions: string; @@ -64,9 +81,27 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, } } + private requiredAsErrorValue: boolean; + get requiredAsError(): boolean { + return this.requiredAsErrorValue; + } + @Input() + set requiredAsError(value: boolean) { + const newVal = coerceBooleanProperty(value); + if (this.requiredAsErrorValue !== newVal) { + this.requiredAsErrorValue = newVal; + } + } + @Input() disabled: boolean; + @Input() + existingFileName: string; + + @Output() + fileNameChanged = new EventEmitter(); + fileName: string; fileContent: any; @@ -77,7 +112,8 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, private propagateChange = null; - constructor(protected store: Store) { + constructor(protected store: Store, + public translate: TranslateService) { super(store); } @@ -135,11 +171,23 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, } writeValue(value: any): void { - this.fileName = null; + this.fileName = this.existingFileName || null; + } + + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (change.currentValue !== change.previousValue) { + if (propName === 'existingFileName') { + this.fileName = this.existingFileName || null; + } + } + } } private updateModel() { this.propagateChange(this.fileContent); + this.fileNameChanged.emit(this.fileName); } clearFile() { diff --git a/ui-ngx/src/app/shared/models/rule-node.models.ts b/ui-ngx/src/app/shared/models/rule-node.models.ts index b611eca66c..dd68fea554 100644 --- a/ui-ngx/src/app/shared/models/rule-node.models.ts +++ b/ui-ngx/src/app/shared/models/rule-node.models.ts @@ -24,10 +24,11 @@ import { ComponentDescriptor, ComponentType } from '@shared/models/component-des import { EntityType, EntityTypeResource } from '@shared/models/entity-type.models'; import { Observable } from 'rxjs'; import { PageComponent } from '@shared/components/page.component'; -import { ComponentFactory, EventEmitter, Inject, OnDestroy, OnInit } from '@angular/core'; +import { AfterViewInit, ComponentFactory, EventEmitter, Inject, OnDestroy, OnInit } from '@angular/core'; import { RafService } from '@core/services/raf.service'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; +import { AbstractControl, FormGroup } from '@angular/forms'; export enum MsgDataType { JSON = 'JSON', @@ -83,10 +84,28 @@ export interface IRuleNodeConfigurationComponent { } export abstract class RuleNodeConfigurationComponent extends PageComponent implements - IRuleNodeConfigurationComponent, OnInit { + IRuleNodeConfigurationComponent, OnInit, AfterViewInit { ruleNodeId: string; - configuration: RuleNodeConfiguration; + + configurationValue: RuleNodeConfiguration; + + private configurationSet = false; + + set configuration(value: RuleNodeConfiguration) { + this.configurationValue = value; + if (!this.configurationSet) { + this.configurationSet = true; + this.setupConfiguration(value); + } else { + this.updateConfiguration(value); + } + } + + get configuration(): RuleNodeConfiguration { + return this.configurationValue; + } + configurationChangedEmiter = new EventEmitter(); configurationChanged = this.configurationChangedEmiter.asObservable(); @@ -94,21 +113,77 @@ export abstract class RuleNodeConfigurationComponent extends PageComponent imple super(store); } - ngOnInit() { - this.onConfigurationSet(this.configuration); + ngOnInit() {} + + ngAfterViewInit(): void { + setTimeout(() => { + if (!this.validateConfig()) { + this.configurationChangedEmiter.emit(null); + } + }, 0); } validate() { this.onValidate(); } - protected abstract onConfigurationSet(configuration: RuleNodeConfiguration); + protected setupConfiguration(configuration: RuleNodeConfiguration) { + this.onConfigurationSet(this.prepareInputConfig(configuration)); + this.updateValidators(false); + for (const trigger of this.validatorTriggers()) { + const path = trigger.split('.'); + let control: AbstractControl = this.configForm(); + for (const part of path) { + control = control.get(part); + } + control.valueChanges.subscribe(() => { + this.updateValidators(true); + }); + } + this.configForm().valueChanges.subscribe((updated: RuleNodeConfiguration) => { + this.onConfigurationChanged(updated); + }); + } - protected notifyConfigurationUpdated(configuration: RuleNodeConfiguration) { - this.configurationChangedEmiter.emit(configuration); + protected updateConfiguration(configuration: RuleNodeConfiguration) { + this.configForm().reset(this.prepareInputConfig(configuration), {emitEvent: false}); + this.updateValidators(false); + } + + protected updateValidators(emitEvent: boolean) { + } + + protected validatorTriggers(): string[] { + return []; + } + + protected onConfigurationChanged(updated: RuleNodeConfiguration) { + this.configurationValue = updated; + if (this.validateConfig()) { + this.configurationChangedEmiter.emit(this.prepareOutputConfig(updated)); + } else { + this.configurationChangedEmiter.emit(null); + } + } + + protected prepareInputConfig(configuration: RuleNodeConfiguration): RuleNodeConfiguration { + return configuration; + } + + protected prepareOutputConfig(configuration: RuleNodeConfiguration): RuleNodeConfiguration { + return configuration; + } + + protected validateConfig(): boolean { + return this.configForm().valid; } protected onValidate() {} + + protected abstract configForm(): FormGroup; + + protected abstract onConfigurationSet(configuration: RuleNodeConfiguration); + }