Commit 53b6aeb4fa1b9a976affa96d66563d7d6a939d12
1 parent
08e5a7e0
Rule chain page. Inprove hotkeys handling
Showing
22 changed files
with
1076 additions
and
186 deletions
... | ... | @@ -1407,14 +1407,12 @@ |
1407 | 1407 | "balanced-match": { |
1408 | 1408 | "version": "1.0.0", |
1409 | 1409 | "bundled": true, |
1410 | - "dev": true, | |
1411 | - "optional": true | |
1410 | + "dev": true | |
1412 | 1411 | }, |
1413 | 1412 | "brace-expansion": { |
1414 | 1413 | "version": "1.1.11", |
1415 | 1414 | "bundled": true, |
1416 | 1415 | "dev": true, |
1417 | - "optional": true, | |
1418 | 1416 | "requires": { |
1419 | 1417 | "balanced-match": "^1.0.0", |
1420 | 1418 | "concat-map": "0.0.1" |
... | ... | @@ -1429,20 +1427,17 @@ |
1429 | 1427 | "code-point-at": { |
1430 | 1428 | "version": "1.1.0", |
1431 | 1429 | "bundled": true, |
1432 | - "dev": true, | |
1433 | - "optional": true | |
1430 | + "dev": true | |
1434 | 1431 | }, |
1435 | 1432 | "concat-map": { |
1436 | 1433 | "version": "0.0.1", |
1437 | 1434 | "bundled": true, |
1438 | - "dev": true, | |
1439 | - "optional": true | |
1435 | + "dev": true | |
1440 | 1436 | }, |
1441 | 1437 | "console-control-strings": { |
1442 | 1438 | "version": "1.1.0", |
1443 | 1439 | "bundled": true, |
1444 | - "dev": true, | |
1445 | - "optional": true | |
1440 | + "dev": true | |
1446 | 1441 | }, |
1447 | 1442 | "core-util-is": { |
1448 | 1443 | "version": "1.0.2", |
... | ... | @@ -1559,8 +1554,7 @@ |
1559 | 1554 | "inherits": { |
1560 | 1555 | "version": "2.0.3", |
1561 | 1556 | "bundled": true, |
1562 | - "dev": true, | |
1563 | - "optional": true | |
1557 | + "dev": true | |
1564 | 1558 | }, |
1565 | 1559 | "ini": { |
1566 | 1560 | "version": "1.3.5", |
... | ... | @@ -1572,7 +1566,6 @@ |
1572 | 1566 | "version": "1.0.0", |
1573 | 1567 | "bundled": true, |
1574 | 1568 | "dev": true, |
1575 | - "optional": true, | |
1576 | 1569 | "requires": { |
1577 | 1570 | "number-is-nan": "^1.0.0" |
1578 | 1571 | } |
... | ... | @@ -1587,7 +1580,6 @@ |
1587 | 1580 | "version": "3.0.4", |
1588 | 1581 | "bundled": true, |
1589 | 1582 | "dev": true, |
1590 | - "optional": true, | |
1591 | 1583 | "requires": { |
1592 | 1584 | "brace-expansion": "^1.1.7" |
1593 | 1585 | } |
... | ... | @@ -1699,8 +1691,7 @@ |
1699 | 1691 | "number-is-nan": { |
1700 | 1692 | "version": "1.0.1", |
1701 | 1693 | "bundled": true, |
1702 | - "dev": true, | |
1703 | - "optional": true | |
1694 | + "dev": true | |
1704 | 1695 | }, |
1705 | 1696 | "object-assign": { |
1706 | 1697 | "version": "4.1.1", |
... | ... | @@ -1712,7 +1703,6 @@ |
1712 | 1703 | "version": "1.4.0", |
1713 | 1704 | "bundled": true, |
1714 | 1705 | "dev": true, |
1715 | - "optional": true, | |
1716 | 1706 | "requires": { |
1717 | 1707 | "wrappy": "1" |
1718 | 1708 | } |
... | ... | @@ -1834,7 +1824,6 @@ |
1834 | 1824 | "version": "1.0.2", |
1835 | 1825 | "bundled": true, |
1836 | 1826 | "dev": true, |
1837 | - "optional": true, | |
1838 | 1827 | "requires": { |
1839 | 1828 | "code-point-at": "^1.0.0", |
1840 | 1829 | "is-fullwidth-code-point": "^1.0.0", | ... | ... |
... | ... | @@ -186,7 +186,9 @@ export class AliasController implements IAliasController { |
186 | 186 | ); |
187 | 187 | } else { |
188 | 188 | resolvedAliasSubject.error(null); |
189 | + const res = this.resolvedAliasesObservable[aliasId]; | |
189 | 190 | delete this.resolvedAliasesObservable[aliasId]; |
191 | + return res; | |
190 | 192 | } |
191 | 193 | return this.resolvedAliasesObservable[aliasId]; |
192 | 194 | } | ... | ... |
... | ... | @@ -24,6 +24,8 @@ import * as equal from 'deep-equal'; |
24 | 24 | import { UtilsService } from '@core/services/utils.service'; |
25 | 25 | import { Observable, of, throwError } from 'rxjs'; |
26 | 26 | import { map } from 'rxjs/operators'; |
27 | +import { FcRuleEdge, FcRuleNode, ruleNodeTypeDescriptors } from '@shared/models/rule-node.models'; | |
28 | +import { RuleChainService } from '@core/http/rule-chain.service'; | |
27 | 29 | |
28 | 30 | const WIDGET_ITEM = 'widget_item'; |
29 | 31 | const WIDGET_REFERENCE = 'widget_reference'; |
... | ... | @@ -45,6 +47,21 @@ export interface WidgetReference { |
45 | 47 | originalColumns: number; |
46 | 48 | } |
47 | 49 | |
50 | +export interface RuleNodeConnection { | |
51 | + isInputSource: boolean; | |
52 | + fromIndex: number; | |
53 | + toIndex: number; | |
54 | + label: string; | |
55 | + labels: string[]; | |
56 | +} | |
57 | + | |
58 | +export interface RuleNodesReference { | |
59 | + nodes: FcRuleNode[]; | |
60 | + connections: RuleNodeConnection[]; | |
61 | + originX?: number; | |
62 | + originY?: number; | |
63 | +} | |
64 | + | |
48 | 65 | @Injectable({ |
49 | 66 | providedIn: 'root' |
50 | 67 | }) |
... | ... | @@ -54,6 +71,7 @@ export class ItemBufferService { |
54 | 71 | private delimiter = '.'; |
55 | 72 | |
56 | 73 | constructor(private dashboardUtils: DashboardUtilsService, |
74 | + private ruleChainService: RuleChainService, | |
57 | 75 | private utils: UtilsService) {} |
58 | 76 | |
59 | 77 | public prepareWidgetItem(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): WidgetItem { |
... | ... | @@ -99,12 +117,12 @@ export class ItemBufferService { |
99 | 117 | |
100 | 118 | public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void { |
101 | 119 | const widgetItem = this.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget); |
102 | - this.storeSet(WIDGET_ITEM, JSON.stringify(widgetItem)); | |
120 | + this.storeSet(WIDGET_ITEM, widgetItem); | |
103 | 121 | } |
104 | 122 | |
105 | 123 | public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void { |
106 | 124 | const widgetReference = this.prepareWidgetReference(dashboard, sourceState, sourceLayout, widget); |
107 | - this.storeSet(WIDGET_REFERENCE, JSON.stringify(widgetReference)); | |
125 | + this.storeSet(WIDGET_REFERENCE, widgetReference); | |
108 | 126 | } |
109 | 127 | |
110 | 128 | public hasWidget(): boolean { |
... | ... | @@ -112,9 +130,8 @@ export class ItemBufferService { |
112 | 130 | } |
113 | 131 | |
114 | 132 | public canPasteWidgetReference(dashboard: Dashboard, state: string, layout: DashboardLayoutId): boolean { |
115 | - const widgetReferenceJson = this.storeGet(WIDGET_REFERENCE); | |
116 | - if (widgetReferenceJson) { | |
117 | - const widgetReference: WidgetReference = JSON.parse(widgetReferenceJson); | |
133 | + const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE); | |
134 | + if (widgetReference) { | |
118 | 135 | if (widgetReference.dashboardId === dashboard.id.id) { |
119 | 136 | if ((widgetReference.sourceState !== state || widgetReference.sourceLayout !== layout) |
120 | 137 | && dashboard.configuration.widgets[widgetReference.widgetId]) { |
... | ... | @@ -128,9 +145,8 @@ export class ItemBufferService { |
128 | 145 | public pasteWidget(targetDashboard: Dashboard, targetState: string, |
129 | 146 | targetLayout: DashboardLayoutId, position: WidgetPosition, |
130 | 147 | onAliasesUpdateFunction: () => void): Observable<Widget> { |
131 | - const widgetItemJson = this.storeGet(WIDGET_ITEM); | |
132 | - if (widgetItemJson) { | |
133 | - const widgetItem: WidgetItem = JSON.parse(widgetItemJson); | |
148 | + const widgetItem: WidgetItem = this.storeGet(WIDGET_ITEM); | |
149 | + if (widgetItem) { | |
134 | 150 | const widget = widgetItem.widget; |
135 | 151 | const aliasesInfo = widgetItem.aliasesInfo; |
136 | 152 | const originalColumns = widgetItem.originalColumns; |
... | ... | @@ -155,9 +171,8 @@ export class ItemBufferService { |
155 | 171 | |
156 | 172 | public pasteWidgetReference(targetDashboard: Dashboard, targetState: string, |
157 | 173 | targetLayout: DashboardLayoutId, position: WidgetPosition): Observable<Widget> { |
158 | - const widgetReferenceJson = this.storeGet(WIDGET_REFERENCE); | |
159 | - if (widgetReferenceJson) { | |
160 | - const widgetReference: WidgetReference = JSON.parse(widgetReferenceJson); | |
174 | + const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE); | |
175 | + if (widgetReference) { | |
161 | 176 | const widget = targetDashboard.configuration.widgets[widgetReference.widgetId]; |
162 | 177 | if (widget) { |
163 | 178 | const originalColumns = widgetReference.originalColumns; |
... | ... | @@ -216,6 +231,89 @@ export class ItemBufferService { |
216 | 231 | return of(theDashboard); |
217 | 232 | } |
218 | 233 | |
234 | + public copyRuleNodes(nodes: FcRuleNode[], connections: RuleNodeConnection[]) { | |
235 | + const ruleNodes: RuleNodesReference = { | |
236 | + nodes: [], | |
237 | + connections: [] | |
238 | + }; | |
239 | + let top = -1, left = -1, bottom = -1, right = -1; | |
240 | + for (let i = 0; i < nodes.length; i++) { | |
241 | + const origNode = nodes[i]; | |
242 | + const node: FcRuleNode = { | |
243 | + id: '', | |
244 | + connectors: [], | |
245 | + additionalInfo: origNode.additionalInfo, | |
246 | + configuration: origNode.configuration, | |
247 | + debugMode: origNode.debugMode, | |
248 | + x: origNode.x, | |
249 | + y: origNode.y, | |
250 | + name: origNode.name, | |
251 | + componentClazz: origNode.component.clazz, | |
252 | + } | |
253 | + if (origNode.targetRuleChainId) { | |
254 | + node.targetRuleChainId = origNode.targetRuleChainId; | |
255 | + } | |
256 | + if (origNode.error) { | |
257 | + node.error = origNode.error; | |
258 | + } | |
259 | + ruleNodes.nodes.push(node); | |
260 | + if (i==0) { | |
261 | + top = node.y; | |
262 | + left = node.x; | |
263 | + bottom = node.y + 50; | |
264 | + right = node.x + 170; | |
265 | + } else { | |
266 | + top = Math.min(top, node.y); | |
267 | + left = Math.min(left, node.x); | |
268 | + bottom = Math.max(bottom, node.y + 50); | |
269 | + right = Math.max(right, node.x + 170); | |
270 | + } | |
271 | + } | |
272 | + ruleNodes.originX = left + (right-left)/2; | |
273 | + ruleNodes.originY = top + (bottom-top)/2; | |
274 | + connections.forEach(connection => { | |
275 | + ruleNodes.connections.push(connection); | |
276 | + }); | |
277 | + this.storeSet(RULE_NODES, ruleNodes); | |
278 | + } | |
279 | + | |
280 | + public hasRuleNodes(): boolean { | |
281 | + return this.storeHas(RULE_NODES); | |
282 | + } | |
283 | + | |
284 | + public pasteRuleNodes(x: number, y: number): RuleNodesReference { | |
285 | + const ruleNodes: RuleNodesReference = this.storeGet(RULE_NODES); | |
286 | + if (ruleNodes) { | |
287 | + const deltaX = x - ruleNodes.originX; | |
288 | + const deltaY = y - ruleNodes.originY; | |
289 | + for (const node of ruleNodes.nodes) { | |
290 | + const component = this.ruleChainService.getRuleNodeComponentByClazz(node.componentClazz); | |
291 | + if (component) { | |
292 | + let icon = ruleNodeTypeDescriptors.get(component.type).icon; | |
293 | + let iconUrl: string = null; | |
294 | + if (component.configurationDescriptor.nodeDefinition.icon) { | |
295 | + icon = component.configurationDescriptor.nodeDefinition.icon; | |
296 | + } | |
297 | + if (component.configurationDescriptor.nodeDefinition.iconUrl) { | |
298 | + iconUrl = component.configurationDescriptor.nodeDefinition.iconUrl; | |
299 | + } | |
300 | + delete node.componentClazz; | |
301 | + node.component = component; | |
302 | + node.nodeClass = ruleNodeTypeDescriptors.get(component.type).nodeClass; | |
303 | + node.icon = icon; | |
304 | + node.iconUrl = iconUrl; | |
305 | + node.connectors = []; | |
306 | + node.x = Math.round(node.x + deltaX); | |
307 | + node.y = Math.round(node.y + deltaY); | |
308 | + } else { | |
309 | + return null; | |
310 | + } | |
311 | + } | |
312 | + return ruleNodes; | |
313 | + } | |
314 | + return null; | |
315 | + } | |
316 | + | |
219 | 317 | private getOriginalColumns(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId): number { |
220 | 318 | let originalColumns = 24; |
221 | 319 | let gridSettings = null; | ... | ... |
... | ... | @@ -737,18 +737,22 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
737 | 737 | dataLoading: (subscription) => { |
738 | 738 | if (this.loadingData !== subscription.loadingData) { |
739 | 739 | this.loadingData = subscription.loadingData; |
740 | - this.cd.detectChanges(); | |
740 | + if (!this.destroyed) { | |
741 | + this.cd.detectChanges(); | |
742 | + } | |
741 | 743 | } |
742 | 744 | }, |
743 | 745 | legendDataUpdated: (subscription, detectChanges) => { |
744 | - if (detectChanges) { | |
746 | + if (detectChanges && !this.destroyed) { | |
745 | 747 | this.cd.detectChanges(); |
746 | 748 | } |
747 | 749 | }, |
748 | 750 | timeWindowUpdated: (subscription, timeWindowConfig) => { |
749 | 751 | this.ngZone.run(() => { |
750 | 752 | this.widget.config.timewindow = timeWindowConfig; |
751 | - this.cd.detectChanges(); | |
753 | + if (!this.destroyed) { | |
754 | + this.cd.detectChanges(); | |
755 | + } | |
752 | 756 | }); |
753 | 757 | } |
754 | 758 | }; | ... | ... |
... | ... | @@ -17,6 +17,7 @@ |
17 | 17 | --> |
18 | 18 | <div class="tb-dashboard-page mat-content" style="padding-top: 150px;" |
19 | 19 | fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen"> |
20 | + <tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet> | |
20 | 21 | <section class="tb-dashboard-toolbar" |
21 | 22 | [ngClass]="{ 'tb-dashboard-toolbar-opened': toolbarOpened, |
22 | 23 | 'tb-dashboard-toolbar-closed': !toolbarOpened }"> |
... | ... | @@ -146,6 +147,7 @@ |
146 | 147 | [mode]="isMobile ? 'over' : 'side'" |
147 | 148 | [(opened)]="rightLayoutOpened"> |
148 | 149 | <tb-dashboard-layout style="height: 100%;" |
150 | + [dashboardCheatSheet]="cheatSheetComponent" | |
149 | 151 | [layoutCtx]="layouts.right.layoutCtx" |
150 | 152 | [dashboardCtx]="dashboardCtx" |
151 | 153 | [isEdit]="isEdit" |
... | ... | @@ -159,6 +161,7 @@ |
159 | 161 | [ngStyle]="{width: mainLayoutWidth(), |
160 | 162 | height: mainLayoutHeight()}"> |
161 | 163 | <tb-dashboard-layout |
164 | + [dashboardCheatSheet]="cheatSheetComponent" | |
162 | 165 | [layoutCtx]="layouts.main.layoutCtx" |
163 | 166 | [dashboardCtx]="dashboardCtx" |
164 | 167 | [isEdit]="isEdit" | ... | ... |
... | ... | @@ -15,13 +15,14 @@ |
15 | 15 | limitations under the License. |
16 | 16 | |
17 | 17 | --> |
18 | -<hotkeys-cheatsheet></hotkeys-cheatsheet> | |
19 | 18 | <div fxLayout="column" class="tb-progress-cover" fxLayoutAlign="center center" |
20 | 19 | *ngIf="layoutCtx.widgets.isLoading()"> |
21 | 20 | <mat-spinner color="warn" mode="indeterminate" diameter="100"> |
22 | 21 | </mat-spinner> |
23 | 22 | </div> |
24 | -<div class="mat-content" style="position: relative; width: 100%; height: 100%;" | |
23 | +<div class="mat-content" | |
24 | + style="position: relative; width: 100%; height: 100%;" tb-hotkeys [hotkeys]="hotKeys" | |
25 | + [cheatSheet]="dashboardCheatSheet" | |
25 | 26 | [style.backgroundImage]="backgroundImage" |
26 | 27 | [ngStyle]="dashboardStyle"> |
27 | 28 | <section *ngIf="layoutCtx.widgets.isEmpty()" fxLayoutAlign="center center" | ... | ... |
... | ... | @@ -14,27 +14,25 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, OnDestroy, OnInit, Input, ChangeDetectorRef, ViewChild } from '@angular/core'; | |
18 | -import { StateControllerComponent } from '@home/pages/dashboard/states/state-controller.component'; | |
17 | +import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; | |
19 | 18 | import { ILayoutController } from '@home/pages/dashboard/layout/layout.models'; |
20 | 19 | import { DashboardContext, DashboardPageLayoutContext } from '@home/pages/dashboard/dashboard-page.models'; |
21 | 20 | import { PageComponent } from '@shared/components/page.component'; |
22 | 21 | import { Store } from '@ngrx/store'; |
23 | 22 | import { AppState } from '@core/core.state'; |
24 | 23 | import { Widget } from '@shared/models/widget.models'; |
25 | -import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models'; | |
26 | -import { GridsterComponent } from 'angular-gridster2'; | |
27 | 24 | import { |
28 | 25 | DashboardCallbacks, |
29 | 26 | DashboardContextMenuItem, |
30 | - IDashboardComponent, WidgetContextMenuItem | |
27 | + IDashboardComponent, | |
28 | + WidgetContextMenuItem | |
31 | 29 | } from '@home/models/dashboard-component.models'; |
32 | -import { Observable, of, Subscription } from 'rxjs'; | |
30 | +import { Subscription } from 'rxjs'; | |
33 | 31 | import { Hotkey, HotkeysService } from 'angular2-hotkeys'; |
34 | -import { getCurrentIsLoading } from '@core/interceptors/load.selectors'; | |
35 | 32 | import { TranslateService } from '@ngx-translate/core'; |
36 | 33 | import { ItemBufferService } from '@app/core/services/item-buffer.service'; |
37 | 34 | import { DomSanitizer, SafeStyle } from '@angular/platform-browser'; |
35 | +import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component'; | |
38 | 36 | |
39 | 37 | @Component({ |
40 | 38 | selector: 'tb-dashboard-layout', |
... | ... | @@ -47,6 +45,10 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo |
47 | 45 | dashboardStyle: {[klass: string]: any} = null; |
48 | 46 | backgroundImage: SafeStyle | string; |
49 | 47 | |
48 | + hotKeys: Hotkey[] = []; | |
49 | + | |
50 | + @Input() dashboardCheatSheet: TbCheatSheetComponent; | |
51 | + | |
50 | 52 | @Input() |
51 | 53 | set layoutCtx(val: DashboardPageLayoutContext) { |
52 | 54 | this.layoutCtxValue = val; |
... | ... | @@ -81,11 +83,11 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo |
81 | 83 | private rxSubscriptions = new Array<Subscription>(); |
82 | 84 | |
83 | 85 | constructor(protected store: Store<AppState>, |
84 | - private hotkeysService: HotkeysService, | |
85 | 86 | private translate: TranslateService, |
86 | 87 | private itembuffer: ItemBufferService, |
87 | 88 | private sanitizer: DomSanitizer) { |
88 | 89 | super(store); |
90 | + this.initHotKeys(); | |
89 | 91 | } |
90 | 92 | |
91 | 93 | ngOnInit(): void { |
... | ... | @@ -95,7 +97,6 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo |
95 | 97 | this.dashboardCtx.runChangeDetection(); |
96 | 98 | }) |
97 | 99 | ); |
98 | - this.initHotKeys(); | |
99 | 100 | } |
100 | 101 | |
101 | 102 | ngOnDestroy(): void { |
... | ... | @@ -106,7 +107,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo |
106 | 107 | } |
107 | 108 | |
108 | 109 | private initHotKeys(): void { |
109 | - this.hotkeysService.add( | |
110 | + this.hotKeys.push( | |
110 | 111 | new Hotkey('ctrl+c', (event: KeyboardEvent) => { |
111 | 112 | if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { |
112 | 113 | const widget = this.dashboard.getSelectedWidget(); |
... | ... | @@ -119,7 +120,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo |
119 | 120 | }, null, |
120 | 121 | this.translate.instant('action.copy')) |
121 | 122 | ); |
122 | - this.hotkeysService.add( | |
123 | + this.hotKeys.push( | |
123 | 124 | new Hotkey('ctrl+r', (event: KeyboardEvent) => { |
124 | 125 | if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { |
125 | 126 | const widget = this.dashboard.getSelectedWidget(); |
... | ... | @@ -132,7 +133,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo |
132 | 133 | }, null, |
133 | 134 | this.translate.instant('action.copy-reference')) |
134 | 135 | ); |
135 | - this.hotkeysService.add( | |
136 | + this.hotKeys.push( | |
136 | 137 | new Hotkey('ctrl+v', (event: KeyboardEvent) => { |
137 | 138 | if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { |
138 | 139 | if (this.itembuffer.hasWidget()) { |
... | ... | @@ -144,7 +145,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo |
144 | 145 | }, null, |
145 | 146 | this.translate.instant('action.paste')) |
146 | 147 | ); |
147 | - this.hotkeysService.add( | |
148 | + this.hotKeys.push( | |
148 | 149 | new Hotkey('ctrl+i', (event: KeyboardEvent) => { |
149 | 150 | if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { |
150 | 151 | if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.getDashboard(), |
... | ... | @@ -157,7 +158,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo |
157 | 158 | }, null, |
158 | 159 | this.translate.instant('action.paste-reference')) |
159 | 160 | ); |
160 | - this.hotkeysService.add( | |
161 | + this.hotKeys.push( | |
161 | 162 | new Hotkey('ctrl+x', (event: KeyboardEvent) => { |
162 | 163 | if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { |
163 | 164 | const widget = this.dashboard.getSelectedWidget(); | ... | ... |
... | ... | @@ -14,31 +14,14 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { | |
18 | - AfterViewInit, | |
19 | - Component, ElementRef, | |
20 | - EventEmitter, forwardRef, | |
21 | - Input, | |
22 | - OnChanges, | |
23 | - OnInit, | |
24 | - Output, | |
25 | - SimpleChanges, | |
26 | - ViewChild | |
27 | -} from '@angular/core'; | |
28 | -import { PageComponent } from '@shared/components/page.component'; | |
29 | -import { Store } from '@ngrx/store'; | |
30 | -import { AppState } from '@core/core.state'; | |
31 | -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, NgForm, Validators } from '@angular/forms'; | |
32 | -import { FcRuleNode, FcRuleEdge } from './rulechain-page.models'; | |
33 | -import { RuleNodeType, LinkLabel } from '@shared/models/rule-node.models'; | |
34 | -import { EntityType } from '@shared/models/entity-type.models'; | |
35 | -import { Observable, of, Subscription } from 'rxjs'; | |
36 | -import { RuleChainService } from '@core/http/rule-chain.service'; | |
17 | +import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; | |
18 | +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; | |
19 | +import { LinkLabel } from '@shared/models/rule-node.models'; | |
20 | +import { Observable, of } from 'rxjs'; | |
37 | 21 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
38 | 22 | import { deepClone } from '@core/utils'; |
39 | -import { EntityAlias } from '@shared/models/alias.models'; | |
40 | 23 | import { TruncatePipe } from '@shared/pipe/truncate.pipe'; |
41 | -import { MatChipList, MatAutocomplete, MatChipInputEvent, MatAutocompleteSelectedEvent } from '@angular/material'; | |
24 | +import { MatAutocomplete, MatAutocompleteSelectedEvent, MatChipInputEvent, MatChipList } from '@angular/material'; | |
42 | 25 | import { TranslateService } from '@ngx-translate/core'; |
43 | 26 | import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; |
44 | 27 | import { map, mergeMap, share, startWith } from 'rxjs/operators'; | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2019 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | + | |
17 | +@mixin rule-node-colors { | |
18 | + &.tb-filter-type { | |
19 | + background-color: #f1e861; | |
20 | + } | |
21 | + | |
22 | + &.tb-enrichment-type { | |
23 | + background-color: #cdf14e; | |
24 | + } | |
25 | + | |
26 | + &.tb-transformation-type { | |
27 | + background-color: #79cef1; | |
28 | + } | |
29 | + | |
30 | + &.tb-action-type { | |
31 | + background-color: #f1928f; | |
32 | + } | |
33 | + | |
34 | + &.tb-external-type { | |
35 | + background-color: #fbc766; | |
36 | + } | |
37 | + | |
38 | + &.tb-rule-chain-type { | |
39 | + background-color: #d6c4f1; | |
40 | + } | |
41 | + | |
42 | + &.tb-unknown-type { | |
43 | + background-color: #f16c29; | |
44 | + } | |
45 | + | |
46 | +} | ... | ... |
... | ... | @@ -19,12 +19,10 @@ import { PageComponent } from '@shared/components/page.component'; |
19 | 19 | import { Store } from '@ngrx/store'; |
20 | 20 | import { AppState } from '@core/core.state'; |
21 | 21 | import { FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms'; |
22 | -import { FcRuleNode } from './rulechain-page.models'; | |
23 | -import { RuleNodeType } from '@shared/models/rule-node.models'; | |
22 | +import { FcRuleNode, RuleNodeType } from '@shared/models/rule-node.models'; | |
24 | 23 | import { EntityType } from '@shared/models/entity-type.models'; |
25 | 24 | import { Subscription } from 'rxjs'; |
26 | 25 | import { RuleChainService } from '@core/http/rule-chain.service'; |
27 | -import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component'; | |
28 | 26 | import { RuleNodeConfigComponent } from './rule-node-config.component'; |
29 | 27 | |
30 | 28 | @Component({ | ... | ... |
... | ... | @@ -14,34 +14,12 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { | |
18 | - AfterViewInit, | |
19 | - Component, ElementRef, | |
20 | - EventEmitter, forwardRef, | |
21 | - Input, | |
22 | - OnChanges, | |
23 | - OnInit, | |
24 | - Output, | |
25 | - SimpleChanges, | |
26 | - ViewChild | |
27 | -} from '@angular/core'; | |
28 | -import { PageComponent } from '@shared/components/page.component'; | |
29 | -import { Store } from '@ngrx/store'; | |
30 | -import { AppState } from '@core/core.state'; | |
17 | +import { Component, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; | |
31 | 18 | import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, NgForm, Validators } from '@angular/forms'; |
32 | -import { FcRuleNode, FcRuleEdge } from './rulechain-page.models'; | |
33 | -import { RuleNodeType, LinkLabel } from '@shared/models/rule-node.models'; | |
34 | -import { EntityType } from '@shared/models/entity-type.models'; | |
35 | -import { Observable, of, Subscription } from 'rxjs'; | |
36 | -import { RuleChainService } from '@core/http/rule-chain.service'; | |
19 | +import { FcRuleEdge, LinkLabel } from '@shared/models/rule-node.models'; | |
37 | 20 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
38 | -import { deepClone } from '@core/utils'; | |
39 | -import { EntityAlias } from '@shared/models/alias.models'; | |
40 | 21 | import { TruncatePipe } from '@shared/pipe/truncate.pipe'; |
41 | -import { MatChipList, MatAutocomplete, MatChipInputEvent, MatAutocompleteSelectedEvent } from '@angular/material'; | |
42 | 22 | import { TranslateService } from '@ngx-translate/core'; |
43 | -import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; | |
44 | -import { map, mergeMap, share } from 'rxjs/operators'; | |
45 | 23 | |
46 | 24 | @Component({ |
47 | 25 | selector: 'tb-rule-node-link', | ... | ... |
... | ... | @@ -15,8 +15,10 @@ |
15 | 15 | limitations under the License. |
16 | 16 | |
17 | 17 | --> |
18 | -<div class="mat-content" fxFlex tb-fullscreen [fullscreen]="isFullscreen" | |
18 | +<div class="mat-content" fxFlex tb-fullscreen [fullscreen]="isFullscreen" tb-hotkeys [hotkeys]="hotKeys" | |
19 | + [cheatSheet]="cheatSheetComponent" | |
19 | 20 | fxLayout="column" class="tb-rulechain"> |
21 | + <tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet> | |
20 | 22 | <section class="tb-rulechain-container" fxFlex fxLayout="column"> |
21 | 23 | <div class="tb-rulechain-layout" fxFlex fxLayout="row"> |
22 | 24 | <section fxLayout="row" |
... | ... | @@ -80,6 +82,7 @@ |
80 | 82 | [model]="ruleNodeTypesModel[ruleNodeType].model" |
81 | 83 | [selectedObjects]="ruleNodeTypesModel[ruleNodeType].selectedObjects" |
82 | 84 | [automaticResize]="false" |
85 | + fitModelSizeByDefault | |
83 | 86 | [userCallbacks]="nodeLibCallbacks" |
84 | 87 | [nodeWidth]="170" |
85 | 88 | [nodeHeight]="50" |
... | ... | @@ -162,7 +165,38 @@ |
162 | 165 | matTooltipPosition="above"> |
163 | 166 | <mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> |
164 | 167 | </button> |
165 | - <div class="tb-absolute-fill tb-rulechain-graph"> | |
168 | + <div class="tb-absolute-fill tb-rulechain-graph" (contextmenu)="openRuleChainContextMenu($event)"> | |
169 | + <div #ruleChainMenuTrigger="matMenuTrigger" style="visibility: hidden; position: fixed" | |
170 | + [style.left]="ruleChainMenuPosition.x" | |
171 | + [style.top]="ruleChainMenuPosition.y" | |
172 | + [matMenuTriggerFor]="ruleChainMenu"> | |
173 | + </div> | |
174 | + <mat-menu #ruleChainMenu="matMenu" class="tb-rule-chain-context-menu" | |
175 | + [overlapTrigger]="true"> | |
176 | + <ng-template matMenuContent let-contextInfo="contextInfo"> | |
177 | + <div class="tb-rule-chain-context-menu-container" (mouseleave)="onRuleChainContextMenuMouseLeave()"> | |
178 | + <div class="tb-context-menu-header {{contextInfo.headerClass}}"> | |
179 | + <mat-icon *ngIf="!contextInfo.iconUrl">{{contextInfo.icon}}</mat-icon> | |
180 | + <img *ngIf="contextInfo.iconUrl" [src]="contextInfo.iconUrl"/> | |
181 | + <div fxFlex> | |
182 | + <div class="tb-context-menu-title">{{contextInfo.title}}</div> | |
183 | + <div class="tb-context-menu-subtitle">{{contextInfo.subtitle}}</div> | |
184 | + </div> | |
185 | + </div> | |
186 | + <div *ngFor="let menuItem of contextInfo.menuItems"> | |
187 | + <mat-divider *ngIf="menuItem.divider"></mat-divider> | |
188 | + <button *ngIf="!menuItem.divider" | |
189 | + mat-menu-item | |
190 | + [disabled]="!menuItem.enabled" | |
191 | + (click)="menuItem.action(contextMenuEvent)"> | |
192 | + <span *ngIf="menuItem.shortcut" class="tb-alt-text"> {{ menuItem.shortcut | keyboardShortcut }}</span> | |
193 | + <mat-icon *ngIf="menuItem.icon">{{menuItem.icon}}</mat-icon> | |
194 | + <span translate>{{menuItem.value}}</span> | |
195 | + </button> | |
196 | + </div> | |
197 | + </div> | |
198 | + </ng-template> | |
199 | + </mat-menu> | |
166 | 200 | <fc-canvas #ruleChainCanvas |
167 | 201 | id="tb-rulchain-canvas" |
168 | 202 | [model]="ruleChainModel" |
... | ... | @@ -170,6 +204,7 @@ |
170 | 204 | [selectedObjects]="selectedObjects" |
171 | 205 | [edgeStyle]="flowchartConstants.curvedStyle" |
172 | 206 | [automaticResize]="true" |
207 | + fitModelSizeByDefault="false" | |
173 | 208 | [nodeWidth]="170" |
174 | 209 | [nodeHeight]="50" |
175 | 210 | [dragAnimation]="flowchartConstants.dragAnimationRepaint" | ... | ... |
... | ... | @@ -14,6 +14,8 @@ |
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | 16 | |
17 | +@import './rule-node-colors'; | |
18 | + | |
17 | 19 | .tb-rulechain { |
18 | 20 | width: 100%; |
19 | 21 | height: 100%; |
... | ... | @@ -267,3 +269,60 @@ |
267 | 269 | } |
268 | 270 | } |
269 | 271 | } |
272 | + | |
273 | +.tb-rule-chain-context-menu { | |
274 | + min-width: 256px; | |
275 | + max-height: 404px; | |
276 | + border-radius: 8px; | |
277 | + margin-left: -20px; | |
278 | + | |
279 | + &.mat-menu-below { | |
280 | + margin-top: -60px; | |
281 | + } | |
282 | + | |
283 | + .mat-menu-content { | |
284 | + padding: 0; | |
285 | + display: flex; | |
286 | + flex-direction: column; | |
287 | + .tb-rule-chain-context-menu-container { | |
288 | + pointer-events: auto; | |
289 | + padding: 0 0 8px; | |
290 | + display: flex; | |
291 | + flex-direction: column; | |
292 | + overflow-y: auto; | |
293 | + } | |
294 | + } | |
295 | + | |
296 | + .tb-context-menu-header { | |
297 | + display: flex; | |
298 | + flex-direction: row; | |
299 | + height: 36px; | |
300 | + min-height: 36px; | |
301 | + padding: 8px 5px 5px; | |
302 | + font-size: 14px; | |
303 | + | |
304 | + @include rule-node-colors(); | |
305 | + | |
306 | + &.tb-rulechain-header { | |
307 | + background-color: #aac7e4; | |
308 | + } | |
309 | + | |
310 | + &.tb-link-header { | |
311 | + background-color: #aac7e4; | |
312 | + } | |
313 | + | |
314 | + .mat-icon { | |
315 | + padding-right: 10px; | |
316 | + padding-left: 2px; | |
317 | + margin: auto; | |
318 | + } | |
319 | + | |
320 | + .tb-context-menu-title { | |
321 | + font-weight: 500; | |
322 | + } | |
323 | + | |
324 | + .tb-context-menu-subtitle { | |
325 | + font-size: 12px; | |
326 | + } | |
327 | + } | |
328 | +} | ... | ... |
... | ... | @@ -18,9 +18,11 @@ import { |
18 | 18 | AfterViewInit, |
19 | 19 | Component, |
20 | 20 | ElementRef, |
21 | - HostBinding, Inject, | |
21 | + HostBinding, | |
22 | + Inject, | |
22 | 23 | OnInit, |
23 | - QueryList, SkipSelf, | |
24 | + QueryList, | |
25 | + SkipSelf, | |
24 | 26 | ViewChild, |
25 | 27 | ViewChildren, |
26 | 28 | ViewEncapsulation |
... | ... | @@ -31,7 +33,7 @@ import { AppState } from '@core/core.state'; |
31 | 33 | import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; |
32 | 34 | import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard'; |
33 | 35 | import { TranslateService } from '@ngx-translate/core'; |
34 | -import { MatDialog, MatExpansionPanel, ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; | |
36 | +import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialog, MatDialogRef, MatExpansionPanel } from '@angular/material'; | |
35 | 37 | import { DialogService } from '@core/services/dialog.service'; |
36 | 38 | import { AuthService } from '@core/auth/auth.service'; |
37 | 39 | import { ActivatedRoute, Router } from '@angular/router'; |
... | ... | @@ -41,8 +43,11 @@ import { |
41 | 43 | RuleChain, |
42 | 44 | ruleChainNodeComponent |
43 | 45 | } from '@shared/models/rule-chain.models'; |
44 | -import { FlowchartConstants, NgxFlowchartComponent, UserCallbacks } from 'ngx-flowchart/dist/ngx-flowchart'; | |
46 | +import { FcItemInfo, FlowchartConstants, NgxFlowchartComponent, UserCallbacks } from 'ngx-flowchart/dist/ngx-flowchart'; | |
45 | 47 | import { |
48 | + FcRuleEdge, | |
49 | + FcRuleNode, | |
50 | + FcRuleNodeType, | |
46 | 51 | getRuleNodeHelpLink, |
47 | 52 | LinkLabel, |
48 | 53 | RuleNodeComponentDescriptor, |
... | ... | @@ -50,24 +55,19 @@ import { |
50 | 55 | ruleNodeTypeDescriptors, |
51 | 56 | ruleNodeTypesLibrary |
52 | 57 | } from '@shared/models/rule-node.models'; |
53 | -import { FcRuleEdge, FcRuleNode, FcRuleNodeModel, FcRuleNodeType, FcRuleNodeTypeModel } from './rulechain-page.models'; | |
58 | +import { FcRuleNodeModel, FcRuleNodeTypeModel, RuleChainMenuContextInfo } from './rulechain-page.models'; | |
54 | 59 | import { RuleChainService } from '@core/http/rule-chain.service'; |
55 | -import { fromEvent, never, of, throwError, NEVER, Observable } from 'rxjs'; | |
56 | -import { debounceTime, distinctUntilChanged, map, tap, mergeMap } from 'rxjs/operators'; | |
60 | +import { fromEvent, NEVER, Observable, of } from 'rxjs'; | |
61 | +import { debounceTime, distinctUntilChanged, mergeMap, tap } from 'rxjs/operators'; | |
57 | 62 | import { ISearchableComponent } from '../../models/searchable-component.models'; |
58 | -import { deepClone, isDefined, isString } from '@core/utils'; | |
63 | +import { deepClone } from '@core/utils'; | |
59 | 64 | import { RuleNodeDetailsComponent } from '@home/pages/rulechain/rule-node-details.component'; |
60 | 65 | import { RuleNodeLinkComponent } from './rule-node-link.component'; |
61 | -import Timeout = NodeJS.Timeout; | |
62 | -import { Dashboard } from '@shared/models/dashboard.models'; | |
63 | -import { IAliasController } from '@core/api/widget-api.models'; | |
64 | -import { Widget, widgetTypesData } from '@shared/models/widget.models'; | |
65 | -import { WidgetConfigComponentData, WidgetInfo } from '@home/models/widget-component.models'; | |
66 | 66 | import { DialogComponent } from '@shared/components/dialog.component'; |
67 | -import { UtilsService } from '@core/services/utils.service'; | |
68 | -import { EntityService } from '@core/http/entity.service'; | |
69 | -import { AddWidgetDialogComponent, AddWidgetDialogData } from '@home/pages/dashboard/add-widget-dialog.component'; | |
70 | -import { RuleNodeConfigComponent } from '@home/pages/rulechain/rule-node-config.component'; | |
67 | +import { MatMenuTrigger } from '@angular/material/menu'; | |
68 | +import { ItemBufferService, RuleNodeConnection } from '@core/services/item-buffer.service'; | |
69 | +import Timeout = NodeJS.Timeout; | |
70 | +import { Hotkey, HotkeysService } from 'angular2-hotkeys'; | |
71 | 71 | |
72 | 72 | @Component({ |
73 | 73 | selector: 'tb-rulechain-page', |
... | ... | @@ -92,6 +92,12 @@ export class RuleChainPageComponent extends PageComponent |
92 | 92 | @ViewChildren('ruleNodeTypeExpansionPanels', |
93 | 93 | {read: MatExpansionPanel}) expansionPanels: QueryList<MatExpansionPanel>; |
94 | 94 | |
95 | + @ViewChild('ruleChainMenuTrigger', {static: true}) ruleChainMenuTrigger: MatMenuTrigger; | |
96 | + | |
97 | + ruleChainMenuPosition = { x: '0px', y: '0px' }; | |
98 | + | |
99 | + contextMenuEvent: MouseEvent; | |
100 | + | |
95 | 101 | ruleNodeTypeDescriptorsMap = ruleNodeTypeDescriptors; |
96 | 102 | ruleNodeTypesLibraryArray = ruleNodeTypesLibrary; |
97 | 103 | |
... | ... | @@ -116,6 +122,9 @@ export class RuleChainPageComponent extends PageComponent |
116 | 122 | isEditingRuleNodeLink = false; |
117 | 123 | editingRuleNodeLinkIndex = -1; |
118 | 124 | |
125 | + hotKeys: Hotkey[] = []; | |
126 | + | |
127 | + enableHotKeys = true; | |
119 | 128 | isLibraryOpen = true; |
120 | 129 | |
121 | 130 | ruleNodeSearch = ''; |
... | ... | @@ -173,7 +182,11 @@ export class RuleChainPageComponent extends PageComponent |
173 | 182 | } else { |
174 | 183 | const labels = this.ruleChainService.getRuleNodeSupportedLinks(sourceNode.component); |
175 | 184 | const allowCustomLabels = this.ruleChainService.ruleNodeAllowCustomLinks(sourceNode.component); |
185 | + this.enableHotKeys = false; | |
176 | 186 | return this.addRuleNodeLink(edge, labels, allowCustomLabels).pipe( |
187 | + tap(() => { | |
188 | + this.enableHotKeys = true; | |
189 | + }), | |
177 | 190 | mergeMap((res) => { |
178 | 191 | if (res) { |
179 | 192 | return of(res); |
... | ... | @@ -216,6 +229,7 @@ export class RuleChainPageComponent extends PageComponent |
216 | 229 | private ruleChainService: RuleChainService, |
217 | 230 | private authService: AuthService, |
218 | 231 | private translate: TranslateService, |
232 | + private itembuffer: ItemBufferService, | |
219 | 233 | public dialog: MatDialog, |
220 | 234 | public dialogService: DialogService, |
221 | 235 | public fb: FormBuilder) { |
... | ... | @@ -236,6 +250,7 @@ export class RuleChainPageComponent extends PageComponent |
236 | 250 | }) |
237 | 251 | ) |
238 | 252 | .subscribe(); |
253 | + this.ruleChainCanvas.adjustCanvasSize(true); | |
239 | 254 | } |
240 | 255 | |
241 | 256 | onSearchTextUpdated(searchText: string) { |
... | ... | @@ -244,6 +259,7 @@ export class RuleChainPageComponent extends PageComponent |
244 | 259 | } |
245 | 260 | |
246 | 261 | private init() { |
262 | + this.initHotKeys(); | |
247 | 263 | this.ruleChain = this.route.snapshot.data.ruleChain; |
248 | 264 | if (this.route.snapshot.data.import && !this.ruleChain) { |
249 | 265 | this.router.navigateByUrl('ruleChains'); |
... | ... | @@ -268,6 +284,89 @@ export class RuleChainPageComponent extends PageComponent |
268 | 284 | this.createRuleChainModel(); |
269 | 285 | } |
270 | 286 | |
287 | + private initHotKeys(): void { | |
288 | + this.hotKeys.push( | |
289 | + new Hotkey('ctrl+a', (event: KeyboardEvent) => { | |
290 | + if (this.enableHotKeys) { | |
291 | + event.preventDefault(); | |
292 | + this.ruleChainCanvas.modelService.selectAll(); | |
293 | + return false; | |
294 | + } | |
295 | + return true; | |
296 | + }, ['INPUT', 'SELECT', 'TEXTAREA'], | |
297 | + this.translate.instant('rulenode.select-all-objects')) | |
298 | + ); | |
299 | + this.hotKeys.push( | |
300 | + new Hotkey('ctrl+c', (event: KeyboardEvent) => { | |
301 | + if (this.enableHotKeys) { | |
302 | + event.preventDefault(); | |
303 | + this.copyRuleNodes(); | |
304 | + return false; | |
305 | + } | |
306 | + return true; | |
307 | + }, ['INPUT', 'SELECT', 'TEXTAREA'], | |
308 | + this.translate.instant('rulenode.copy-selected')) | |
309 | + ); | |
310 | + this.hotKeys.push( | |
311 | + new Hotkey('ctrl+v', (event: KeyboardEvent) => { | |
312 | + if (this.enableHotKeys) { | |
313 | + event.preventDefault(); | |
314 | + if (this.itembuffer.hasRuleNodes()) { | |
315 | + this.pasteRuleNodes(); | |
316 | + } | |
317 | + return false; | |
318 | + } | |
319 | + return true; | |
320 | + }, ['INPUT', 'SELECT', 'TEXTAREA'], | |
321 | + this.translate.instant('action.paste')) | |
322 | + ); | |
323 | + this.hotKeys.push( | |
324 | + new Hotkey('esc', (event: KeyboardEvent) => { | |
325 | + if (this.enableHotKeys) { | |
326 | + event.preventDefault(); | |
327 | + event.stopPropagation(); | |
328 | + this.ruleChainCanvas.modelService.deselectAll(); | |
329 | + return false; | |
330 | + } | |
331 | + return true; | |
332 | + }, ['INPUT', 'SELECT', 'TEXTAREA'], | |
333 | + this.translate.instant('rulenode.deselect-all-objects')) | |
334 | + ); | |
335 | + this.hotKeys.push( | |
336 | + new Hotkey('ctrl+s', (event: KeyboardEvent) => { | |
337 | + if (this.enableHotKeys) { | |
338 | + event.preventDefault(); | |
339 | + this.saveRuleChain(); | |
340 | + return false; | |
341 | + } | |
342 | + return true; | |
343 | + }, ['INPUT', 'SELECT', 'TEXTAREA'], | |
344 | + this.translate.instant('action.apply')) | |
345 | + ); | |
346 | + this.hotKeys.push( | |
347 | + new Hotkey('ctrl+z', (event: KeyboardEvent) => { | |
348 | + if (this.enableHotKeys) { | |
349 | + event.preventDefault(); | |
350 | + this.revertRuleChain(); | |
351 | + return false; | |
352 | + } | |
353 | + return true; | |
354 | + }, ['INPUT', 'SELECT', 'TEXTAREA'], | |
355 | + this.translate.instant('action.decline-changes')) | |
356 | + ); | |
357 | + this.hotKeys.push( | |
358 | + new Hotkey('del', (event: KeyboardEvent) => { | |
359 | + if (this.enableHotKeys) { | |
360 | + event.preventDefault(); | |
361 | + this.ruleChainCanvas.modelService.deleteSelected(); | |
362 | + return false; | |
363 | + } | |
364 | + return true; | |
365 | + }, ['INPUT', 'SELECT', 'TEXTAREA'], | |
366 | + this.translate.instant('rulenode.delete-selected-objects')) | |
367 | + ); | |
368 | + } | |
369 | + | |
271 | 370 | updateRuleChainLibrary() { |
272 | 371 | const search = this.ruleNodeTypeSearch.toUpperCase(); |
273 | 372 | const res = this.ruleNodeComponents.filter( |
... | ... | @@ -510,11 +609,229 @@ export class RuleChainPageComponent extends PageComponent |
510 | 609 | } |
511 | 610 | }); |
512 | 611 | } |
612 | + if (this.ruleChainCanvas) { | |
613 | + this.ruleChainCanvas.adjustCanvasSize(true); | |
614 | + } | |
513 | 615 | this.isDirtyValue = false; |
514 | 616 | this.updateRuleNodesHighlight(); |
515 | 617 | this.validate(); |
516 | 618 | } |
517 | 619 | |
620 | + openRuleChainContextMenu($event: MouseEvent) { | |
621 | + if (this.ruleChainCanvas.modelService && !$event.ctrlKey && !$event.metaKey) { | |
622 | + const x = $event.clientX; | |
623 | + const y = $event.clientY; | |
624 | + const item = this.ruleChainCanvas.modelService.getItemInfoAtPoint(x, y); | |
625 | + const contextInfo = this.prepareContextMenu(item); | |
626 | + if (contextInfo.menuItems && contextInfo.menuItems.length > 0) { | |
627 | + $event.preventDefault(); | |
628 | + $event.stopPropagation(); | |
629 | + this.contextMenuEvent = $event; | |
630 | + this.ruleChainMenuPosition.x = x + 'px'; | |
631 | + this.ruleChainMenuPosition.y = y + 'px'; | |
632 | + this.ruleChainMenuTrigger.menuData = { contextInfo }; | |
633 | + this.ruleChainMenuTrigger.openMenu(); | |
634 | + } | |
635 | + } | |
636 | + } | |
637 | + | |
638 | + onRuleChainContextMenuMouseLeave() { | |
639 | + this.ruleChainMenuTrigger.closeMenu(); | |
640 | + } | |
641 | + | |
642 | + private prepareContextMenu(item: FcItemInfo): RuleChainMenuContextInfo { | |
643 | + if (this.objectsSelected() || (!item.node && !item.edge)) { | |
644 | + return this.prepareRuleChainContextMenu(); | |
645 | + } else if (item.node) { | |
646 | + return this.prepareRuleNodeContextMenu(item.node); | |
647 | + } else if (item.edge) { | |
648 | + return this.prepareEdgeContextMenu(item.edge); | |
649 | + } | |
650 | + } | |
651 | + | |
652 | + private prepareRuleChainContextMenu(): RuleChainMenuContextInfo { | |
653 | + const contextInfo: RuleChainMenuContextInfo = { | |
654 | + headerClass: 'tb-rulechain-header', | |
655 | + icon: 'settings_ethernet', | |
656 | + title: this.ruleChain.name, | |
657 | + subtitle: this.translate.instant('rulechain.rulechain'), | |
658 | + menuItems: [] | |
659 | + }; | |
660 | + if (this.ruleChainCanvas.modelService.nodes.getSelectedNodes().length) { | |
661 | + contextInfo.menuItems.push( | |
662 | + { | |
663 | + action: () => { | |
664 | + this.copyRuleNodes(); | |
665 | + }, | |
666 | + enabled: true, | |
667 | + value: 'rulenode.copy-selected', | |
668 | + icon: 'content_copy', | |
669 | + shortcut: 'M-C' | |
670 | + } | |
671 | + ); | |
672 | + } | |
673 | + contextInfo.menuItems.push( | |
674 | + { | |
675 | + action: ($event) => { | |
676 | + this.pasteRuleNodes($event); | |
677 | + }, | |
678 | + enabled: this.itembuffer.hasRuleNodes(), | |
679 | + value: 'action.paste', | |
680 | + icon: 'content_paste', | |
681 | + shortcut: 'M-V' | |
682 | + } | |
683 | + ); | |
684 | + contextInfo.menuItems.push( | |
685 | + { | |
686 | + divider: true | |
687 | + } | |
688 | + ); | |
689 | + if (this.objectsSelected()) { | |
690 | + contextInfo.menuItems.push( | |
691 | + { | |
692 | + action: () => { | |
693 | + this.ruleChainCanvas.modelService.deselectAll(); | |
694 | + }, | |
695 | + enabled: true, | |
696 | + value: 'rulenode.deselect-all', | |
697 | + icon: 'tab_unselected', | |
698 | + shortcut: 'Esc' | |
699 | + } | |
700 | + ); | |
701 | + contextInfo.menuItems.push( | |
702 | + { | |
703 | + action: () => { | |
704 | + this.ruleChainCanvas.modelService.deleteSelected(); | |
705 | + }, | |
706 | + enabled: true, | |
707 | + value: 'rulenode.delete-selected', | |
708 | + icon: 'clear', | |
709 | + shortcut: 'Del' | |
710 | + } | |
711 | + ); | |
712 | + } else { | |
713 | + contextInfo.menuItems.push( | |
714 | + { | |
715 | + action: () => { | |
716 | + this.ruleChainCanvas.modelService.selectAll(); | |
717 | + }, | |
718 | + enabled: true, | |
719 | + value: 'rulenode.select-all', | |
720 | + icon: 'select_all', | |
721 | + shortcut: 'M-A' | |
722 | + } | |
723 | + ); | |
724 | + } | |
725 | + contextInfo.menuItems.push( | |
726 | + { | |
727 | + divider: true | |
728 | + } | |
729 | + ); | |
730 | + contextInfo.menuItems.push( | |
731 | + { | |
732 | + action: () => { | |
733 | + this.saveRuleChain(); | |
734 | + }, | |
735 | + enabled: !(this.isInvalid || (!this.isDirty && !this.isImport)), | |
736 | + value: 'action.apply-changes', | |
737 | + icon: 'done', | |
738 | + shortcut: 'M-S' | |
739 | + } | |
740 | + ); | |
741 | + contextInfo.menuItems.push( | |
742 | + { | |
743 | + action: () => { | |
744 | + this.revertRuleChain(); | |
745 | + }, | |
746 | + enabled: this.isDirty, | |
747 | + value: 'action.decline-changes', | |
748 | + icon: 'close', | |
749 | + shortcut: 'M-Z' | |
750 | + } | |
751 | + ); | |
752 | + return contextInfo; | |
753 | + } | |
754 | + | |
755 | + private prepareRuleNodeContextMenu(node: FcRuleNode): RuleChainMenuContextInfo { | |
756 | + const contextInfo: RuleChainMenuContextInfo = { | |
757 | + headerClass: node.nodeClass, | |
758 | + icon: node.icon, | |
759 | + iconUrl: node.iconUrl, | |
760 | + title: node.name, | |
761 | + subtitle: node.component.name, | |
762 | + menuItems: [] | |
763 | + }; | |
764 | + if (!node.readonly) { | |
765 | + contextInfo.menuItems.push( | |
766 | + { | |
767 | + action: () => { | |
768 | + this.openNodeDetails(node); | |
769 | + }, | |
770 | + enabled: true, | |
771 | + value: 'rulenode.details', | |
772 | + icon: 'menu' | |
773 | + } | |
774 | + ); | |
775 | + contextInfo.menuItems.push( | |
776 | + { | |
777 | + action: () => { | |
778 | + this.copyNode(node); | |
779 | + }, | |
780 | + enabled: true, | |
781 | + value: 'action.copy', | |
782 | + icon: 'content_copy' | |
783 | + } | |
784 | + ); | |
785 | + contextInfo.menuItems.push( | |
786 | + { | |
787 | + action: () => { | |
788 | + this.ruleChainCanvas.modelService.nodes.delete(node); | |
789 | + }, | |
790 | + enabled: true, | |
791 | + value: 'action.delete', | |
792 | + icon: 'clear', | |
793 | + shortcut: 'M-X' | |
794 | + } | |
795 | + ); | |
796 | + } | |
797 | + return contextInfo; | |
798 | + } | |
799 | + | |
800 | + private prepareEdgeContextMenu(edge: FcRuleEdge): RuleChainMenuContextInfo { | |
801 | + const contextInfo: RuleChainMenuContextInfo = { | |
802 | + headerClass: 'tb-link-header', | |
803 | + icon: 'trending_flat', | |
804 | + title: edge.label, | |
805 | + subtitle: this.translate.instant('rulenode.link'), | |
806 | + menuItems: [] | |
807 | + }; | |
808 | + const sourceNode: FcRuleNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source); | |
809 | + if (sourceNode.component.type != RuleNodeType.INPUT) { | |
810 | + contextInfo.menuItems.push( | |
811 | + { | |
812 | + action: () => { | |
813 | + this.openLinkDetails(edge); | |
814 | + }, | |
815 | + enabled: true, | |
816 | + value: 'rulenode.details', | |
817 | + icon: 'menu' | |
818 | + } | |
819 | + ); | |
820 | + } | |
821 | + contextInfo.menuItems.push( | |
822 | + { | |
823 | + action: () => { | |
824 | + this.ruleChainCanvas.modelService.edges.delete(edge); | |
825 | + }, | |
826 | + enabled: true, | |
827 | + value: 'action.delete', | |
828 | + icon: 'clear', | |
829 | + shortcut: 'M-X' | |
830 | + } | |
831 | + ); | |
832 | + return contextInfo; | |
833 | + } | |
834 | + | |
518 | 835 | onModelChanged() { |
519 | 836 | console.log('Model changed!'); |
520 | 837 | this.isDirtyValue = true; |
... | ... | @@ -531,6 +848,8 @@ export class RuleChainPageComponent extends PageComponent |
531 | 848 | |
532 | 849 | openNodeDetails(node: FcRuleNode) { |
533 | 850 | if (node.component.type !== RuleNodeType.INPUT) { |
851 | + this.enableHotKeys = false; | |
852 | + this.updateErrorTooltips(true); | |
534 | 853 | this.isEditingRuleNodeLink = false; |
535 | 854 | this.editingRuleNodeLink = null; |
536 | 855 | this.isEditingRuleNode = true; |
... | ... | @@ -545,6 +864,8 @@ export class RuleChainPageComponent extends PageComponent |
545 | 864 | openLinkDetails(edge: FcRuleEdge) { |
546 | 865 | const sourceNode: FcRuleNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source) as FcRuleNode; |
547 | 866 | if (sourceNode.component.type !== RuleNodeType.INPUT) { |
867 | + this.enableHotKeys = false; | |
868 | + this.updateErrorTooltips(true); | |
548 | 869 | this.isEditingRuleNode = false; |
549 | 870 | this.editingRuleNode = null; |
550 | 871 | this.editingRuleNodeLinkLabels = this.ruleChainService.getRuleNodeSupportedLinks(sourceNode.component); |
... | ... | @@ -558,9 +879,121 @@ export class RuleChainPageComponent extends PageComponent |
558 | 879 | } |
559 | 880 | } |
560 | 881 | |
882 | + private copyNode(node: FcRuleNode) { | |
883 | + this.itembuffer.copyRuleNodes([node], []); | |
884 | + } | |
885 | + | |
886 | + private copyRuleNodes() { | |
887 | + const nodes: FcRuleNode[] = this.ruleChainCanvas.modelService.nodes.getSelectedNodes(); | |
888 | + const edges: FcRuleEdge[] = this.ruleChainCanvas.modelService.edges.getSelectedEdges(); | |
889 | + const connections: RuleNodeConnection[] = []; | |
890 | + edges.forEach((edge) => { | |
891 | + const sourceNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source); | |
892 | + const destNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.destination); | |
893 | + const isInputSource = sourceNode.component.type == RuleNodeType.INPUT; | |
894 | + const fromIndex = nodes.indexOf(sourceNode); | |
895 | + const toIndex = nodes.indexOf(destNode); | |
896 | + if ( (isInputSource || fromIndex > -1) && toIndex > -1 ) { | |
897 | + const connection: RuleNodeConnection = { | |
898 | + isInputSource: isInputSource, | |
899 | + fromIndex: fromIndex, | |
900 | + toIndex: toIndex, | |
901 | + label: edge.label, | |
902 | + labels: edge.labels | |
903 | + }; | |
904 | + connections.push(connection); | |
905 | + } | |
906 | + }); | |
907 | + this.itembuffer.copyRuleNodes(nodes, connections); | |
908 | + } | |
909 | + | |
910 | + private pasteRuleNodes(event?: MouseEvent) { | |
911 | + const canvas = $(this.ruleChainCanvas.modelService.canvasHtmlElement); | |
912 | + let x: number; | |
913 | + let y: number; | |
914 | + if (event) { | |
915 | + const offset = canvas.offset(); | |
916 | + x = Math.round(event.clientX - offset.left); | |
917 | + y = Math.round(event.clientY - offset.top); | |
918 | + } else { | |
919 | + const scrollParent = canvas.parent(); | |
920 | + const scrollTop = scrollParent.scrollTop(); | |
921 | + const scrollLeft = scrollParent.scrollLeft(); | |
922 | + x = scrollLeft + scrollParent.width()/2; | |
923 | + y = scrollTop + scrollParent.height()/2; | |
924 | + } | |
925 | + const ruleNodes = this.itembuffer.pasteRuleNodes(x, y); | |
926 | + if (ruleNodes) { | |
927 | + this.ruleChainCanvas.modelService.deselectAll(); | |
928 | + const nodes: FcRuleNode[] = []; | |
929 | + ruleNodes.nodes.forEach((node) => { | |
930 | + node.id = 'rule-chain-node-' + this.nextNodeID++; | |
931 | + const component = node.component; | |
932 | + if (component.configurationDescriptor.nodeDefinition.inEnabled) { | |
933 | + node.connectors.push( | |
934 | + { | |
935 | + type: FlowchartConstants.leftConnectorType, | |
936 | + id: (this.nextConnectorID++) + '' | |
937 | + } | |
938 | + ); | |
939 | + } | |
940 | + if (component.configurationDescriptor.nodeDefinition.outEnabled) { | |
941 | + node.connectors.push( | |
942 | + { | |
943 | + type: FlowchartConstants.rightConnectorType, | |
944 | + id: (this.nextConnectorID++) + '' | |
945 | + } | |
946 | + ); | |
947 | + } | |
948 | + nodes.push(node); | |
949 | + this.ruleChainModel.nodes.push(node); | |
950 | + this.ruleChainCanvas.modelService.nodes.select(node); | |
951 | + }); | |
952 | + ruleNodes.connections.forEach((connection) => { | |
953 | + const sourceNode = nodes[connection.fromIndex]; | |
954 | + const destNode = nodes[connection.toIndex]; | |
955 | + if ( (connection.isInputSource || sourceNode) && destNode ) { | |
956 | + let source: string; | |
957 | + let destination: string; | |
958 | + if (connection.isInputSource) { | |
959 | + source = this.inputConnectorId + ''; | |
960 | + const found = this.ruleChainModel.edges.find(theEdge => theEdge.source === (this.inputConnectorId + '')); | |
961 | + if (found) { | |
962 | + this.ruleChainCanvas.modelService.edges.delete(found); | |
963 | + } | |
964 | + } else { | |
965 | + const sourceConnectors = this.ruleChainCanvas.modelService.nodes.getConnectorsByType(sourceNode, FlowchartConstants.rightConnectorType); | |
966 | + if (sourceConnectors && sourceConnectors.length) { | |
967 | + source = sourceConnectors[0].id; | |
968 | + } | |
969 | + } | |
970 | + const destConnectors = this.ruleChainCanvas.modelService.nodes.getConnectorsByType(destNode, FlowchartConstants.leftConnectorType); | |
971 | + if (destConnectors && destConnectors.length) { | |
972 | + destination = destConnectors[0].id; | |
973 | + } | |
974 | + if (source && destination) { | |
975 | + const edge: FcRuleEdge = { | |
976 | + source: source, | |
977 | + destination: destination, | |
978 | + label: connection.label, | |
979 | + labels: connection.labels | |
980 | + }; | |
981 | + this.ruleChainModel.edges.push(edge); | |
982 | + this.ruleChainCanvas.modelService.edges.select(edge); | |
983 | + } | |
984 | + } | |
985 | + }); | |
986 | + this.updateRuleNodesHighlight(); | |
987 | + this.validate(); | |
988 | + this.onModelChanged(); | |
989 | + } | |
990 | + } | |
991 | + | |
561 | 992 | onDetailsDrawerClosed() { |
562 | 993 | this.onEditRuleNodeClosed(); |
563 | 994 | this.onEditRuleNodeLinkClosed(); |
995 | + this.enableHotKeys = true; | |
996 | + this.updateErrorTooltips(false); | |
564 | 997 | } |
565 | 998 | |
566 | 999 | onEditRuleNodeClosed() { |
... | ... | @@ -739,6 +1172,7 @@ export class RuleChainPageComponent extends PageComponent |
739 | 1172 | addRuleNode(ruleNode: FcRuleNode) { |
740 | 1173 | ruleNode.configuration = deepClone(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration); |
741 | 1174 | const ruleChainId = this.ruleChain.id ? this.ruleChain.id.id : null; |
1175 | + this.enableHotKeys = false; | |
742 | 1176 | this.dialog.open<AddRuleNodeDialogComponent, AddRuleNodeDialogData, |
743 | 1177 | FcRuleNode>(AddRuleNodeDialogComponent, { |
744 | 1178 | disableClose: true, |
... | ... | @@ -772,6 +1206,7 @@ export class RuleChainPageComponent extends PageComponent |
772 | 1206 | this.onModelChanged(); |
773 | 1207 | this.updateRuleNodesHighlight(); |
774 | 1208 | } |
1209 | + this.enableHotKeys = true; | |
775 | 1210 | } |
776 | 1211 | ); |
777 | 1212 | } |
... | ... | @@ -836,6 +1271,17 @@ export class RuleChainPageComponent extends PageComponent |
836 | 1271 | } |
837 | 1272 | } |
838 | 1273 | |
1274 | + private updateErrorTooltips(hide: boolean) { | |
1275 | + for (const nodeId of Object.keys(this.errorTooltips)) { | |
1276 | + const tooltip = this.errorTooltips[nodeId]; | |
1277 | + if (hide) { | |
1278 | + tooltip.close(); | |
1279 | + } else { | |
1280 | + tooltip.open(); | |
1281 | + } | |
1282 | + } | |
1283 | + } | |
1284 | + | |
839 | 1285 | private displayTooltip(event: MouseEvent, content: string) { |
840 | 1286 | this.destroyTooltips(); |
841 | 1287 | this.tooltipTimeout = setTimeout(() => { | ... | ... |
... | ... | @@ -14,37 +14,32 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { FcNode, FcEdge, FcModel } from 'ngx-flowchart/dist/ngx-flowchart'; | |
18 | -import { RuleNodeComponentDescriptor, RuleNodeConfiguration } from '@shared/models/rule-node.models'; | |
19 | -import { RuleNodeId } from '@app/shared/models/id/rule-node-id'; | |
20 | -import { RuleChainId } from '@shared/models/id/rule-chain-id'; | |
21 | - | |
22 | -export interface FcRuleNodeType extends FcNode { | |
23 | - component: RuleNodeComponentDescriptor; | |
24 | - nodeClass: string; | |
25 | - icon: string; | |
26 | - iconUrl?: string; | |
27 | -} | |
17 | +import { FcModel } from 'ngx-flowchart/dist/ngx-flowchart'; | |
18 | +import { FcRuleEdge, FcRuleNode, FcRuleNodeType } from '@shared/models/rule-node.models'; | |
28 | 19 | |
29 | 20 | export interface FcRuleNodeTypeModel extends FcModel { |
30 | 21 | nodes: Array<FcRuleNodeType>; |
31 | 22 | } |
32 | 23 | |
33 | -export interface FcRuleNode extends FcRuleNodeType { | |
34 | - ruleNodeId?: RuleNodeId; | |
35 | - additionalInfo?: any; | |
36 | - configuration?: RuleNodeConfiguration; | |
37 | - debugMode?: boolean; | |
38 | - targetRuleChainId?: string; | |
39 | - error?: string; | |
40 | - highlighted?: boolean; | |
24 | +export interface FcRuleNodeModel extends FcModel { | |
25 | + nodes: Array<FcRuleNode>; | |
26 | + edges: Array<FcRuleEdge>; | |
41 | 27 | } |
42 | 28 | |
43 | -export interface FcRuleEdge extends FcEdge { | |
44 | - labels?: string[]; | |
29 | +export interface RuleChainMenuItem { | |
30 | + action?: ($event: MouseEvent) => void; | |
31 | + enabled?: boolean; | |
32 | + value?: string; | |
33 | + icon?: string; | |
34 | + shortcut?: string; | |
35 | + divider?: boolean; | |
45 | 36 | } |
46 | 37 | |
47 | -export interface FcRuleNodeModel extends FcModel { | |
48 | - nodes: Array<FcRuleNode>; | |
49 | - edges: Array<FcRuleEdge>; | |
38 | +export interface RuleChainMenuContextInfo { | |
39 | + headerClass: string; | |
40 | + icon: string; | |
41 | + iconUrl?: string; | |
42 | + title: string; | |
43 | + subtitle: string; | |
44 | + menuItems: RuleChainMenuItem[]; | |
50 | 45 | } | ... | ... |
... | ... | @@ -14,6 +14,8 @@ |
14 | 14 | * limitations under the License. |
15 | 15 | */ |
16 | 16 | |
17 | +@import './rule-node-colors'; | |
18 | + | |
17 | 19 | :host { |
18 | 20 | |
19 | 21 | .fc-node-overlay { |
... | ... | @@ -63,33 +65,7 @@ |
63 | 65 | border: solid 1px #777; |
64 | 66 | border-radius: 5px; |
65 | 67 | |
66 | - &.tb-filter-type { | |
67 | - background-color: #f1e861; | |
68 | - } | |
69 | - | |
70 | - &.tb-enrichment-type { | |
71 | - background-color: #cdf14e; | |
72 | - } | |
73 | - | |
74 | - &.tb-transformation-type { | |
75 | - background-color: #79cef1; | |
76 | - } | |
77 | - | |
78 | - &.tb-action-type { | |
79 | - background-color: #f1928f; | |
80 | - } | |
81 | - | |
82 | - &.tb-external-type { | |
83 | - background-color: #fbc766; | |
84 | - } | |
85 | - | |
86 | - &.tb-rule-chain-type { | |
87 | - background-color: #d6c4f1; | |
88 | - } | |
89 | - | |
90 | - &.tb-unknown-type { | |
91 | - background-color: #f16c29; | |
92 | - } | |
68 | + @include rule-node-colors(); | |
93 | 69 | |
94 | 70 | &.tb-rule-node-highlighted:not(.tb-rule-node-invalid) { |
95 | 71 | box-shadow: 0 0 10px 6px #51cbee; | ... | ... |
... | ... | @@ -15,9 +15,9 @@ |
15 | 15 | limitations under the License. |
16 | 16 | |
17 | 17 | --> |
18 | -<hotkeys-cheatsheet></hotkeys-cheatsheet> | |
19 | 18 | <div fxFlex fxLayout="column"> |
20 | - <div fxFlex fxLayout="column" tb-fullscreen [fullscreen]="fullscreen"> | |
19 | + <div fxFlex fxLayout="column" tb-fullscreen [fullscreen]="fullscreen" tb-hotkeys [hotkeys]="hotKeys" [cheatSheet]="cheatSheetComponent"> | |
20 | + <tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet> | |
21 | 21 | <mat-toolbar class="mat-elevation-z1 tb-edit-toolbar mat-hue-3" fxLayoutGap="16px"> |
22 | 22 | <mat-form-field floatLabel="always" hideRequiredMarker class="tb-widget-title"> |
23 | 23 | <mat-label></mat-label> | ... | ... |
... | ... | @@ -139,6 +139,8 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe |
139 | 139 | |
140 | 140 | saveWidgetTimeout: Timeout; |
141 | 141 | |
142 | + hotKeys: Hotkey[] = []; | |
143 | + | |
142 | 144 | private rxSubscriptions = new Array<Subscription>(); |
143 | 145 | |
144 | 146 | constructor(protected store: Store<AppState>, |
... | ... | @@ -146,7 +148,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe |
146 | 148 | private route: ActivatedRoute, |
147 | 149 | private router: Router, |
148 | 150 | private widgetService: WidgetService, |
149 | - private hotkeysService: HotkeysService, | |
150 | 151 | private translate: TranslateService, |
151 | 152 | private raf: RafService, |
152 | 153 | private dialog: MatDialog) { |
... | ... | @@ -159,6 +160,8 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe |
159 | 160 | this.init(data); |
160 | 161 | } |
161 | 162 | )); |
163 | + | |
164 | + this.initHotKeys(); | |
162 | 165 | } |
163 | 166 | |
164 | 167 | private init(data: any) { |
... | ... | @@ -181,7 +184,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe |
181 | 184 | } |
182 | 185 | |
183 | 186 | ngOnInit(): void { |
184 | - this.initHotKeys(); | |
185 | 187 | this.initSplitLayout(); |
186 | 188 | this.initAceEditors(); |
187 | 189 | this.iframe = $(this.widgetIFrameElmRef.nativeElement); |
... | ... | @@ -203,7 +205,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe |
203 | 205 | } |
204 | 206 | |
205 | 207 | private initHotKeys(): void { |
206 | - this.hotkeysService.add( | |
208 | + this.hotKeys.push( | |
207 | 209 | new Hotkey('ctrl+q', (event: KeyboardEvent) => { |
208 | 210 | if (!getCurrentIsLoading(this.store) && !this.undoDisabled()) { |
209 | 211 | event.preventDefault(); |
... | ... | @@ -213,7 +215,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe |
213 | 215 | }, ['INPUT', 'SELECT', 'TEXTAREA'], |
214 | 216 | this.translate.instant('widget.undo')) |
215 | 217 | ); |
216 | - this.hotkeysService.add( | |
218 | + this.hotKeys.push( | |
217 | 219 | new Hotkey('ctrl+s', (event: KeyboardEvent) => { |
218 | 220 | if (!getCurrentIsLoading(this.store) && !this.saveDisabled()) { |
219 | 221 | event.preventDefault(); |
... | ... | @@ -223,7 +225,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe |
223 | 225 | }, ['INPUT', 'SELECT', 'TEXTAREA'], |
224 | 226 | this.translate.instant('widget.save')) |
225 | 227 | ); |
226 | - this.hotkeysService.add( | |
228 | + this.hotKeys.push( | |
227 | 229 | new Hotkey('shift+ctrl+s', (event: KeyboardEvent) => { |
228 | 230 | if (!getCurrentIsLoading(this.store) && !this.saveAsDisabled()) { |
229 | 231 | event.preventDefault(); |
... | ... | @@ -233,7 +235,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe |
233 | 235 | }, ['INPUT', 'SELECT', 'TEXTAREA'], |
234 | 236 | this.translate.instant('widget.saveAs')) |
235 | 237 | ); |
236 | - this.hotkeysService.add( | |
238 | + this.hotKeys.push( | |
237 | 239 | new Hotkey('shift+ctrl+f', (event: KeyboardEvent) => { |
238 | 240 | event.preventDefault(); |
239 | 241 | this.fullscreen = !this.fullscreen; |
... | ... | @@ -241,7 +243,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe |
241 | 243 | }, ['INPUT', 'SELECT', 'TEXTAREA'], |
242 | 244 | this.translate.instant('widget.toggle-fullscreen')) |
243 | 245 | ); |
244 | - this.hotkeysService.add( | |
246 | + this.hotKeys.push( | |
245 | 247 | new Hotkey('ctrl+enter', (event: KeyboardEvent) => { |
246 | 248 | event.preventDefault(); |
247 | 249 | this.applyWidgetScript(); | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import { Component, ElementRef, Input, OnDestroy, OnInit } from '@angular/core'; | |
18 | +import { Hotkey, HotkeysService } from 'angular2-hotkeys'; | |
19 | + | |
20 | +@Component({ | |
21 | + selector : 'tb-hotkeys-cheatsheet', | |
22 | + styles : [` | |
23 | +.tb-hotkeys-container { | |
24 | + display: table !important; | |
25 | + position: fixed; | |
26 | + width: 100%; | |
27 | + height: 100%; | |
28 | + top: 0; | |
29 | + left: 0; | |
30 | + color: #333; | |
31 | + font-size: 1em; | |
32 | + background-color: rgba(255,255,255,0.9); | |
33 | + outline: 0; | |
34 | +} | |
35 | +.tb-hotkeys-container.fade { | |
36 | + z-index: -1024; | |
37 | + visibility: hidden; | |
38 | + opacity: 0; | |
39 | + -webkit-transition: opacity 0.15s linear; | |
40 | + -moz-transition: opacity 0.15s linear; | |
41 | + -o-transition: opacity 0.15s linear; | |
42 | + transition: opacity 0.15s linear; | |
43 | +} | |
44 | +.tb-hotkeys-container.fade.in { | |
45 | + z-index: 10002; | |
46 | + visibility: visible; | |
47 | + opacity: 1; | |
48 | +} | |
49 | +.tb-hotkeys-title { | |
50 | + font-weight: bold; | |
51 | + text-align: center; | |
52 | + font-size: 1.2em; | |
53 | +} | |
54 | +.tb-hotkeys { | |
55 | + width: 100%; | |
56 | + height: 100%; | |
57 | + display: table-cell; | |
58 | + vertical-align: middle; | |
59 | +} | |
60 | +.tb-hotkeys table { | |
61 | + margin: auto; | |
62 | + color: #333; | |
63 | +} | |
64 | +.tb-content { | |
65 | + display: table-cell; | |
66 | + vertical-align: middle; | |
67 | +} | |
68 | +.tb-hotkeys-keys { | |
69 | + padding: 5px; | |
70 | + text-align: right; | |
71 | +} | |
72 | +.tb-hotkeys-key { | |
73 | + display: inline-block; | |
74 | + color: #fff; | |
75 | + background-color: #333; | |
76 | + border: 1px solid #333; | |
77 | + border-radius: 5px; | |
78 | + text-align: center; | |
79 | + margin-right: 5px; | |
80 | + box-shadow: inset 0 1px 0 #666, 0 1px 0 #bbb; | |
81 | + padding: 5px 9px; | |
82 | + font-size: 1em; | |
83 | +} | |
84 | +.tb-hotkeys-text { | |
85 | + padding-left: 10px; | |
86 | + font-size: 1em; | |
87 | +} | |
88 | +.tb-hotkeys-close { | |
89 | + position: fixed; | |
90 | + top: 20px; | |
91 | + right: 20px; | |
92 | + font-size: 2em; | |
93 | + font-weight: bold; | |
94 | + padding: 5px 10px; | |
95 | + border: 1px solid #ddd; | |
96 | + border-radius: 5px; | |
97 | + min-height: 45px; | |
98 | + min-width: 45px; | |
99 | + text-align: center; | |
100 | +} | |
101 | +.tb-hotkeys-close:hover { | |
102 | + background-color: #fff; | |
103 | + cursor: pointer; | |
104 | +} | |
105 | +@media all and (max-width: 500px) { | |
106 | + .tb-hotkeys { | |
107 | + font-size: 0.8em; | |
108 | + } | |
109 | +} | |
110 | +@media all and (min-width: 750px) { | |
111 | + .tb-hotkeys { | |
112 | + font-size: 1.2em; | |
113 | + } | |
114 | +} `], | |
115 | + template : `<div tabindex="-1" class="tb-hotkeys-container fade" [ngClass]="{'in': helpVisible}" style="display:none"><div class="tb-hotkeys"> | |
116 | + <h4 class="tb-hotkeys-title">{{ title }}</h4> | |
117 | + <table *ngIf="helpVisible"><tbody> | |
118 | + <tr *ngFor="let hotkey of hotkeysList"> | |
119 | + <td class="tb-hotkeys-keys"> | |
120 | + <span *ngFor="let key of hotkey.formatted" class="tb-hotkeys-key">{{ key }}</span> | |
121 | + </td> | |
122 | + <td class="tb-hotkeys-text">{{ hotkey.description }}</td> | |
123 | + </tr> | |
124 | + </tbody></table> | |
125 | + <div class="tb-hotkeys-close" (click)="toggleCheatSheet()">×</div> | |
126 | +</div></div>`, | |
127 | +}) | |
128 | +export class TbCheatSheetComponent implements OnInit, OnDestroy { | |
129 | + | |
130 | + helpVisible = false; | |
131 | + @Input() title: string = 'Keyboard Shortcuts:'; | |
132 | + | |
133 | + @Input() | |
134 | + hotkeys: Hotkey[]; | |
135 | + | |
136 | + hotkeysList: Hotkey[]; | |
137 | + | |
138 | + private mousetrap: MousetrapInstance; | |
139 | + | |
140 | + constructor(private _elementRef: ElementRef, | |
141 | + private hotkeysService: HotkeysService) { | |
142 | + this.mousetrap = new Mousetrap(this._elementRef.nativeElement); | |
143 | + this.mousetrap.bind('?', (event: KeyboardEvent, combo: string) => { | |
144 | + this.toggleCheatSheet(); | |
145 | + }); | |
146 | + } | |
147 | + | |
148 | + public ngOnInit(): void { | |
149 | + if (this.hotkeys) { | |
150 | + this.hotkeysList = this.hotkeys.filter(hotkey => hotkey.description); | |
151 | + } | |
152 | + } | |
153 | + | |
154 | + public setHotKeys(hotkeys: Hotkey[]) { | |
155 | + this.hotkeysList = hotkeys.filter(hotkey => hotkey.description); | |
156 | + } | |
157 | + | |
158 | + public toggleCheatSheet(): void { | |
159 | + this.helpVisible = !this.helpVisible; | |
160 | + } | |
161 | + | |
162 | + ngOnDestroy() { | |
163 | + this.mousetrap.unbind('?'); | |
164 | + } | |
165 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 The Thingsboard Authors | |
3 | +/// | |
4 | +/// Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | +/// you may not use this file except in compliance with the License. | |
6 | +/// You may obtain a copy of the License at | |
7 | +/// | |
8 | +/// http://www.apache.org/licenses/LICENSE-2.0 | |
9 | +/// | |
10 | +/// Unless required by applicable law or agreed to in writing, software | |
11 | +/// distributed under the License is distributed on an "AS IS" BASIS, | |
12 | +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | +/// See the License for the specific language governing permissions and | |
14 | +/// limitations under the License. | |
15 | +/// | |
16 | + | |
17 | +import {Directive, Input, OnInit, OnDestroy, ElementRef} from '@angular/core'; | |
18 | +import {Hotkey, ExtendedKeyboardEvent} from 'angular2-hotkeys'; | |
19 | +import 'mousetrap'; | |
20 | +import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component'; | |
21 | + | |
22 | +@Directive({ | |
23 | + selector : '[tb-hotkeys]' | |
24 | +}) | |
25 | +export class TbHotkeysDirective implements OnInit, OnDestroy { | |
26 | + @Input() hotkeys: Hotkey[] = []; | |
27 | + @Input() cheatSheet: TbCheatSheetComponent; | |
28 | + | |
29 | + private mousetrap: MousetrapInstance; | |
30 | + private hotkeysList: Hotkey[] = []; | |
31 | + | |
32 | + private _preventIn = ['INPUT', 'SELECT', 'TEXTAREA']; | |
33 | + | |
34 | + constructor(private _elementRef: ElementRef) { | |
35 | + this.mousetrap = new Mousetrap(this._elementRef.nativeElement); | |
36 | + (this._elementRef.nativeElement as HTMLElement).tabIndex = -1; | |
37 | + (this._elementRef.nativeElement as HTMLElement).style.outline = '0'; | |
38 | + } | |
39 | + | |
40 | + ngOnInit() { | |
41 | + for (let hotkey of this.hotkeys) { | |
42 | + this.hotkeysList.push(hotkey); | |
43 | + this.bindEvent(hotkey); | |
44 | + } | |
45 | + if (this.cheatSheet) { | |
46 | + let hotkeyObj: Hotkey = new Hotkey( | |
47 | + '?', | |
48 | + (event: KeyboardEvent) => { | |
49 | + this.cheatSheet.toggleCheatSheet(); | |
50 | + return false; | |
51 | + }, | |
52 | + [], | |
53 | + 'Show / hide this help menu', | |
54 | + ); | |
55 | + this.hotkeysList.unshift(hotkeyObj); | |
56 | + this.bindEvent(hotkeyObj); | |
57 | + this.cheatSheet.setHotKeys(this.hotkeysList); | |
58 | + } | |
59 | + } | |
60 | + | |
61 | + private bindEvent(hotkey: Hotkey): void { | |
62 | + this.mousetrap.bind((<Hotkey>hotkey).combo, (event: KeyboardEvent, combo: string) => { | |
63 | + let shouldExecute = true; | |
64 | + if(event) { | |
65 | + let target: HTMLElement = <HTMLElement>(event.target || event.srcElement); | |
66 | + let nodeName: string = target.nodeName.toUpperCase(); | |
67 | + if((' ' + target.className + ' ').indexOf(' mousetrap ') > -1) { | |
68 | + shouldExecute = true; | |
69 | + } else if(this._preventIn.indexOf(nodeName) > -1 && (<Hotkey>hotkey).allowIn.map(allow => allow.toUpperCase()).indexOf(nodeName) === -1) { | |
70 | + shouldExecute = false; | |
71 | + } | |
72 | + } | |
73 | + | |
74 | + if(shouldExecute) { | |
75 | + return (<Hotkey>hotkey).callback.apply(this, [event, combo]); | |
76 | + } | |
77 | + }); | |
78 | + } | |
79 | + | |
80 | + ngOnDestroy() { | |
81 | + for (let hotkey of this.hotkeysList) { | |
82 | + this.mousetrap.unbind(hotkey.combo); | |
83 | + } | |
84 | + } | |
85 | + | |
86 | +} | ... | ... |
... | ... | @@ -14,18 +14,14 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import {BaseData} from '@shared/models/base-data'; | |
18 | -import {AssetId} from '@shared/models/id/asset-id'; | |
19 | -import {TenantId} from '@shared/models/id/tenant-id'; | |
20 | -import {CustomerId} from '@shared/models/id/customer-id'; | |
21 | -import {RuleChainId} from '@shared/models/id/rule-chain-id'; | |
22 | -import {RuleNodeId} from '@shared/models/id/rule-node-id'; | |
23 | -import { ComponentDescriptor, ComponentType } from '@shared/models/component-descriptor.models'; | |
24 | -import { EntityType, EntityTypeResource } from '@shared/models/entity-type.models'; | |
17 | +import { BaseData } from '@shared/models/base-data'; | |
18 | +import { RuleChainId } from '@shared/models/id/rule-chain-id'; | |
19 | +import { RuleNodeId } from '@shared/models/id/rule-node-id'; | |
20 | +import { ComponentDescriptor } from '@shared/models/component-descriptor.models'; | |
21 | +import { FcEdge, FcNode } from 'ngx-flowchart/dist/ngx-flowchart'; | |
25 | 22 | import { Observable } from 'rxjs'; |
26 | 23 | import { PageComponent } from '@shared/components/page.component'; |
27 | -import { AfterViewInit, ComponentFactory, EventEmitter, Inject, OnDestroy, OnInit } from '@angular/core'; | |
28 | -import { RafService } from '@core/services/raf.service'; | |
24 | +import { AfterViewInit, EventEmitter, Inject, OnInit } from '@angular/core'; | |
29 | 25 | import { Store } from '@ngrx/store'; |
30 | 26 | import { AppState } from '@core/core.state'; |
31 | 27 | import { AbstractControl, FormGroup } from '@angular/forms'; |
... | ... | @@ -38,7 +34,6 @@ export enum MsgDataType { |
38 | 34 | |
39 | 35 | export interface RuleNodeConfiguration { |
40 | 36 | [key: string]: any; |
41 | - // TODO: | |
42 | 37 | } |
43 | 38 | |
44 | 39 | export interface RuleNode extends BaseData<RuleNodeId> { |
... | ... | @@ -307,6 +302,28 @@ export interface RuleNodeComponentDescriptor extends ComponentDescriptor { |
307 | 302 | configurationDescriptor?: RuleNodeConfigurationDescriptor; |
308 | 303 | } |
309 | 304 | |
305 | +export interface FcRuleNodeType extends FcNode { | |
306 | + component?: RuleNodeComponentDescriptor; | |
307 | + nodeClass?: string; | |
308 | + icon?: string; | |
309 | + iconUrl?: string; | |
310 | +} | |
311 | + | |
312 | +export interface FcRuleNode extends FcRuleNodeType { | |
313 | + ruleNodeId?: RuleNodeId; | |
314 | + additionalInfo?: any; | |
315 | + configuration?: RuleNodeConfiguration; | |
316 | + debugMode?: boolean; | |
317 | + targetRuleChainId?: string; | |
318 | + error?: string; | |
319 | + highlighted?: boolean; | |
320 | + componentClazz?: string; | |
321 | +} | |
322 | + | |
323 | +export interface FcRuleEdge extends FcEdge { | |
324 | + labels?: string[]; | |
325 | +} | |
326 | + | |
310 | 327 | export interface TestScriptInputParams { |
311 | 328 | script: string; |
312 | 329 | scriptType: string; | ... | ... |
... | ... | @@ -118,6 +118,8 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc |
118 | 118 | import { MessageTypeAutocompleteComponent } from './components/message-type-autocomplete.component'; |
119 | 119 | import { JsonContentComponent } from './components/json-content.component'; |
120 | 120 | import { KeyValMapComponent } from './components/kv-map.component'; |
121 | +import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component'; | |
122 | +import { TbHotkeysDirective } from '@shared/components/hotkeys.directive'; | |
121 | 123 | |
122 | 124 | @NgModule({ |
123 | 125 | providers: [ |
... | ... | @@ -149,11 +151,13 @@ import { KeyValMapComponent } from './components/kv-map.component'; |
149 | 151 | FullscreenDirective, |
150 | 152 | CircularProgressDirective, |
151 | 153 | MatChipDraggableDirective, |
154 | + TbHotkeysDirective, | |
152 | 155 | TbAnchorComponent, |
153 | 156 | HelpComponent, |
154 | 157 | TbCheckboxComponent, |
155 | 158 | TbSnackBarComponent, |
156 | 159 | TbErrorComponent, |
160 | + TbCheatSheetComponent, | |
157 | 161 | BreadcrumbComponent, |
158 | 162 | UserMenuComponent, |
159 | 163 | TimewindowComponent, |
... | ... | @@ -256,10 +260,12 @@ import { KeyValMapComponent } from './components/kv-map.component'; |
256 | 260 | FullscreenDirective, |
257 | 261 | CircularProgressDirective, |
258 | 262 | MatChipDraggableDirective, |
263 | + TbHotkeysDirective, | |
259 | 264 | TbAnchorComponent, |
260 | 265 | HelpComponent, |
261 | 266 | TbCheckboxComponent, |
262 | 267 | TbErrorComponent, |
268 | + TbCheatSheetComponent, | |
263 | 269 | BreadcrumbComponent, |
264 | 270 | UserMenuComponent, |
265 | 271 | TimewindowComponent, | ... | ... |