Showing
6 changed files
with
161 additions
and
49 deletions
... | ... | @@ -356,7 +356,7 @@ function utf8ToBytes(input: string, units?: number): number[] { |
356 | 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 | 360 | if (target === null) { |
361 | 361 | return target; |
362 | 362 | } |
... | ... | @@ -371,7 +371,9 @@ export function deepClone<T>(target: T): T { |
371 | 371 | if (typeof target === 'object' && target !== {}) { |
372 | 372 | const cp = { ...(target as { [key: string]: any }) } as { [key: string]: any }; |
373 | 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 | 378 | return cp as T; |
377 | 379 | } | ... | ... |
... | ... | @@ -16,39 +16,25 @@ |
16 | 16 | |
17 | 17 | import { |
18 | 18 | AfterViewInit, |
19 | - Component, ElementRef, | |
20 | - EventEmitter, forwardRef, | |
19 | + Component, | |
20 | + ComponentRef, | |
21 | + forwardRef, | |
21 | 22 | Input, |
22 | - OnChanges, | |
23 | + OnDestroy, | |
23 | 24 | OnInit, |
24 | - Output, | |
25 | - SimpleChanges, | |
26 | 25 | ViewChild, |
27 | - Compiler, | |
28 | - Injector, ComponentRef, OnDestroy | |
26 | + ViewContainerRef | |
29 | 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 | 35 | import { RuleChainService } from '@core/http/rule-chain.service'; |
39 | 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 | 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 | 38 | import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component'; |
53 | 39 | |
54 | 40 | @Component({ | ... | ... |
... | ... | @@ -341,9 +341,9 @@ export class RuleChainPageComponent extends PageComponent |
341 | 341 | this.nextNodeID = 1; |
342 | 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 | 348 | this.inputConnectorId = this.nextConnectorID++; |
349 | 349 | this.ruleChainModel.nodes.push( |
... | ... | @@ -535,7 +535,7 @@ export class RuleChainPageComponent extends PageComponent |
535 | 535 | this.editingRuleNodeLink = null; |
536 | 536 | this.isEditingRuleNode = true; |
537 | 537 | this.editingRuleNodeIndex = this.ruleChainModel.nodes.indexOf(node); |
538 | - this.editingRuleNode = deepClone(node); | |
538 | + this.editingRuleNode = deepClone(node, ['component']); | |
539 | 539 | setTimeout(() => { |
540 | 540 | this.ruleNodeComponent.ruleNodeFormGroup.markAsPristine(); |
541 | 541 | }, 0); |
... | ... | @@ -576,7 +576,7 @@ export class RuleChainPageComponent extends PageComponent |
576 | 576 | onRevertRuleNodeEdit() { |
577 | 577 | this.ruleNodeComponent.ruleNodeFormGroup.markAsPristine(); |
578 | 578 | const node = this.ruleChainModel.nodes[this.editingRuleNodeIndex]; |
579 | - this.editingRuleNode = deepClone(node); | |
579 | + this.editingRuleNode = deepClone(node, ['component']); | |
580 | 580 | } |
581 | 581 | |
582 | 582 | onRevertRuleNodeLinkEdit() { |
... | ... | @@ -593,7 +593,7 @@ export class RuleChainPageComponent extends PageComponent |
593 | 593 | delete this.editingRuleNode.error; |
594 | 594 | } |
595 | 595 | this.ruleChainModel.nodes[this.editingRuleNodeIndex] = this.editingRuleNode; |
596 | - this.editingRuleNode = deepClone(this.editingRuleNode); | |
596 | + this.editingRuleNode = deepClone(this.editingRuleNode, ['component']); | |
597 | 597 | this.onModelChanged(); |
598 | 598 | this.updateRuleNodesHighlight(); |
599 | 599 | } | ... | ... |
... | ... | @@ -33,13 +33,14 @@ |
33 | 33 | <div class="drop-area tb-flow-drop" |
34 | 34 | flowDrop |
35 | 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 | 38 | </div> |
39 | 39 | </div> |
40 | 40 | </ng-container> |
41 | 41 | </div> |
42 | 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 | 45 | <div *ngIf="fileName">{{ fileName }}</div> |
45 | 46 | </div> | ... | ... |
... | ... | @@ -14,7 +14,17 @@ |
14 | 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 | 28 | import { PageComponent } from '@shared/components/page.component'; |
19 | 29 | import { Store } from '@ngrx/store'; |
20 | 30 | import { AppState } from '@core/core.state'; |
... | ... | @@ -22,6 +32,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; |
22 | 32 | import { Subscription } from 'rxjs'; |
23 | 33 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
24 | 34 | import { FlowDirective } from '@flowjs/ngx-flow'; |
35 | +import { TranslateService } from '@ngx-translate/core'; | |
25 | 36 | |
26 | 37 | @Component({ |
27 | 38 | selector: 'tb-file-input', |
... | ... | @@ -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 | 51 | @Input() |
41 | 52 | label: string; |
... | ... | @@ -44,6 +55,12 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, |
44 | 55 | accept = '*/*'; |
45 | 56 | |
46 | 57 | @Input() |
58 | + noFileText = 'import.no-file'; | |
59 | + | |
60 | + @Input() | |
61 | + inputId = 'select'; | |
62 | + | |
63 | + @Input() | |
47 | 64 | allowedExtensions: string; |
48 | 65 | |
49 | 66 | @Input() |
... | ... | @@ -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 | 96 | @Input() |
68 | 97 | disabled: boolean; |
69 | 98 | |
99 | + @Input() | |
100 | + existingFileName: string; | |
101 | + | |
102 | + @Output() | |
103 | + fileNameChanged = new EventEmitter<string>(); | |
104 | + | |
70 | 105 | fileName: string; |
71 | 106 | fileContent: any; |
72 | 107 | |
... | ... | @@ -77,7 +112,8 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, |
77 | 112 | |
78 | 113 | private propagateChange = null; |
79 | 114 | |
80 | - constructor(protected store: Store<AppState>) { | |
115 | + constructor(protected store: Store<AppState>, | |
116 | + public translate: TranslateService) { | |
81 | 117 | super(store); |
82 | 118 | } |
83 | 119 | |
... | ... | @@ -135,11 +171,23 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, |
135 | 171 | } |
136 | 172 | |
137 | 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 | 188 | private updateModel() { |
142 | 189 | this.propagateChange(this.fileContent); |
190 | + this.fileNameChanged.emit(this.fileName); | |
143 | 191 | } |
144 | 192 | |
145 | 193 | clearFile() { | ... | ... |
... | ... | @@ -24,10 +24,11 @@ import { ComponentDescriptor, ComponentType } from '@shared/models/component-des |
24 | 24 | import { EntityType, EntityTypeResource } from '@shared/models/entity-type.models'; |
25 | 25 | import { Observable } from 'rxjs'; |
26 | 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 | 28 | import { RafService } from '@core/services/raf.service'; |
29 | 29 | import { Store } from '@ngrx/store'; |
30 | 30 | import { AppState } from '@core/core.state'; |
31 | +import { AbstractControl, FormGroup } from '@angular/forms'; | |
31 | 32 | |
32 | 33 | export enum MsgDataType { |
33 | 34 | JSON = 'JSON', |
... | ... | @@ -83,10 +84,28 @@ export interface IRuleNodeConfigurationComponent { |
83 | 84 | } |
84 | 85 | |
85 | 86 | export abstract class RuleNodeConfigurationComponent extends PageComponent implements |
86 | - IRuleNodeConfigurationComponent, OnInit { | |
87 | + IRuleNodeConfigurationComponent, OnInit, AfterViewInit { | |
87 | 88 | |
88 | 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 | 109 | configurationChangedEmiter = new EventEmitter<RuleNodeConfiguration>(); |
91 | 110 | configurationChanged = this.configurationChangedEmiter.asObservable(); |
92 | 111 | |
... | ... | @@ -94,21 +113,77 @@ export abstract class RuleNodeConfigurationComponent extends PageComponent imple |
94 | 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 | 126 | validate() { |
102 | 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 | 181 | protected onValidate() {} |
182 | + | |
183 | + protected abstract configForm(): FormGroup; | |
184 | + | |
185 | + protected abstract onConfigurationSet(configuration: RuleNodeConfiguration); | |
186 | + | |
112 | 187 | } |
113 | 188 | |
114 | 189 | ... | ... |