UI: RuleChain improvements.

This commit is contained in:
Igor Kulikov 2020-01-16 12:50:40 +02:00
parent f345092b2e
commit 833de64653
6 changed files with 161 additions and 49 deletions

View File

@ -356,7 +356,7 @@ function utf8ToBytes(input: string, units?: number): number[] {
return bytes; return bytes;
} }
export function deepClone<T>(target: T): T { export function deepClone<T>(target: T, ignoreFields?: string[]): T {
if (target === null) { if (target === null) {
return target; return target;
} }
@ -371,7 +371,9 @@ export function deepClone<T>(target: T): T {
if (typeof target === 'object' && target !== {}) { if (typeof target === 'object' && target !== {}) {
const cp = { ...(target as { [key: string]: any }) } as { [key: string]: any }; const cp = { ...(target as { [key: string]: any }) } as { [key: string]: any };
Object.keys(cp).forEach(k => { Object.keys(cp).forEach(k => {
cp[k] = deepClone<any>(cp[k]); if (!ignoreFields || ignoreFields.indexOf(k) === -1) {
cp[k] = deepClone<any>(cp[k]);
}
}); });
return cp as T; return cp as T;
} }

View File

@ -16,39 +16,25 @@
import { import {
AfterViewInit, AfterViewInit,
Component, ElementRef, Component,
EventEmitter, forwardRef, ComponentRef,
forwardRef,
Input, Input,
OnChanges, OnDestroy,
OnInit, OnInit,
Output,
SimpleChanges,
ViewChild, ViewChild,
Compiler, ViewContainerRef
Injector, ComponentRef, OnDestroy
} from '@angular/core'; } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Store } from '@ngrx/store'; import {
import { AppState } from '@core/core.state'; IRuleNodeConfigurationComponent,
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, NgForm, Validators } from '@angular/forms'; RuleNodeConfiguration,
import { FcRuleNode, FcRuleEdge } from './rulechain-page.models'; RuleNodeDefinition
import { RuleNodeType, LinkLabel, RuleNodeDefinition, RuleNodeConfiguration, IRuleNodeConfigurationComponent } from '@shared/models/rule-node.models'; } from '@shared/models/rule-node.models';
import { EntityType } from '@shared/models/entity-type.models'; import { Subscription } from 'rxjs';
import { Observable, of, Subscription } from 'rxjs';
import { RuleChainService } from '@core/http/rule-chain.service'; import { RuleChainService } from '@core/http/rule-chain.service';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; 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 { 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'; import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component';
@Component({ @Component({

View File

@ -341,9 +341,9 @@ export class RuleChainPageComponent extends PageComponent
this.nextNodeID = 1; this.nextNodeID = 1;
this.nextConnectorID = 1; this.nextConnectorID = 1;
this.selectedObjects.length = 0; this.selectedObjects = [];
this.ruleChainModel.nodes.length = 0; this.ruleChainModel.nodes = [];
this.ruleChainModel.edges.length = 0; this.ruleChainModel.edges = [];
this.inputConnectorId = this.nextConnectorID++; this.inputConnectorId = this.nextConnectorID++;
this.ruleChainModel.nodes.push( this.ruleChainModel.nodes.push(
@ -535,7 +535,7 @@ export class RuleChainPageComponent extends PageComponent
this.editingRuleNodeLink = null; this.editingRuleNodeLink = null;
this.isEditingRuleNode = true; this.isEditingRuleNode = true;
this.editingRuleNodeIndex = this.ruleChainModel.nodes.indexOf(node); this.editingRuleNodeIndex = this.ruleChainModel.nodes.indexOf(node);
this.editingRuleNode = deepClone(node); this.editingRuleNode = deepClone(node, ['component']);
setTimeout(() => { setTimeout(() => {
this.ruleNodeComponent.ruleNodeFormGroup.markAsPristine(); this.ruleNodeComponent.ruleNodeFormGroup.markAsPristine();
}, 0); }, 0);
@ -576,7 +576,7 @@ export class RuleChainPageComponent extends PageComponent
onRevertRuleNodeEdit() { onRevertRuleNodeEdit() {
this.ruleNodeComponent.ruleNodeFormGroup.markAsPristine(); this.ruleNodeComponent.ruleNodeFormGroup.markAsPristine();
const node = this.ruleChainModel.nodes[this.editingRuleNodeIndex]; const node = this.ruleChainModel.nodes[this.editingRuleNodeIndex];
this.editingRuleNode = deepClone(node); this.editingRuleNode = deepClone(node, ['component']);
} }
onRevertRuleNodeLinkEdit() { onRevertRuleNodeLinkEdit() {
@ -593,7 +593,7 @@ export class RuleChainPageComponent extends PageComponent
delete this.editingRuleNode.error; delete this.editingRuleNode.error;
} }
this.ruleChainModel.nodes[this.editingRuleNodeIndex] = this.editingRuleNode; this.ruleChainModel.nodes[this.editingRuleNodeIndex] = this.editingRuleNode;
this.editingRuleNode = deepClone(this.editingRuleNode); this.editingRuleNode = deepClone(this.editingRuleNode, ['component']);
this.onModelChanged(); this.onModelChanged();
this.updateRuleNodesHighlight(); this.updateRuleNodesHighlight();
} }

View File

@ -33,13 +33,14 @@
<div class="drop-area tb-flow-drop" <div class="drop-area tb-flow-drop"
flowDrop flowDrop
[flow]="flow.flowJs"> [flow]="flow.flowJs">
<label for="select">{{ dropLabel }}</label> <label for="{{inputId}}">{{ dropLabel }}</label>
<input class="file-input" flowButton [flow]="flow.flowJs" [flowAttributes]="{accept: accept}" id="select"> <input class="file-input" flowButton [flow]="flow.flowJs" [flowAttributes]="{accept: accept}" id="{{inputId}}">
</div> </div>
</div> </div>
</ng-container> </ng-container>
</div> </div>
<div> <div>
<div *ngIf="!fileName" translate>import.no-file</div> <tb-error *ngIf="!fileName && required && requiredAsError" error="{{ noFileText | translate }}"></tb-error>
<div *ngIf="!fileName && !requiredAsError" translate>{{ noFileText }}</div>
<div *ngIf="fileName">{{ fileName }}</div> <div *ngIf="fileName">{{ fileName }}</div>
</div> </div>

View File

@ -14,7 +14,17 @@
/// limitations under the License. /// 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 { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -22,6 +32,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { FlowDirective } from '@flowjs/ngx-flow'; import { FlowDirective } from '@flowjs/ngx-flow';
import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'tb-file-input', 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() @Input()
label: string; label: string;
@ -43,6 +54,12 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
@Input() @Input()
accept = '*/*'; accept = '*/*';
@Input()
noFileText = 'import.no-file';
@Input()
inputId = 'select';
@Input() @Input()
allowedExtensions: string; 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() @Input()
disabled: boolean; disabled: boolean;
@Input()
existingFileName: string;
@Output()
fileNameChanged = new EventEmitter<string>();
fileName: string; fileName: string;
fileContent: any; fileContent: any;
@ -77,7 +112,8 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
private propagateChange = null; private propagateChange = null;
constructor(protected store: Store<AppState>) { constructor(protected store: Store<AppState>,
public translate: TranslateService) {
super(store); super(store);
} }
@ -135,11 +171,23 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
} }
writeValue(value: any): void { 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() { private updateModel() {
this.propagateChange(this.fileContent); this.propagateChange(this.fileContent);
this.fileNameChanged.emit(this.fileName);
} }
clearFile() { clearFile() {

View File

@ -24,10 +24,11 @@ import { ComponentDescriptor, ComponentType } from '@shared/models/component-des
import { EntityType, EntityTypeResource } from '@shared/models/entity-type.models'; import { EntityType, EntityTypeResource } from '@shared/models/entity-type.models';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { PageComponent } from '@shared/components/page.component'; 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 { RafService } from '@core/services/raf.service';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { AbstractControl, FormGroup } from '@angular/forms';
export enum MsgDataType { export enum MsgDataType {
JSON = 'JSON', JSON = 'JSON',
@ -83,10 +84,28 @@ export interface IRuleNodeConfigurationComponent {
} }
export abstract class RuleNodeConfigurationComponent extends PageComponent implements export abstract class RuleNodeConfigurationComponent extends PageComponent implements
IRuleNodeConfigurationComponent, OnInit { IRuleNodeConfigurationComponent, OnInit, AfterViewInit {
ruleNodeId: string; 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<RuleNodeConfiguration>(); configurationChangedEmiter = new EventEmitter<RuleNodeConfiguration>();
configurationChanged = this.configurationChangedEmiter.asObservable(); configurationChanged = this.configurationChangedEmiter.asObservable();
@ -94,21 +113,77 @@ export abstract class RuleNodeConfigurationComponent extends PageComponent imple
super(store); super(store);
} }
ngOnInit() { ngOnInit() {}
this.onConfigurationSet(this.configuration);
ngAfterViewInit(): void {
setTimeout(() => {
if (!this.validateConfig()) {
this.configurationChangedEmiter.emit(null);
}
}, 0);
} }
validate() { validate() {
this.onValidate(); 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) { protected updateConfiguration(configuration: RuleNodeConfiguration) {
this.configurationChangedEmiter.emit(configuration); 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 onValidate() {}
protected abstract configForm(): FormGroup;
protected abstract onConfigurationSet(configuration: RuleNodeConfiguration);
} }