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;
|
||||
}
|
||||
|
||||
export function deepClone<T>(target: T): T {
|
||||
export function deepClone<T>(target: T, ignoreFields?: string[]): T {
|
||||
if (target === null) {
|
||||
return target;
|
||||
}
|
||||
@ -371,7 +371,9 @@ export function deepClone<T>(target: T): T {
|
||||
if (typeof target === 'object' && target !== {}) {
|
||||
const cp = { ...(target as { [key: string]: any }) } as { [key: string]: any };
|
||||
Object.keys(cp).forEach(k => {
|
||||
if (!ignoreFields || ignoreFields.indexOf(k) === -1) {
|
||||
cp[k] = deepClone<any>(cp[k]);
|
||||
}
|
||||
});
|
||||
return cp as T;
|
||||
}
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -33,13 +33,14 @@
|
||||
<div class="drop-area tb-flow-drop"
|
||||
flowDrop
|
||||
[flow]="flow.flowJs">
|
||||
<label for="select">{{ dropLabel }}</label>
|
||||
<input class="file-input" flowButton [flow]="flow.flowJs" [flowAttributes]="{accept: accept}" id="select">
|
||||
<label for="{{inputId}}">{{ dropLabel }}</label>
|
||||
<input class="file-input" flowButton [flow]="flow.flowJs" [flowAttributes]="{accept: accept}" id="{{inputId}}">
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</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>
|
||||
|
||||
@ -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<string>();
|
||||
|
||||
fileName: string;
|
||||
fileContent: any;
|
||||
|
||||
@ -77,7 +112,8 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
|
||||
|
||||
private propagateChange = null;
|
||||
|
||||
constructor(protected store: Store<AppState>) {
|
||||
constructor(protected store: Store<AppState>,
|
||||
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() {
|
||||
|
||||
@ -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<RuleNodeConfiguration>();
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user