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,14 +1407,12 @@ | ||
1407 | "balanced-match": { | 1407 | "balanced-match": { |
1408 | "version": "1.0.0", | 1408 | "version": "1.0.0", |
1409 | "bundled": true, | 1409 | "bundled": true, |
1410 | - "dev": true, | ||
1411 | - "optional": true | 1410 | + "dev": true |
1412 | }, | 1411 | }, |
1413 | "brace-expansion": { | 1412 | "brace-expansion": { |
1414 | "version": "1.1.11", | 1413 | "version": "1.1.11", |
1415 | "bundled": true, | 1414 | "bundled": true, |
1416 | "dev": true, | 1415 | "dev": true, |
1417 | - "optional": true, | ||
1418 | "requires": { | 1416 | "requires": { |
1419 | "balanced-match": "^1.0.0", | 1417 | "balanced-match": "^1.0.0", |
1420 | "concat-map": "0.0.1" | 1418 | "concat-map": "0.0.1" |
@@ -1429,20 +1427,17 @@ | @@ -1429,20 +1427,17 @@ | ||
1429 | "code-point-at": { | 1427 | "code-point-at": { |
1430 | "version": "1.1.0", | 1428 | "version": "1.1.0", |
1431 | "bundled": true, | 1429 | "bundled": true, |
1432 | - "dev": true, | ||
1433 | - "optional": true | 1430 | + "dev": true |
1434 | }, | 1431 | }, |
1435 | "concat-map": { | 1432 | "concat-map": { |
1436 | "version": "0.0.1", | 1433 | "version": "0.0.1", |
1437 | "bundled": true, | 1434 | "bundled": true, |
1438 | - "dev": true, | ||
1439 | - "optional": true | 1435 | + "dev": true |
1440 | }, | 1436 | }, |
1441 | "console-control-strings": { | 1437 | "console-control-strings": { |
1442 | "version": "1.1.0", | 1438 | "version": "1.1.0", |
1443 | "bundled": true, | 1439 | "bundled": true, |
1444 | - "dev": true, | ||
1445 | - "optional": true | 1440 | + "dev": true |
1446 | }, | 1441 | }, |
1447 | "core-util-is": { | 1442 | "core-util-is": { |
1448 | "version": "1.0.2", | 1443 | "version": "1.0.2", |
@@ -1559,8 +1554,7 @@ | @@ -1559,8 +1554,7 @@ | ||
1559 | "inherits": { | 1554 | "inherits": { |
1560 | "version": "2.0.3", | 1555 | "version": "2.0.3", |
1561 | "bundled": true, | 1556 | "bundled": true, |
1562 | - "dev": true, | ||
1563 | - "optional": true | 1557 | + "dev": true |
1564 | }, | 1558 | }, |
1565 | "ini": { | 1559 | "ini": { |
1566 | "version": "1.3.5", | 1560 | "version": "1.3.5", |
@@ -1572,7 +1566,6 @@ | @@ -1572,7 +1566,6 @@ | ||
1572 | "version": "1.0.0", | 1566 | "version": "1.0.0", |
1573 | "bundled": true, | 1567 | "bundled": true, |
1574 | "dev": true, | 1568 | "dev": true, |
1575 | - "optional": true, | ||
1576 | "requires": { | 1569 | "requires": { |
1577 | "number-is-nan": "^1.0.0" | 1570 | "number-is-nan": "^1.0.0" |
1578 | } | 1571 | } |
@@ -1587,7 +1580,6 @@ | @@ -1587,7 +1580,6 @@ | ||
1587 | "version": "3.0.4", | 1580 | "version": "3.0.4", |
1588 | "bundled": true, | 1581 | "bundled": true, |
1589 | "dev": true, | 1582 | "dev": true, |
1590 | - "optional": true, | ||
1591 | "requires": { | 1583 | "requires": { |
1592 | "brace-expansion": "^1.1.7" | 1584 | "brace-expansion": "^1.1.7" |
1593 | } | 1585 | } |
@@ -1699,8 +1691,7 @@ | @@ -1699,8 +1691,7 @@ | ||
1699 | "number-is-nan": { | 1691 | "number-is-nan": { |
1700 | "version": "1.0.1", | 1692 | "version": "1.0.1", |
1701 | "bundled": true, | 1693 | "bundled": true, |
1702 | - "dev": true, | ||
1703 | - "optional": true | 1694 | + "dev": true |
1704 | }, | 1695 | }, |
1705 | "object-assign": { | 1696 | "object-assign": { |
1706 | "version": "4.1.1", | 1697 | "version": "4.1.1", |
@@ -1712,7 +1703,6 @@ | @@ -1712,7 +1703,6 @@ | ||
1712 | "version": "1.4.0", | 1703 | "version": "1.4.0", |
1713 | "bundled": true, | 1704 | "bundled": true, |
1714 | "dev": true, | 1705 | "dev": true, |
1715 | - "optional": true, | ||
1716 | "requires": { | 1706 | "requires": { |
1717 | "wrappy": "1" | 1707 | "wrappy": "1" |
1718 | } | 1708 | } |
@@ -1834,7 +1824,6 @@ | @@ -1834,7 +1824,6 @@ | ||
1834 | "version": "1.0.2", | 1824 | "version": "1.0.2", |
1835 | "bundled": true, | 1825 | "bundled": true, |
1836 | "dev": true, | 1826 | "dev": true, |
1837 | - "optional": true, | ||
1838 | "requires": { | 1827 | "requires": { |
1839 | "code-point-at": "^1.0.0", | 1828 | "code-point-at": "^1.0.0", |
1840 | "is-fullwidth-code-point": "^1.0.0", | 1829 | "is-fullwidth-code-point": "^1.0.0", |
@@ -186,7 +186,9 @@ export class AliasController implements IAliasController { | @@ -186,7 +186,9 @@ export class AliasController implements IAliasController { | ||
186 | ); | 186 | ); |
187 | } else { | 187 | } else { |
188 | resolvedAliasSubject.error(null); | 188 | resolvedAliasSubject.error(null); |
189 | + const res = this.resolvedAliasesObservable[aliasId]; | ||
189 | delete this.resolvedAliasesObservable[aliasId]; | 190 | delete this.resolvedAliasesObservable[aliasId]; |
191 | + return res; | ||
190 | } | 192 | } |
191 | return this.resolvedAliasesObservable[aliasId]; | 193 | return this.resolvedAliasesObservable[aliasId]; |
192 | } | 194 | } |
@@ -24,6 +24,8 @@ import * as equal from 'deep-equal'; | @@ -24,6 +24,8 @@ import * as equal from 'deep-equal'; | ||
24 | import { UtilsService } from '@core/services/utils.service'; | 24 | import { UtilsService } from '@core/services/utils.service'; |
25 | import { Observable, of, throwError } from 'rxjs'; | 25 | import { Observable, of, throwError } from 'rxjs'; |
26 | import { map } from 'rxjs/operators'; | 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 | const WIDGET_ITEM = 'widget_item'; | 30 | const WIDGET_ITEM = 'widget_item'; |
29 | const WIDGET_REFERENCE = 'widget_reference'; | 31 | const WIDGET_REFERENCE = 'widget_reference'; |
@@ -45,6 +47,21 @@ export interface WidgetReference { | @@ -45,6 +47,21 @@ export interface WidgetReference { | ||
45 | originalColumns: number; | 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 | @Injectable({ | 65 | @Injectable({ |
49 | providedIn: 'root' | 66 | providedIn: 'root' |
50 | }) | 67 | }) |
@@ -54,6 +71,7 @@ export class ItemBufferService { | @@ -54,6 +71,7 @@ export class ItemBufferService { | ||
54 | private delimiter = '.'; | 71 | private delimiter = '.'; |
55 | 72 | ||
56 | constructor(private dashboardUtils: DashboardUtilsService, | 73 | constructor(private dashboardUtils: DashboardUtilsService, |
74 | + private ruleChainService: RuleChainService, | ||
57 | private utils: UtilsService) {} | 75 | private utils: UtilsService) {} |
58 | 76 | ||
59 | public prepareWidgetItem(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): WidgetItem { | 77 | public prepareWidgetItem(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): WidgetItem { |
@@ -99,12 +117,12 @@ export class ItemBufferService { | @@ -99,12 +117,12 @@ export class ItemBufferService { | ||
99 | 117 | ||
100 | public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void { | 118 | public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void { |
101 | const widgetItem = this.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget); | 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 | public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void { | 123 | public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void { |
106 | const widgetReference = this.prepareWidgetReference(dashboard, sourceState, sourceLayout, widget); | 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 | public hasWidget(): boolean { | 128 | public hasWidget(): boolean { |
@@ -112,9 +130,8 @@ export class ItemBufferService { | @@ -112,9 +130,8 @@ export class ItemBufferService { | ||
112 | } | 130 | } |
113 | 131 | ||
114 | public canPasteWidgetReference(dashboard: Dashboard, state: string, layout: DashboardLayoutId): boolean { | 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 | if (widgetReference.dashboardId === dashboard.id.id) { | 135 | if (widgetReference.dashboardId === dashboard.id.id) { |
119 | if ((widgetReference.sourceState !== state || widgetReference.sourceLayout !== layout) | 136 | if ((widgetReference.sourceState !== state || widgetReference.sourceLayout !== layout) |
120 | && dashboard.configuration.widgets[widgetReference.widgetId]) { | 137 | && dashboard.configuration.widgets[widgetReference.widgetId]) { |
@@ -128,9 +145,8 @@ export class ItemBufferService { | @@ -128,9 +145,8 @@ export class ItemBufferService { | ||
128 | public pasteWidget(targetDashboard: Dashboard, targetState: string, | 145 | public pasteWidget(targetDashboard: Dashboard, targetState: string, |
129 | targetLayout: DashboardLayoutId, position: WidgetPosition, | 146 | targetLayout: DashboardLayoutId, position: WidgetPosition, |
130 | onAliasesUpdateFunction: () => void): Observable<Widget> { | 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 | const widget = widgetItem.widget; | 150 | const widget = widgetItem.widget; |
135 | const aliasesInfo = widgetItem.aliasesInfo; | 151 | const aliasesInfo = widgetItem.aliasesInfo; |
136 | const originalColumns = widgetItem.originalColumns; | 152 | const originalColumns = widgetItem.originalColumns; |
@@ -155,9 +171,8 @@ export class ItemBufferService { | @@ -155,9 +171,8 @@ export class ItemBufferService { | ||
155 | 171 | ||
156 | public pasteWidgetReference(targetDashboard: Dashboard, targetState: string, | 172 | public pasteWidgetReference(targetDashboard: Dashboard, targetState: string, |
157 | targetLayout: DashboardLayoutId, position: WidgetPosition): Observable<Widget> { | 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 | const widget = targetDashboard.configuration.widgets[widgetReference.widgetId]; | 176 | const widget = targetDashboard.configuration.widgets[widgetReference.widgetId]; |
162 | if (widget) { | 177 | if (widget) { |
163 | const originalColumns = widgetReference.originalColumns; | 178 | const originalColumns = widgetReference.originalColumns; |
@@ -216,6 +231,89 @@ export class ItemBufferService { | @@ -216,6 +231,89 @@ export class ItemBufferService { | ||
216 | return of(theDashboard); | 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 | private getOriginalColumns(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId): number { | 317 | private getOriginalColumns(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId): number { |
220 | let originalColumns = 24; | 318 | let originalColumns = 24; |
221 | let gridSettings = null; | 319 | let gridSettings = null; |
@@ -737,18 +737,22 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -737,18 +737,22 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
737 | dataLoading: (subscription) => { | 737 | dataLoading: (subscription) => { |
738 | if (this.loadingData !== subscription.loadingData) { | 738 | if (this.loadingData !== subscription.loadingData) { |
739 | this.loadingData = subscription.loadingData; | 739 | this.loadingData = subscription.loadingData; |
740 | - this.cd.detectChanges(); | 740 | + if (!this.destroyed) { |
741 | + this.cd.detectChanges(); | ||
742 | + } | ||
741 | } | 743 | } |
742 | }, | 744 | }, |
743 | legendDataUpdated: (subscription, detectChanges) => { | 745 | legendDataUpdated: (subscription, detectChanges) => { |
744 | - if (detectChanges) { | 746 | + if (detectChanges && !this.destroyed) { |
745 | this.cd.detectChanges(); | 747 | this.cd.detectChanges(); |
746 | } | 748 | } |
747 | }, | 749 | }, |
748 | timeWindowUpdated: (subscription, timeWindowConfig) => { | 750 | timeWindowUpdated: (subscription, timeWindowConfig) => { |
749 | this.ngZone.run(() => { | 751 | this.ngZone.run(() => { |
750 | this.widget.config.timewindow = timeWindowConfig; | 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,6 +17,7 @@ | ||
17 | --> | 17 | --> |
18 | <div class="tb-dashboard-page mat-content" style="padding-top: 150px;" | 18 | <div class="tb-dashboard-page mat-content" style="padding-top: 150px;" |
19 | fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen"> | 19 | fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen"> |
20 | + <tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet> | ||
20 | <section class="tb-dashboard-toolbar" | 21 | <section class="tb-dashboard-toolbar" |
21 | [ngClass]="{ 'tb-dashboard-toolbar-opened': toolbarOpened, | 22 | [ngClass]="{ 'tb-dashboard-toolbar-opened': toolbarOpened, |
22 | 'tb-dashboard-toolbar-closed': !toolbarOpened }"> | 23 | 'tb-dashboard-toolbar-closed': !toolbarOpened }"> |
@@ -146,6 +147,7 @@ | @@ -146,6 +147,7 @@ | ||
146 | [mode]="isMobile ? 'over' : 'side'" | 147 | [mode]="isMobile ? 'over' : 'side'" |
147 | [(opened)]="rightLayoutOpened"> | 148 | [(opened)]="rightLayoutOpened"> |
148 | <tb-dashboard-layout style="height: 100%;" | 149 | <tb-dashboard-layout style="height: 100%;" |
150 | + [dashboardCheatSheet]="cheatSheetComponent" | ||
149 | [layoutCtx]="layouts.right.layoutCtx" | 151 | [layoutCtx]="layouts.right.layoutCtx" |
150 | [dashboardCtx]="dashboardCtx" | 152 | [dashboardCtx]="dashboardCtx" |
151 | [isEdit]="isEdit" | 153 | [isEdit]="isEdit" |
@@ -159,6 +161,7 @@ | @@ -159,6 +161,7 @@ | ||
159 | [ngStyle]="{width: mainLayoutWidth(), | 161 | [ngStyle]="{width: mainLayoutWidth(), |
160 | height: mainLayoutHeight()}"> | 162 | height: mainLayoutHeight()}"> |
161 | <tb-dashboard-layout | 163 | <tb-dashboard-layout |
164 | + [dashboardCheatSheet]="cheatSheetComponent" | ||
162 | [layoutCtx]="layouts.main.layoutCtx" | 165 | [layoutCtx]="layouts.main.layoutCtx" |
163 | [dashboardCtx]="dashboardCtx" | 166 | [dashboardCtx]="dashboardCtx" |
164 | [isEdit]="isEdit" | 167 | [isEdit]="isEdit" |
@@ -15,13 +15,14 @@ | @@ -15,13 +15,14 @@ | ||
15 | limitations under the License. | 15 | limitations under the License. |
16 | 16 | ||
17 | --> | 17 | --> |
18 | -<hotkeys-cheatsheet></hotkeys-cheatsheet> | ||
19 | <div fxLayout="column" class="tb-progress-cover" fxLayoutAlign="center center" | 18 | <div fxLayout="column" class="tb-progress-cover" fxLayoutAlign="center center" |
20 | *ngIf="layoutCtx.widgets.isLoading()"> | 19 | *ngIf="layoutCtx.widgets.isLoading()"> |
21 | <mat-spinner color="warn" mode="indeterminate" diameter="100"> | 20 | <mat-spinner color="warn" mode="indeterminate" diameter="100"> |
22 | </mat-spinner> | 21 | </mat-spinner> |
23 | </div> | 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 | [style.backgroundImage]="backgroundImage" | 26 | [style.backgroundImage]="backgroundImage" |
26 | [ngStyle]="dashboardStyle"> | 27 | [ngStyle]="dashboardStyle"> |
27 | <section *ngIf="layoutCtx.widgets.isEmpty()" fxLayoutAlign="center center" | 28 | <section *ngIf="layoutCtx.widgets.isEmpty()" fxLayoutAlign="center center" |
@@ -14,27 +14,25 @@ | @@ -14,27 +14,25 @@ | ||
14 | /// limitations under the License. | 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 | import { ILayoutController } from '@home/pages/dashboard/layout/layout.models'; | 18 | import { ILayoutController } from '@home/pages/dashboard/layout/layout.models'; |
20 | import { DashboardContext, DashboardPageLayoutContext } from '@home/pages/dashboard/dashboard-page.models'; | 19 | import { DashboardContext, DashboardPageLayoutContext } from '@home/pages/dashboard/dashboard-page.models'; |
21 | import { PageComponent } from '@shared/components/page.component'; | 20 | import { PageComponent } from '@shared/components/page.component'; |
22 | import { Store } from '@ngrx/store'; | 21 | import { Store } from '@ngrx/store'; |
23 | import { AppState } from '@core/core.state'; | 22 | import { AppState } from '@core/core.state'; |
24 | import { Widget } from '@shared/models/widget.models'; | 23 | import { Widget } from '@shared/models/widget.models'; |
25 | -import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models'; | ||
26 | -import { GridsterComponent } from 'angular-gridster2'; | ||
27 | import { | 24 | import { |
28 | DashboardCallbacks, | 25 | DashboardCallbacks, |
29 | DashboardContextMenuItem, | 26 | DashboardContextMenuItem, |
30 | - IDashboardComponent, WidgetContextMenuItem | 27 | + IDashboardComponent, |
28 | + WidgetContextMenuItem | ||
31 | } from '@home/models/dashboard-component.models'; | 29 | } from '@home/models/dashboard-component.models'; |
32 | -import { Observable, of, Subscription } from 'rxjs'; | 30 | +import { Subscription } from 'rxjs'; |
33 | import { Hotkey, HotkeysService } from 'angular2-hotkeys'; | 31 | import { Hotkey, HotkeysService } from 'angular2-hotkeys'; |
34 | -import { getCurrentIsLoading } from '@core/interceptors/load.selectors'; | ||
35 | import { TranslateService } from '@ngx-translate/core'; | 32 | import { TranslateService } from '@ngx-translate/core'; |
36 | import { ItemBufferService } from '@app/core/services/item-buffer.service'; | 33 | import { ItemBufferService } from '@app/core/services/item-buffer.service'; |
37 | import { DomSanitizer, SafeStyle } from '@angular/platform-browser'; | 34 | import { DomSanitizer, SafeStyle } from '@angular/platform-browser'; |
35 | +import { TbCheatSheetComponent } from '@shared/components/cheatsheet.component'; | ||
38 | 36 | ||
39 | @Component({ | 37 | @Component({ |
40 | selector: 'tb-dashboard-layout', | 38 | selector: 'tb-dashboard-layout', |
@@ -47,6 +45,10 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | @@ -47,6 +45,10 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | ||
47 | dashboardStyle: {[klass: string]: any} = null; | 45 | dashboardStyle: {[klass: string]: any} = null; |
48 | backgroundImage: SafeStyle | string; | 46 | backgroundImage: SafeStyle | string; |
49 | 47 | ||
48 | + hotKeys: Hotkey[] = []; | ||
49 | + | ||
50 | + @Input() dashboardCheatSheet: TbCheatSheetComponent; | ||
51 | + | ||
50 | @Input() | 52 | @Input() |
51 | set layoutCtx(val: DashboardPageLayoutContext) { | 53 | set layoutCtx(val: DashboardPageLayoutContext) { |
52 | this.layoutCtxValue = val; | 54 | this.layoutCtxValue = val; |
@@ -81,11 +83,11 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | @@ -81,11 +83,11 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | ||
81 | private rxSubscriptions = new Array<Subscription>(); | 83 | private rxSubscriptions = new Array<Subscription>(); |
82 | 84 | ||
83 | constructor(protected store: Store<AppState>, | 85 | constructor(protected store: Store<AppState>, |
84 | - private hotkeysService: HotkeysService, | ||
85 | private translate: TranslateService, | 86 | private translate: TranslateService, |
86 | private itembuffer: ItemBufferService, | 87 | private itembuffer: ItemBufferService, |
87 | private sanitizer: DomSanitizer) { | 88 | private sanitizer: DomSanitizer) { |
88 | super(store); | 89 | super(store); |
90 | + this.initHotKeys(); | ||
89 | } | 91 | } |
90 | 92 | ||
91 | ngOnInit(): void { | 93 | ngOnInit(): void { |
@@ -95,7 +97,6 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | @@ -95,7 +97,6 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | ||
95 | this.dashboardCtx.runChangeDetection(); | 97 | this.dashboardCtx.runChangeDetection(); |
96 | }) | 98 | }) |
97 | ); | 99 | ); |
98 | - this.initHotKeys(); | ||
99 | } | 100 | } |
100 | 101 | ||
101 | ngOnDestroy(): void { | 102 | ngOnDestroy(): void { |
@@ -106,7 +107,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | @@ -106,7 +107,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | ||
106 | } | 107 | } |
107 | 108 | ||
108 | private initHotKeys(): void { | 109 | private initHotKeys(): void { |
109 | - this.hotkeysService.add( | 110 | + this.hotKeys.push( |
110 | new Hotkey('ctrl+c', (event: KeyboardEvent) => { | 111 | new Hotkey('ctrl+c', (event: KeyboardEvent) => { |
111 | if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { | 112 | if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { |
112 | const widget = this.dashboard.getSelectedWidget(); | 113 | const widget = this.dashboard.getSelectedWidget(); |
@@ -119,7 +120,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | @@ -119,7 +120,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | ||
119 | }, null, | 120 | }, null, |
120 | this.translate.instant('action.copy')) | 121 | this.translate.instant('action.copy')) |
121 | ); | 122 | ); |
122 | - this.hotkeysService.add( | 123 | + this.hotKeys.push( |
123 | new Hotkey('ctrl+r', (event: KeyboardEvent) => { | 124 | new Hotkey('ctrl+r', (event: KeyboardEvent) => { |
124 | if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { | 125 | if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { |
125 | const widget = this.dashboard.getSelectedWidget(); | 126 | const widget = this.dashboard.getSelectedWidget(); |
@@ -132,7 +133,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | @@ -132,7 +133,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | ||
132 | }, null, | 133 | }, null, |
133 | this.translate.instant('action.copy-reference')) | 134 | this.translate.instant('action.copy-reference')) |
134 | ); | 135 | ); |
135 | - this.hotkeysService.add( | 136 | + this.hotKeys.push( |
136 | new Hotkey('ctrl+v', (event: KeyboardEvent) => { | 137 | new Hotkey('ctrl+v', (event: KeyboardEvent) => { |
137 | if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { | 138 | if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { |
138 | if (this.itembuffer.hasWidget()) { | 139 | if (this.itembuffer.hasWidget()) { |
@@ -144,7 +145,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | @@ -144,7 +145,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | ||
144 | }, null, | 145 | }, null, |
145 | this.translate.instant('action.paste')) | 146 | this.translate.instant('action.paste')) |
146 | ); | 147 | ); |
147 | - this.hotkeysService.add( | 148 | + this.hotKeys.push( |
148 | new Hotkey('ctrl+i', (event: KeyboardEvent) => { | 149 | new Hotkey('ctrl+i', (event: KeyboardEvent) => { |
149 | if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { | 150 | if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { |
150 | if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.getDashboard(), | 151 | if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.getDashboard(), |
@@ -157,7 +158,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | @@ -157,7 +158,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | ||
157 | }, null, | 158 | }, null, |
158 | this.translate.instant('action.paste-reference')) | 159 | this.translate.instant('action.paste-reference')) |
159 | ); | 160 | ); |
160 | - this.hotkeysService.add( | 161 | + this.hotKeys.push( |
161 | new Hotkey('ctrl+x', (event: KeyboardEvent) => { | 162 | new Hotkey('ctrl+x', (event: KeyboardEvent) => { |
162 | if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { | 163 | if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { |
163 | const widget = this.dashboard.getSelectedWidget(); | 164 | const widget = this.dashboard.getSelectedWidget(); |
@@ -14,31 +14,14 @@ | @@ -14,31 +14,14 @@ | ||
14 | /// limitations under the License. | 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 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; | 21 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
38 | import { deepClone } from '@core/utils'; | 22 | import { deepClone } from '@core/utils'; |
39 | -import { EntityAlias } from '@shared/models/alias.models'; | ||
40 | import { TruncatePipe } from '@shared/pipe/truncate.pipe'; | 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 | import { TranslateService } from '@ngx-translate/core'; | 25 | import { TranslateService } from '@ngx-translate/core'; |
43 | import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; | 26 | import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; |
44 | import { map, mergeMap, share, startWith } from 'rxjs/operators'; | 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,12 +19,10 @@ import { PageComponent } from '@shared/components/page.component'; | ||
19 | import { Store } from '@ngrx/store'; | 19 | import { Store } from '@ngrx/store'; |
20 | import { AppState } from '@core/core.state'; | 20 | import { AppState } from '@core/core.state'; |
21 | import { FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms'; | 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 | import { EntityType } from '@shared/models/entity-type.models'; | 23 | import { EntityType } from '@shared/models/entity-type.models'; |
25 | import { Subscription } from 'rxjs'; | 24 | import { Subscription } from 'rxjs'; |
26 | import { RuleChainService } from '@core/http/rule-chain.service'; | 25 | import { RuleChainService } from '@core/http/rule-chain.service'; |
27 | -import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component'; | ||
28 | import { RuleNodeConfigComponent } from './rule-node-config.component'; | 26 | import { RuleNodeConfigComponent } from './rule-node-config.component'; |
29 | 27 | ||
30 | @Component({ | 28 | @Component({ |
@@ -14,34 +14,12 @@ | @@ -14,34 +14,12 @@ | ||
14 | /// limitations under the License. | 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 | import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, NgForm, Validators } from '@angular/forms'; | 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 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; | 20 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
38 | -import { deepClone } from '@core/utils'; | ||
39 | -import { EntityAlias } from '@shared/models/alias.models'; | ||
40 | import { TruncatePipe } from '@shared/pipe/truncate.pipe'; | 21 | import { TruncatePipe } from '@shared/pipe/truncate.pipe'; |
41 | -import { MatChipList, MatAutocomplete, MatChipInputEvent, MatAutocompleteSelectedEvent } from '@angular/material'; | ||
42 | import { TranslateService } from '@ngx-translate/core'; | 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 | @Component({ | 24 | @Component({ |
47 | selector: 'tb-rule-node-link', | 25 | selector: 'tb-rule-node-link', |
@@ -15,8 +15,10 @@ | @@ -15,8 +15,10 @@ | ||
15 | limitations under the License. | 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 | fxLayout="column" class="tb-rulechain"> | 20 | fxLayout="column" class="tb-rulechain"> |
21 | + <tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet> | ||
20 | <section class="tb-rulechain-container" fxFlex fxLayout="column"> | 22 | <section class="tb-rulechain-container" fxFlex fxLayout="column"> |
21 | <div class="tb-rulechain-layout" fxFlex fxLayout="row"> | 23 | <div class="tb-rulechain-layout" fxFlex fxLayout="row"> |
22 | <section fxLayout="row" | 24 | <section fxLayout="row" |
@@ -80,6 +82,7 @@ | @@ -80,6 +82,7 @@ | ||
80 | [model]="ruleNodeTypesModel[ruleNodeType].model" | 82 | [model]="ruleNodeTypesModel[ruleNodeType].model" |
81 | [selectedObjects]="ruleNodeTypesModel[ruleNodeType].selectedObjects" | 83 | [selectedObjects]="ruleNodeTypesModel[ruleNodeType].selectedObjects" |
82 | [automaticResize]="false" | 84 | [automaticResize]="false" |
85 | + fitModelSizeByDefault | ||
83 | [userCallbacks]="nodeLibCallbacks" | 86 | [userCallbacks]="nodeLibCallbacks" |
84 | [nodeWidth]="170" | 87 | [nodeWidth]="170" |
85 | [nodeHeight]="50" | 88 | [nodeHeight]="50" |
@@ -162,7 +165,38 @@ | @@ -162,7 +165,38 @@ | ||
162 | matTooltipPosition="above"> | 165 | matTooltipPosition="above"> |
163 | <mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> | 166 | <mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> |
164 | </button> | 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 | <fc-canvas #ruleChainCanvas | 200 | <fc-canvas #ruleChainCanvas |
167 | id="tb-rulchain-canvas" | 201 | id="tb-rulchain-canvas" |
168 | [model]="ruleChainModel" | 202 | [model]="ruleChainModel" |
@@ -170,6 +204,7 @@ | @@ -170,6 +204,7 @@ | ||
170 | [selectedObjects]="selectedObjects" | 204 | [selectedObjects]="selectedObjects" |
171 | [edgeStyle]="flowchartConstants.curvedStyle" | 205 | [edgeStyle]="flowchartConstants.curvedStyle" |
172 | [automaticResize]="true" | 206 | [automaticResize]="true" |
207 | + fitModelSizeByDefault="false" | ||
173 | [nodeWidth]="170" | 208 | [nodeWidth]="170" |
174 | [nodeHeight]="50" | 209 | [nodeHeight]="50" |
175 | [dragAnimation]="flowchartConstants.dragAnimationRepaint" | 210 | [dragAnimation]="flowchartConstants.dragAnimationRepaint" |
@@ -14,6 +14,8 @@ | @@ -14,6 +14,8 @@ | ||
14 | * limitations under the License. | 14 | * limitations under the License. |
15 | */ | 15 | */ |
16 | 16 | ||
17 | +@import './rule-node-colors'; | ||
18 | + | ||
17 | .tb-rulechain { | 19 | .tb-rulechain { |
18 | width: 100%; | 20 | width: 100%; |
19 | height: 100%; | 21 | height: 100%; |
@@ -267,3 +269,60 @@ | @@ -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,9 +18,11 @@ import { | ||
18 | AfterViewInit, | 18 | AfterViewInit, |
19 | Component, | 19 | Component, |
20 | ElementRef, | 20 | ElementRef, |
21 | - HostBinding, Inject, | 21 | + HostBinding, |
22 | + Inject, | ||
22 | OnInit, | 23 | OnInit, |
23 | - QueryList, SkipSelf, | 24 | + QueryList, |
25 | + SkipSelf, | ||
24 | ViewChild, | 26 | ViewChild, |
25 | ViewChildren, | 27 | ViewChildren, |
26 | ViewEncapsulation | 28 | ViewEncapsulation |
@@ -31,7 +33,7 @@ import { AppState } from '@core/core.state'; | @@ -31,7 +33,7 @@ import { AppState } from '@core/core.state'; | ||
31 | import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; | 33 | import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; |
32 | import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard'; | 34 | import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard'; |
33 | import { TranslateService } from '@ngx-translate/core'; | 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 | import { DialogService } from '@core/services/dialog.service'; | 37 | import { DialogService } from '@core/services/dialog.service'; |
36 | import { AuthService } from '@core/auth/auth.service'; | 38 | import { AuthService } from '@core/auth/auth.service'; |
37 | import { ActivatedRoute, Router } from '@angular/router'; | 39 | import { ActivatedRoute, Router } from '@angular/router'; |
@@ -41,8 +43,11 @@ import { | @@ -41,8 +43,11 @@ import { | ||
41 | RuleChain, | 43 | RuleChain, |
42 | ruleChainNodeComponent | 44 | ruleChainNodeComponent |
43 | } from '@shared/models/rule-chain.models'; | 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 | import { | 47 | import { |
48 | + FcRuleEdge, | ||
49 | + FcRuleNode, | ||
50 | + FcRuleNodeType, | ||
46 | getRuleNodeHelpLink, | 51 | getRuleNodeHelpLink, |
47 | LinkLabel, | 52 | LinkLabel, |
48 | RuleNodeComponentDescriptor, | 53 | RuleNodeComponentDescriptor, |
@@ -50,24 +55,19 @@ import { | @@ -50,24 +55,19 @@ import { | ||
50 | ruleNodeTypeDescriptors, | 55 | ruleNodeTypeDescriptors, |
51 | ruleNodeTypesLibrary | 56 | ruleNodeTypesLibrary |
52 | } from '@shared/models/rule-node.models'; | 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 | import { RuleChainService } from '@core/http/rule-chain.service'; | 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 | import { ISearchableComponent } from '../../models/searchable-component.models'; | 62 | import { ISearchableComponent } from '../../models/searchable-component.models'; |
58 | -import { deepClone, isDefined, isString } from '@core/utils'; | 63 | +import { deepClone } from '@core/utils'; |
59 | import { RuleNodeDetailsComponent } from '@home/pages/rulechain/rule-node-details.component'; | 64 | import { RuleNodeDetailsComponent } from '@home/pages/rulechain/rule-node-details.component'; |
60 | import { RuleNodeLinkComponent } from './rule-node-link.component'; | 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 | import { DialogComponent } from '@shared/components/dialog.component'; | 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 | @Component({ | 72 | @Component({ |
73 | selector: 'tb-rulechain-page', | 73 | selector: 'tb-rulechain-page', |
@@ -92,6 +92,12 @@ export class RuleChainPageComponent extends PageComponent | @@ -92,6 +92,12 @@ export class RuleChainPageComponent extends PageComponent | ||
92 | @ViewChildren('ruleNodeTypeExpansionPanels', | 92 | @ViewChildren('ruleNodeTypeExpansionPanels', |
93 | {read: MatExpansionPanel}) expansionPanels: QueryList<MatExpansionPanel>; | 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 | ruleNodeTypeDescriptorsMap = ruleNodeTypeDescriptors; | 101 | ruleNodeTypeDescriptorsMap = ruleNodeTypeDescriptors; |
96 | ruleNodeTypesLibraryArray = ruleNodeTypesLibrary; | 102 | ruleNodeTypesLibraryArray = ruleNodeTypesLibrary; |
97 | 103 | ||
@@ -116,6 +122,9 @@ export class RuleChainPageComponent extends PageComponent | @@ -116,6 +122,9 @@ export class RuleChainPageComponent extends PageComponent | ||
116 | isEditingRuleNodeLink = false; | 122 | isEditingRuleNodeLink = false; |
117 | editingRuleNodeLinkIndex = -1; | 123 | editingRuleNodeLinkIndex = -1; |
118 | 124 | ||
125 | + hotKeys: Hotkey[] = []; | ||
126 | + | ||
127 | + enableHotKeys = true; | ||
119 | isLibraryOpen = true; | 128 | isLibraryOpen = true; |
120 | 129 | ||
121 | ruleNodeSearch = ''; | 130 | ruleNodeSearch = ''; |
@@ -173,7 +182,11 @@ export class RuleChainPageComponent extends PageComponent | @@ -173,7 +182,11 @@ export class RuleChainPageComponent extends PageComponent | ||
173 | } else { | 182 | } else { |
174 | const labels = this.ruleChainService.getRuleNodeSupportedLinks(sourceNode.component); | 183 | const labels = this.ruleChainService.getRuleNodeSupportedLinks(sourceNode.component); |
175 | const allowCustomLabels = this.ruleChainService.ruleNodeAllowCustomLinks(sourceNode.component); | 184 | const allowCustomLabels = this.ruleChainService.ruleNodeAllowCustomLinks(sourceNode.component); |
185 | + this.enableHotKeys = false; | ||
176 | return this.addRuleNodeLink(edge, labels, allowCustomLabels).pipe( | 186 | return this.addRuleNodeLink(edge, labels, allowCustomLabels).pipe( |
187 | + tap(() => { | ||
188 | + this.enableHotKeys = true; | ||
189 | + }), | ||
177 | mergeMap((res) => { | 190 | mergeMap((res) => { |
178 | if (res) { | 191 | if (res) { |
179 | return of(res); | 192 | return of(res); |
@@ -216,6 +229,7 @@ export class RuleChainPageComponent extends PageComponent | @@ -216,6 +229,7 @@ export class RuleChainPageComponent extends PageComponent | ||
216 | private ruleChainService: RuleChainService, | 229 | private ruleChainService: RuleChainService, |
217 | private authService: AuthService, | 230 | private authService: AuthService, |
218 | private translate: TranslateService, | 231 | private translate: TranslateService, |
232 | + private itembuffer: ItemBufferService, | ||
219 | public dialog: MatDialog, | 233 | public dialog: MatDialog, |
220 | public dialogService: DialogService, | 234 | public dialogService: DialogService, |
221 | public fb: FormBuilder) { | 235 | public fb: FormBuilder) { |
@@ -236,6 +250,7 @@ export class RuleChainPageComponent extends PageComponent | @@ -236,6 +250,7 @@ export class RuleChainPageComponent extends PageComponent | ||
236 | }) | 250 | }) |
237 | ) | 251 | ) |
238 | .subscribe(); | 252 | .subscribe(); |
253 | + this.ruleChainCanvas.adjustCanvasSize(true); | ||
239 | } | 254 | } |
240 | 255 | ||
241 | onSearchTextUpdated(searchText: string) { | 256 | onSearchTextUpdated(searchText: string) { |
@@ -244,6 +259,7 @@ export class RuleChainPageComponent extends PageComponent | @@ -244,6 +259,7 @@ export class RuleChainPageComponent extends PageComponent | ||
244 | } | 259 | } |
245 | 260 | ||
246 | private init() { | 261 | private init() { |
262 | + this.initHotKeys(); | ||
247 | this.ruleChain = this.route.snapshot.data.ruleChain; | 263 | this.ruleChain = this.route.snapshot.data.ruleChain; |
248 | if (this.route.snapshot.data.import && !this.ruleChain) { | 264 | if (this.route.snapshot.data.import && !this.ruleChain) { |
249 | this.router.navigateByUrl('ruleChains'); | 265 | this.router.navigateByUrl('ruleChains'); |
@@ -268,6 +284,89 @@ export class RuleChainPageComponent extends PageComponent | @@ -268,6 +284,89 @@ export class RuleChainPageComponent extends PageComponent | ||
268 | this.createRuleChainModel(); | 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 | updateRuleChainLibrary() { | 370 | updateRuleChainLibrary() { |
272 | const search = this.ruleNodeTypeSearch.toUpperCase(); | 371 | const search = this.ruleNodeTypeSearch.toUpperCase(); |
273 | const res = this.ruleNodeComponents.filter( | 372 | const res = this.ruleNodeComponents.filter( |
@@ -510,11 +609,229 @@ export class RuleChainPageComponent extends PageComponent | @@ -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 | this.isDirtyValue = false; | 615 | this.isDirtyValue = false; |
514 | this.updateRuleNodesHighlight(); | 616 | this.updateRuleNodesHighlight(); |
515 | this.validate(); | 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 | onModelChanged() { | 835 | onModelChanged() { |
519 | console.log('Model changed!'); | 836 | console.log('Model changed!'); |
520 | this.isDirtyValue = true; | 837 | this.isDirtyValue = true; |
@@ -531,6 +848,8 @@ export class RuleChainPageComponent extends PageComponent | @@ -531,6 +848,8 @@ export class RuleChainPageComponent extends PageComponent | ||
531 | 848 | ||
532 | openNodeDetails(node: FcRuleNode) { | 849 | openNodeDetails(node: FcRuleNode) { |
533 | if (node.component.type !== RuleNodeType.INPUT) { | 850 | if (node.component.type !== RuleNodeType.INPUT) { |
851 | + this.enableHotKeys = false; | ||
852 | + this.updateErrorTooltips(true); | ||
534 | this.isEditingRuleNodeLink = false; | 853 | this.isEditingRuleNodeLink = false; |
535 | this.editingRuleNodeLink = null; | 854 | this.editingRuleNodeLink = null; |
536 | this.isEditingRuleNode = true; | 855 | this.isEditingRuleNode = true; |
@@ -545,6 +864,8 @@ export class RuleChainPageComponent extends PageComponent | @@ -545,6 +864,8 @@ export class RuleChainPageComponent extends PageComponent | ||
545 | openLinkDetails(edge: FcRuleEdge) { | 864 | openLinkDetails(edge: FcRuleEdge) { |
546 | const sourceNode: FcRuleNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source) as FcRuleNode; | 865 | const sourceNode: FcRuleNode = this.ruleChainCanvas.modelService.nodes.getNodeByConnectorId(edge.source) as FcRuleNode; |
547 | if (sourceNode.component.type !== RuleNodeType.INPUT) { | 866 | if (sourceNode.component.type !== RuleNodeType.INPUT) { |
867 | + this.enableHotKeys = false; | ||
868 | + this.updateErrorTooltips(true); | ||
548 | this.isEditingRuleNode = false; | 869 | this.isEditingRuleNode = false; |
549 | this.editingRuleNode = null; | 870 | this.editingRuleNode = null; |
550 | this.editingRuleNodeLinkLabels = this.ruleChainService.getRuleNodeSupportedLinks(sourceNode.component); | 871 | this.editingRuleNodeLinkLabels = this.ruleChainService.getRuleNodeSupportedLinks(sourceNode.component); |
@@ -558,9 +879,121 @@ export class RuleChainPageComponent extends PageComponent | @@ -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 | onDetailsDrawerClosed() { | 992 | onDetailsDrawerClosed() { |
562 | this.onEditRuleNodeClosed(); | 993 | this.onEditRuleNodeClosed(); |
563 | this.onEditRuleNodeLinkClosed(); | 994 | this.onEditRuleNodeLinkClosed(); |
995 | + this.enableHotKeys = true; | ||
996 | + this.updateErrorTooltips(false); | ||
564 | } | 997 | } |
565 | 998 | ||
566 | onEditRuleNodeClosed() { | 999 | onEditRuleNodeClosed() { |
@@ -739,6 +1172,7 @@ export class RuleChainPageComponent extends PageComponent | @@ -739,6 +1172,7 @@ export class RuleChainPageComponent extends PageComponent | ||
739 | addRuleNode(ruleNode: FcRuleNode) { | 1172 | addRuleNode(ruleNode: FcRuleNode) { |
740 | ruleNode.configuration = deepClone(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration); | 1173 | ruleNode.configuration = deepClone(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration); |
741 | const ruleChainId = this.ruleChain.id ? this.ruleChain.id.id : null; | 1174 | const ruleChainId = this.ruleChain.id ? this.ruleChain.id.id : null; |
1175 | + this.enableHotKeys = false; | ||
742 | this.dialog.open<AddRuleNodeDialogComponent, AddRuleNodeDialogData, | 1176 | this.dialog.open<AddRuleNodeDialogComponent, AddRuleNodeDialogData, |
743 | FcRuleNode>(AddRuleNodeDialogComponent, { | 1177 | FcRuleNode>(AddRuleNodeDialogComponent, { |
744 | disableClose: true, | 1178 | disableClose: true, |
@@ -772,6 +1206,7 @@ export class RuleChainPageComponent extends PageComponent | @@ -772,6 +1206,7 @@ export class RuleChainPageComponent extends PageComponent | ||
772 | this.onModelChanged(); | 1206 | this.onModelChanged(); |
773 | this.updateRuleNodesHighlight(); | 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,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 | private displayTooltip(event: MouseEvent, content: string) { | 1285 | private displayTooltip(event: MouseEvent, content: string) { |
840 | this.destroyTooltips(); | 1286 | this.destroyTooltips(); |
841 | this.tooltipTimeout = setTimeout(() => { | 1287 | this.tooltipTimeout = setTimeout(() => { |
@@ -14,37 +14,32 @@ | @@ -14,37 +14,32 @@ | ||
14 | /// limitations under the License. | 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 | export interface FcRuleNodeTypeModel extends FcModel { | 20 | export interface FcRuleNodeTypeModel extends FcModel { |
30 | nodes: Array<FcRuleNodeType>; | 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,6 +14,8 @@ | ||
14 | * limitations under the License. | 14 | * limitations under the License. |
15 | */ | 15 | */ |
16 | 16 | ||
17 | +@import './rule-node-colors'; | ||
18 | + | ||
17 | :host { | 19 | :host { |
18 | 20 | ||
19 | .fc-node-overlay { | 21 | .fc-node-overlay { |
@@ -63,33 +65,7 @@ | @@ -63,33 +65,7 @@ | ||
63 | border: solid 1px #777; | 65 | border: solid 1px #777; |
64 | border-radius: 5px; | 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 | &.tb-rule-node-highlighted:not(.tb-rule-node-invalid) { | 70 | &.tb-rule-node-highlighted:not(.tb-rule-node-invalid) { |
95 | box-shadow: 0 0 10px 6px #51cbee; | 71 | box-shadow: 0 0 10px 6px #51cbee; |
@@ -15,9 +15,9 @@ | @@ -15,9 +15,9 @@ | ||
15 | limitations under the License. | 15 | limitations under the License. |
16 | 16 | ||
17 | --> | 17 | --> |
18 | -<hotkeys-cheatsheet></hotkeys-cheatsheet> | ||
19 | <div fxFlex fxLayout="column"> | 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 | <mat-toolbar class="mat-elevation-z1 tb-edit-toolbar mat-hue-3" fxLayoutGap="16px"> | 21 | <mat-toolbar class="mat-elevation-z1 tb-edit-toolbar mat-hue-3" fxLayoutGap="16px"> |
22 | <mat-form-field floatLabel="always" hideRequiredMarker class="tb-widget-title"> | 22 | <mat-form-field floatLabel="always" hideRequiredMarker class="tb-widget-title"> |
23 | <mat-label></mat-label> | 23 | <mat-label></mat-label> |
@@ -139,6 +139,8 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | @@ -139,6 +139,8 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | ||
139 | 139 | ||
140 | saveWidgetTimeout: Timeout; | 140 | saveWidgetTimeout: Timeout; |
141 | 141 | ||
142 | + hotKeys: Hotkey[] = []; | ||
143 | + | ||
142 | private rxSubscriptions = new Array<Subscription>(); | 144 | private rxSubscriptions = new Array<Subscription>(); |
143 | 145 | ||
144 | constructor(protected store: Store<AppState>, | 146 | constructor(protected store: Store<AppState>, |
@@ -146,7 +148,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | @@ -146,7 +148,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | ||
146 | private route: ActivatedRoute, | 148 | private route: ActivatedRoute, |
147 | private router: Router, | 149 | private router: Router, |
148 | private widgetService: WidgetService, | 150 | private widgetService: WidgetService, |
149 | - private hotkeysService: HotkeysService, | ||
150 | private translate: TranslateService, | 151 | private translate: TranslateService, |
151 | private raf: RafService, | 152 | private raf: RafService, |
152 | private dialog: MatDialog) { | 153 | private dialog: MatDialog) { |
@@ -159,6 +160,8 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | @@ -159,6 +160,8 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | ||
159 | this.init(data); | 160 | this.init(data); |
160 | } | 161 | } |
161 | )); | 162 | )); |
163 | + | ||
164 | + this.initHotKeys(); | ||
162 | } | 165 | } |
163 | 166 | ||
164 | private init(data: any) { | 167 | private init(data: any) { |
@@ -181,7 +184,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | @@ -181,7 +184,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | ||
181 | } | 184 | } |
182 | 185 | ||
183 | ngOnInit(): void { | 186 | ngOnInit(): void { |
184 | - this.initHotKeys(); | ||
185 | this.initSplitLayout(); | 187 | this.initSplitLayout(); |
186 | this.initAceEditors(); | 188 | this.initAceEditors(); |
187 | this.iframe = $(this.widgetIFrameElmRef.nativeElement); | 189 | this.iframe = $(this.widgetIFrameElmRef.nativeElement); |
@@ -203,7 +205,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | @@ -203,7 +205,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | ||
203 | } | 205 | } |
204 | 206 | ||
205 | private initHotKeys(): void { | 207 | private initHotKeys(): void { |
206 | - this.hotkeysService.add( | 208 | + this.hotKeys.push( |
207 | new Hotkey('ctrl+q', (event: KeyboardEvent) => { | 209 | new Hotkey('ctrl+q', (event: KeyboardEvent) => { |
208 | if (!getCurrentIsLoading(this.store) && !this.undoDisabled()) { | 210 | if (!getCurrentIsLoading(this.store) && !this.undoDisabled()) { |
209 | event.preventDefault(); | 211 | event.preventDefault(); |
@@ -213,7 +215,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | @@ -213,7 +215,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | ||
213 | }, ['INPUT', 'SELECT', 'TEXTAREA'], | 215 | }, ['INPUT', 'SELECT', 'TEXTAREA'], |
214 | this.translate.instant('widget.undo')) | 216 | this.translate.instant('widget.undo')) |
215 | ); | 217 | ); |
216 | - this.hotkeysService.add( | 218 | + this.hotKeys.push( |
217 | new Hotkey('ctrl+s', (event: KeyboardEvent) => { | 219 | new Hotkey('ctrl+s', (event: KeyboardEvent) => { |
218 | if (!getCurrentIsLoading(this.store) && !this.saveDisabled()) { | 220 | if (!getCurrentIsLoading(this.store) && !this.saveDisabled()) { |
219 | event.preventDefault(); | 221 | event.preventDefault(); |
@@ -223,7 +225,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | @@ -223,7 +225,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | ||
223 | }, ['INPUT', 'SELECT', 'TEXTAREA'], | 225 | }, ['INPUT', 'SELECT', 'TEXTAREA'], |
224 | this.translate.instant('widget.save')) | 226 | this.translate.instant('widget.save')) |
225 | ); | 227 | ); |
226 | - this.hotkeysService.add( | 228 | + this.hotKeys.push( |
227 | new Hotkey('shift+ctrl+s', (event: KeyboardEvent) => { | 229 | new Hotkey('shift+ctrl+s', (event: KeyboardEvent) => { |
228 | if (!getCurrentIsLoading(this.store) && !this.saveAsDisabled()) { | 230 | if (!getCurrentIsLoading(this.store) && !this.saveAsDisabled()) { |
229 | event.preventDefault(); | 231 | event.preventDefault(); |
@@ -233,7 +235,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | @@ -233,7 +235,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | ||
233 | }, ['INPUT', 'SELECT', 'TEXTAREA'], | 235 | }, ['INPUT', 'SELECT', 'TEXTAREA'], |
234 | this.translate.instant('widget.saveAs')) | 236 | this.translate.instant('widget.saveAs')) |
235 | ); | 237 | ); |
236 | - this.hotkeysService.add( | 238 | + this.hotKeys.push( |
237 | new Hotkey('shift+ctrl+f', (event: KeyboardEvent) => { | 239 | new Hotkey('shift+ctrl+f', (event: KeyboardEvent) => { |
238 | event.preventDefault(); | 240 | event.preventDefault(); |
239 | this.fullscreen = !this.fullscreen; | 241 | this.fullscreen = !this.fullscreen; |
@@ -241,7 +243,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | @@ -241,7 +243,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe | ||
241 | }, ['INPUT', 'SELECT', 'TEXTAREA'], | 243 | }, ['INPUT', 'SELECT', 'TEXTAREA'], |
242 | this.translate.instant('widget.toggle-fullscreen')) | 244 | this.translate.instant('widget.toggle-fullscreen')) |
243 | ); | 245 | ); |
244 | - this.hotkeysService.add( | 246 | + this.hotKeys.push( |
245 | new Hotkey('ctrl+enter', (event: KeyboardEvent) => { | 247 | new Hotkey('ctrl+enter', (event: KeyboardEvent) => { |
246 | event.preventDefault(); | 248 | event.preventDefault(); |
247 | this.applyWidgetScript(); | 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,18 +14,14 @@ | ||
14 | /// limitations under the License. | 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 | import { Observable } from 'rxjs'; | 22 | import { Observable } from 'rxjs'; |
26 | import { PageComponent } from '@shared/components/page.component'; | 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 | import { Store } from '@ngrx/store'; | 25 | import { Store } from '@ngrx/store'; |
30 | import { AppState } from '@core/core.state'; | 26 | import { AppState } from '@core/core.state'; |
31 | import { AbstractControl, FormGroup } from '@angular/forms'; | 27 | import { AbstractControl, FormGroup } from '@angular/forms'; |
@@ -38,7 +34,6 @@ export enum MsgDataType { | @@ -38,7 +34,6 @@ export enum MsgDataType { | ||
38 | 34 | ||
39 | export interface RuleNodeConfiguration { | 35 | export interface RuleNodeConfiguration { |
40 | [key: string]: any; | 36 | [key: string]: any; |
41 | - // TODO: | ||
42 | } | 37 | } |
43 | 38 | ||
44 | export interface RuleNode extends BaseData<RuleNodeId> { | 39 | export interface RuleNode extends BaseData<RuleNodeId> { |
@@ -307,6 +302,28 @@ export interface RuleNodeComponentDescriptor extends ComponentDescriptor { | @@ -307,6 +302,28 @@ export interface RuleNodeComponentDescriptor extends ComponentDescriptor { | ||
307 | configurationDescriptor?: RuleNodeConfigurationDescriptor; | 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 | export interface TestScriptInputParams { | 327 | export interface TestScriptInputParams { |
311 | script: string; | 328 | script: string; |
312 | scriptType: string; | 329 | scriptType: string; |
@@ -118,6 +118,8 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc | @@ -118,6 +118,8 @@ import { NodeScriptTestDialogComponent } from '@shared/components/dialog/node-sc | ||
118 | import { MessageTypeAutocompleteComponent } from './components/message-type-autocomplete.component'; | 118 | import { MessageTypeAutocompleteComponent } from './components/message-type-autocomplete.component'; |
119 | import { JsonContentComponent } from './components/json-content.component'; | 119 | import { JsonContentComponent } from './components/json-content.component'; |
120 | import { KeyValMapComponent } from './components/kv-map.component'; | 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 | @NgModule({ | 124 | @NgModule({ |
123 | providers: [ | 125 | providers: [ |
@@ -149,11 +151,13 @@ import { KeyValMapComponent } from './components/kv-map.component'; | @@ -149,11 +151,13 @@ import { KeyValMapComponent } from './components/kv-map.component'; | ||
149 | FullscreenDirective, | 151 | FullscreenDirective, |
150 | CircularProgressDirective, | 152 | CircularProgressDirective, |
151 | MatChipDraggableDirective, | 153 | MatChipDraggableDirective, |
154 | + TbHotkeysDirective, | ||
152 | TbAnchorComponent, | 155 | TbAnchorComponent, |
153 | HelpComponent, | 156 | HelpComponent, |
154 | TbCheckboxComponent, | 157 | TbCheckboxComponent, |
155 | TbSnackBarComponent, | 158 | TbSnackBarComponent, |
156 | TbErrorComponent, | 159 | TbErrorComponent, |
160 | + TbCheatSheetComponent, | ||
157 | BreadcrumbComponent, | 161 | BreadcrumbComponent, |
158 | UserMenuComponent, | 162 | UserMenuComponent, |
159 | TimewindowComponent, | 163 | TimewindowComponent, |
@@ -256,10 +260,12 @@ import { KeyValMapComponent } from './components/kv-map.component'; | @@ -256,10 +260,12 @@ import { KeyValMapComponent } from './components/kv-map.component'; | ||
256 | FullscreenDirective, | 260 | FullscreenDirective, |
257 | CircularProgressDirective, | 261 | CircularProgressDirective, |
258 | MatChipDraggableDirective, | 262 | MatChipDraggableDirective, |
263 | + TbHotkeysDirective, | ||
259 | TbAnchorComponent, | 264 | TbAnchorComponent, |
260 | HelpComponent, | 265 | HelpComponent, |
261 | TbCheckboxComponent, | 266 | TbCheckboxComponent, |
262 | TbErrorComponent, | 267 | TbErrorComponent, |
268 | + TbCheatSheetComponent, | ||
263 | BreadcrumbComponent, | 269 | BreadcrumbComponent, |
264 | UserMenuComponent, | 270 | UserMenuComponent, |
265 | TimewindowComponent, | 271 | TimewindowComponent, |