Commit 833de6465374f28dcadafcedd9d948e077dc01ee

Authored by Igor Kulikov
1 parent f345092b

UI: RuleChain improvements.

@@ -356,7 +356,7 @@ function utf8ToBytes(input: string, units?: number): number[] { @@ -356,7 +356,7 @@ function utf8ToBytes(input: string, units?: number): number[] {
356 return bytes; 356 return bytes;
357 } 357 }
358 358
359 -export function deepClone<T>(target: T): T { 359 +export function deepClone<T>(target: T, ignoreFields?: string[]): T {
360 if (target === null) { 360 if (target === null) {
361 return target; 361 return target;
362 } 362 }
@@ -371,7 +371,9 @@ export function deepClone<T>(target: T): T { @@ -371,7 +371,9 @@ export function deepClone<T>(target: T): T {
371 if (typeof target === 'object' && target !== {}) { 371 if (typeof target === 'object' && target !== {}) {
372 const cp = { ...(target as { [key: string]: any }) } as { [key: string]: any }; 372 const cp = { ...(target as { [key: string]: any }) } as { [key: string]: any };
373 Object.keys(cp).forEach(k => { 373 Object.keys(cp).forEach(k => {
374 - cp[k] = deepClone<any>(cp[k]); 374 + if (!ignoreFields || ignoreFields.indexOf(k) === -1) {
  375 + cp[k] = deepClone<any>(cp[k]);
  376 + }
375 }); 377 });
376 return cp as T; 378 return cp as T;
377 } 379 }
@@ -16,39 +16,25 @@ @@ -16,39 +16,25 @@
16 16
17 import { 17 import {
18 AfterViewInit, 18 AfterViewInit,
19 - Component, ElementRef,  
20 - EventEmitter, forwardRef, 19 + Component,
  20 + ComponentRef,
  21 + forwardRef,
21 Input, 22 Input,
22 - OnChanges, 23 + OnDestroy,
23 OnInit, 24 OnInit,
24 - Output,  
25 - SimpleChanges,  
26 ViewChild, 25 ViewChild,
27 - Compiler,  
28 - Injector, ComponentRef, OnDestroy 26 + ViewContainerRef
29 } from '@angular/core'; 27 } from '@angular/core';
30 -import { PageComponent } from '@shared/components/page.component';  
31 -import { Store } from '@ngrx/store';  
32 -import { AppState } from '@core/core.state';  
33 -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, NgForm, Validators } from '@angular/forms';  
34 -import { FcRuleNode, FcRuleEdge } from './rulechain-page.models';  
35 -import { RuleNodeType, LinkLabel, RuleNodeDefinition, RuleNodeConfiguration, IRuleNodeConfigurationComponent } from '@shared/models/rule-node.models';  
36 -import { EntityType } from '@shared/models/entity-type.models';  
37 -import { Observable, of, Subscription } from 'rxjs'; 28 +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
  29 +import {
  30 + IRuleNodeConfigurationComponent,
  31 + RuleNodeConfiguration,
  32 + RuleNodeDefinition
  33 +} from '@shared/models/rule-node.models';
  34 +import { Subscription } from 'rxjs';
38 import { RuleChainService } from '@core/http/rule-chain.service'; 35 import { RuleChainService } from '@core/http/rule-chain.service';
39 import { coerceBooleanProperty } from '@angular/cdk/coercion'; 36 import { coerceBooleanProperty } from '@angular/cdk/coercion';
40 -import { deepClone } from '@core/utils';  
41 -import { EntityAlias } from '@shared/models/alias.models';  
42 -import { TruncatePipe } from '@shared/pipe/truncate.pipe';  
43 -import { MatChipList, MatAutocomplete, MatChipInputEvent, MatAutocompleteSelectedEvent } from '@angular/material';  
44 import { TranslateService } from '@ngx-translate/core'; 37 import { TranslateService } from '@ngx-translate/core';
45 -import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';  
46 -import { catchError, map, mergeMap, share } from 'rxjs/operators';  
47 -import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component';  
48 -import { SharedModule } from '@shared/shared.module';  
49 -import { WidgetComponentsModule } from '@home/components/widget/widget-components.module';  
50 -import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service';  
51 -import { ViewContainerRef } from '@angular/core';  
52 import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component'; 38 import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component';
53 39
54 @Component({ 40 @Component({
@@ -341,9 +341,9 @@ export class RuleChainPageComponent extends PageComponent @@ -341,9 +341,9 @@ export class RuleChainPageComponent extends PageComponent
341 this.nextNodeID = 1; 341 this.nextNodeID = 1;
342 this.nextConnectorID = 1; 342 this.nextConnectorID = 1;
343 343
344 - this.selectedObjects.length = 0;  
345 - this.ruleChainModel.nodes.length = 0;  
346 - this.ruleChainModel.edges.length = 0; 344 + this.selectedObjects = [];
  345 + this.ruleChainModel.nodes = [];
  346 + this.ruleChainModel.edges = [];
347 347
348 this.inputConnectorId = this.nextConnectorID++; 348 this.inputConnectorId = this.nextConnectorID++;
349 this.ruleChainModel.nodes.push( 349 this.ruleChainModel.nodes.push(
@@ -535,7 +535,7 @@ export class RuleChainPageComponent extends PageComponent @@ -535,7 +535,7 @@ export class RuleChainPageComponent extends PageComponent
535 this.editingRuleNodeLink = null; 535 this.editingRuleNodeLink = null;
536 this.isEditingRuleNode = true; 536 this.isEditingRuleNode = true;
537 this.editingRuleNodeIndex = this.ruleChainModel.nodes.indexOf(node); 537 this.editingRuleNodeIndex = this.ruleChainModel.nodes.indexOf(node);
538 - this.editingRuleNode = deepClone(node); 538 + this.editingRuleNode = deepClone(node, ['component']);
539 setTimeout(() => { 539 setTimeout(() => {
540 this.ruleNodeComponent.ruleNodeFormGroup.markAsPristine(); 540 this.ruleNodeComponent.ruleNodeFormGroup.markAsPristine();
541 }, 0); 541 }, 0);
@@ -576,7 +576,7 @@ export class RuleChainPageComponent extends PageComponent @@ -576,7 +576,7 @@ export class RuleChainPageComponent extends PageComponent
576 onRevertRuleNodeEdit() { 576 onRevertRuleNodeEdit() {
577 this.ruleNodeComponent.ruleNodeFormGroup.markAsPristine(); 577 this.ruleNodeComponent.ruleNodeFormGroup.markAsPristine();
578 const node = this.ruleChainModel.nodes[this.editingRuleNodeIndex]; 578 const node = this.ruleChainModel.nodes[this.editingRuleNodeIndex];
579 - this.editingRuleNode = deepClone(node); 579 + this.editingRuleNode = deepClone(node, ['component']);
580 } 580 }
581 581
582 onRevertRuleNodeLinkEdit() { 582 onRevertRuleNodeLinkEdit() {
@@ -593,7 +593,7 @@ export class RuleChainPageComponent extends PageComponent @@ -593,7 +593,7 @@ export class RuleChainPageComponent extends PageComponent
593 delete this.editingRuleNode.error; 593 delete this.editingRuleNode.error;
594 } 594 }
595 this.ruleChainModel.nodes[this.editingRuleNodeIndex] = this.editingRuleNode; 595 this.ruleChainModel.nodes[this.editingRuleNodeIndex] = this.editingRuleNode;
596 - this.editingRuleNode = deepClone(this.editingRuleNode); 596 + this.editingRuleNode = deepClone(this.editingRuleNode, ['component']);
597 this.onModelChanged(); 597 this.onModelChanged();
598 this.updateRuleNodesHighlight(); 598 this.updateRuleNodesHighlight();
599 } 599 }
@@ -33,13 +33,14 @@ @@ -33,13 +33,14 @@
33 <div class="drop-area tb-flow-drop" 33 <div class="drop-area tb-flow-drop"
34 flowDrop 34 flowDrop
35 [flow]="flow.flowJs"> 35 [flow]="flow.flowJs">
36 - <label for="select">{{ dropLabel }}</label>  
37 - <input class="file-input" flowButton [flow]="flow.flowJs" [flowAttributes]="{accept: accept}" id="select"> 36 + <label for="{{inputId}}">{{ dropLabel }}</label>
  37 + <input class="file-input" flowButton [flow]="flow.flowJs" [flowAttributes]="{accept: accept}" id="{{inputId}}">
38 </div> 38 </div>
39 </div> 39 </div>
40 </ng-container> 40 </ng-container>
41 </div> 41 </div>
42 <div> 42 <div>
43 - <div *ngIf="!fileName" translate>import.no-file</div> 43 + <tb-error *ngIf="!fileName && required && requiredAsError" error="{{ noFileText | translate }}"></tb-error>
  44 + <div *ngIf="!fileName && !requiredAsError" translate>{{ noFileText }}</div>
44 <div *ngIf="fileName">{{ fileName }}</div> 45 <div *ngIf="fileName">{{ fileName }}</div>
45 </div> 46 </div>
@@ -14,7 +14,17 @@ @@ -14,7 +14,17 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { AfterViewInit, Component, forwardRef, Input, OnDestroy, ViewChild } from '@angular/core'; 17 +import {
  18 + AfterViewInit,
  19 + Component,
  20 + EventEmitter,
  21 + forwardRef,
  22 + Input,
  23 + OnChanges,
  24 + OnDestroy,
  25 + Output, SimpleChanges,
  26 + ViewChild
  27 +} from '@angular/core';
18 import { PageComponent } from '@shared/components/page.component'; 28 import { PageComponent } from '@shared/components/page.component';
19 import { Store } from '@ngrx/store'; 29 import { Store } from '@ngrx/store';
20 import { AppState } from '@core/core.state'; 30 import { AppState } from '@core/core.state';
@@ -22,6 +32,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @@ -22,6 +32,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
22 import { Subscription } from 'rxjs'; 32 import { Subscription } from 'rxjs';
23 import { coerceBooleanProperty } from '@angular/cdk/coercion'; 33 import { coerceBooleanProperty } from '@angular/cdk/coercion';
24 import { FlowDirective } from '@flowjs/ngx-flow'; 34 import { FlowDirective } from '@flowjs/ngx-flow';
  35 +import { TranslateService } from '@ngx-translate/core';
25 36
26 @Component({ 37 @Component({
27 selector: 'tb-file-input', 38 selector: 'tb-file-input',
@@ -35,7 +46,7 @@ import { FlowDirective } from '@flowjs/ngx-flow'; @@ -35,7 +46,7 @@ import { FlowDirective } from '@flowjs/ngx-flow';
35 } 46 }
36 ] 47 ]
37 }) 48 })
38 -export class FileInputComponent extends PageComponent implements AfterViewInit, OnDestroy, ControlValueAccessor { 49 +export class FileInputComponent extends PageComponent implements AfterViewInit, OnDestroy, ControlValueAccessor, OnChanges {
39 50
40 @Input() 51 @Input()
41 label: string; 52 label: string;
@@ -44,6 +55,12 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, @@ -44,6 +55,12 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
44 accept = '*/*'; 55 accept = '*/*';
45 56
46 @Input() 57 @Input()
  58 + noFileText = 'import.no-file';
  59 +
  60 + @Input()
  61 + inputId = 'select';
  62 +
  63 + @Input()
47 allowedExtensions: string; 64 allowedExtensions: string;
48 65
49 @Input() 66 @Input()
@@ -64,9 +81,27 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, @@ -64,9 +81,27 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
64 } 81 }
65 } 82 }
66 83
  84 + private requiredAsErrorValue: boolean;
  85 + get requiredAsError(): boolean {
  86 + return this.requiredAsErrorValue;
  87 + }
  88 + @Input()
  89 + set requiredAsError(value: boolean) {
  90 + const newVal = coerceBooleanProperty(value);
  91 + if (this.requiredAsErrorValue !== newVal) {
  92 + this.requiredAsErrorValue = newVal;
  93 + }
  94 + }
  95 +
67 @Input() 96 @Input()
68 disabled: boolean; 97 disabled: boolean;
69 98
  99 + @Input()
  100 + existingFileName: string;
  101 +
  102 + @Output()
  103 + fileNameChanged = new EventEmitter<string>();
  104 +
70 fileName: string; 105 fileName: string;
71 fileContent: any; 106 fileContent: any;
72 107
@@ -77,7 +112,8 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, @@ -77,7 +112,8 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
77 112
78 private propagateChange = null; 113 private propagateChange = null;
79 114
80 - constructor(protected store: Store<AppState>) { 115 + constructor(protected store: Store<AppState>,
  116 + public translate: TranslateService) {
81 super(store); 117 super(store);
82 } 118 }
83 119
@@ -135,11 +171,23 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, @@ -135,11 +171,23 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
135 } 171 }
136 172
137 writeValue(value: any): void { 173 writeValue(value: any): void {
138 - this.fileName = null; 174 + this.fileName = this.existingFileName || null;
  175 + }
  176 +
  177 + ngOnChanges(changes: SimpleChanges): void {
  178 + for (const propName of Object.keys(changes)) {
  179 + const change = changes[propName];
  180 + if (change.currentValue !== change.previousValue) {
  181 + if (propName === 'existingFileName') {
  182 + this.fileName = this.existingFileName || null;
  183 + }
  184 + }
  185 + }
139 } 186 }
140 187
141 private updateModel() { 188 private updateModel() {
142 this.propagateChange(this.fileContent); 189 this.propagateChange(this.fileContent);
  190 + this.fileNameChanged.emit(this.fileName);
143 } 191 }
144 192
145 clearFile() { 193 clearFile() {
@@ -24,10 +24,11 @@ import { ComponentDescriptor, ComponentType } from '@shared/models/component-des @@ -24,10 +24,11 @@ import { ComponentDescriptor, ComponentType } from '@shared/models/component-des
24 import { EntityType, EntityTypeResource } from '@shared/models/entity-type.models'; 24 import { EntityType, EntityTypeResource } from '@shared/models/entity-type.models';
25 import { Observable } from 'rxjs'; 25 import { Observable } from 'rxjs';
26 import { PageComponent } from '@shared/components/page.component'; 26 import { PageComponent } from '@shared/components/page.component';
27 -import { ComponentFactory, EventEmitter, Inject, OnDestroy, OnInit } from '@angular/core'; 27 +import { AfterViewInit, ComponentFactory, EventEmitter, Inject, OnDestroy, OnInit } from '@angular/core';
28 import { RafService } from '@core/services/raf.service'; 28 import { RafService } from '@core/services/raf.service';
29 import { Store } from '@ngrx/store'; 29 import { Store } from '@ngrx/store';
30 import { AppState } from '@core/core.state'; 30 import { AppState } from '@core/core.state';
  31 +import { AbstractControl, FormGroup } from '@angular/forms';
31 32
32 export enum MsgDataType { 33 export enum MsgDataType {
33 JSON = 'JSON', 34 JSON = 'JSON',
@@ -83,10 +84,28 @@ export interface IRuleNodeConfigurationComponent { @@ -83,10 +84,28 @@ export interface IRuleNodeConfigurationComponent {
83 } 84 }
84 85
85 export abstract class RuleNodeConfigurationComponent extends PageComponent implements 86 export abstract class RuleNodeConfigurationComponent extends PageComponent implements
86 - IRuleNodeConfigurationComponent, OnInit { 87 + IRuleNodeConfigurationComponent, OnInit, AfterViewInit {
87 88
88 ruleNodeId: string; 89 ruleNodeId: string;
89 - configuration: RuleNodeConfiguration; 90 +
  91 + configurationValue: RuleNodeConfiguration;
  92 +
  93 + private configurationSet = false;
  94 +
  95 + set configuration(value: RuleNodeConfiguration) {
  96 + this.configurationValue = value;
  97 + if (!this.configurationSet) {
  98 + this.configurationSet = true;
  99 + this.setupConfiguration(value);
  100 + } else {
  101 + this.updateConfiguration(value);
  102 + }
  103 + }
  104 +
  105 + get configuration(): RuleNodeConfiguration {
  106 + return this.configurationValue;
  107 + }
  108 +
90 configurationChangedEmiter = new EventEmitter<RuleNodeConfiguration>(); 109 configurationChangedEmiter = new EventEmitter<RuleNodeConfiguration>();
91 configurationChanged = this.configurationChangedEmiter.asObservable(); 110 configurationChanged = this.configurationChangedEmiter.asObservable();
92 111
@@ -94,21 +113,77 @@ export abstract class RuleNodeConfigurationComponent extends PageComponent imple @@ -94,21 +113,77 @@ export abstract class RuleNodeConfigurationComponent extends PageComponent imple
94 super(store); 113 super(store);
95 } 114 }
96 115
97 - ngOnInit() {  
98 - this.onConfigurationSet(this.configuration); 116 + ngOnInit() {}
  117 +
  118 + ngAfterViewInit(): void {
  119 + setTimeout(() => {
  120 + if (!this.validateConfig()) {
  121 + this.configurationChangedEmiter.emit(null);
  122 + }
  123 + }, 0);
99 } 124 }
100 125
101 validate() { 126 validate() {
102 this.onValidate(); 127 this.onValidate();
103 } 128 }
104 129
105 - protected abstract onConfigurationSet(configuration: RuleNodeConfiguration); 130 + protected setupConfiguration(configuration: RuleNodeConfiguration) {
  131 + this.onConfigurationSet(this.prepareInputConfig(configuration));
  132 + this.updateValidators(false);
  133 + for (const trigger of this.validatorTriggers()) {
  134 + const path = trigger.split('.');
  135 + let control: AbstractControl = this.configForm();
  136 + for (const part of path) {
  137 + control = control.get(part);
  138 + }
  139 + control.valueChanges.subscribe(() => {
  140 + this.updateValidators(true);
  141 + });
  142 + }
  143 + this.configForm().valueChanges.subscribe((updated: RuleNodeConfiguration) => {
  144 + this.onConfigurationChanged(updated);
  145 + });
  146 + }
  147 +
  148 + protected updateConfiguration(configuration: RuleNodeConfiguration) {
  149 + this.configForm().reset(this.prepareInputConfig(configuration), {emitEvent: false});
  150 + this.updateValidators(false);
  151 + }
  152 +
  153 + protected updateValidators(emitEvent: boolean) {
  154 + }
  155 +
  156 + protected validatorTriggers(): string[] {
  157 + return [];
  158 + }
  159 +
  160 + protected onConfigurationChanged(updated: RuleNodeConfiguration) {
  161 + this.configurationValue = updated;
  162 + if (this.validateConfig()) {
  163 + this.configurationChangedEmiter.emit(this.prepareOutputConfig(updated));
  164 + } else {
  165 + this.configurationChangedEmiter.emit(null);
  166 + }
  167 + }
106 168
107 - protected notifyConfigurationUpdated(configuration: RuleNodeConfiguration) {  
108 - this.configurationChangedEmiter.emit(configuration); 169 + protected prepareInputConfig(configuration: RuleNodeConfiguration): RuleNodeConfiguration {
  170 + return configuration;
  171 + }
  172 +
  173 + protected prepareOutputConfig(configuration: RuleNodeConfiguration): RuleNodeConfiguration {
  174 + return configuration;
  175 + }
  176 +
  177 + protected validateConfig(): boolean {
  178 + return this.configForm().valid;
109 } 179 }
110 180
111 protected onValidate() {} 181 protected onValidate() {}
  182 +
  183 + protected abstract configForm(): FormGroup;
  184 +
  185 + protected abstract onConfigurationSet(configuration: RuleNodeConfiguration);
  186 +
112 } 187 }
113 188
114 189