UI: RuleChain improvements.
This commit is contained in:
parent
f345092b2e
commit
833de64653
@ -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 => {
|
||||||
|
if (!ignoreFields || ignoreFields.indexOf(k) === -1) {
|
||||||
cp[k] = deepClone<any>(cp[k]);
|
cp[k] = deepClone<any>(cp[k]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return cp as T;
|
return cp as T;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user