Commit c3ebc2f20d4dc1e03418c80d278a2cbd34460918
1 parent
c317be2a
Widget Configuration: Widget Actions.
Showing
55 changed files
with
3959 additions
and
177 deletions
Too many changes to show.
To preserve performance only 55 of 67 files are displayed.
@@ -5432,9 +5432,9 @@ | @@ -5432,9 +5432,9 @@ | ||
5432 | "dev": true | 5432 | "dev": true |
5433 | }, | 5433 | }, |
5434 | "https-proxy-agent": { | 5434 | "https-proxy-agent": { |
5435 | - "version": "2.2.2", | ||
5436 | - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.2.tgz", | ||
5437 | - "integrity": "sha512-c8Ndjc9Bkpfx/vCJueCPy0jlP4ccCCSNDp8xwCZzPjKJUm+B+u9WX2x98Qx4n1PiMNTWo3D7KK5ifNV/yJyRzg==", | 5435 | + "version": "2.2.3", |
5436 | + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.3.tgz", | ||
5437 | + "integrity": "sha512-Ytgnz23gm2DVftnzqRRz2dOXZbGd2uiajSw/95bPp6v53zPRspQjLm/AfBgqbJ2qfeRXWIOMVLpp86+/5yX39Q==", | ||
5438 | "dev": true, | 5438 | "dev": true, |
5439 | "requires": { | 5439 | "requires": { |
5440 | "agent-base": "^4.3.0", | 5440 | "agent-base": "^4.3.0", |
@@ -15,7 +15,8 @@ | @@ -15,7 +15,8 @@ | ||
15 | /// | 15 | /// |
16 | 16 | ||
17 | import { | 17 | import { |
18 | - IWidgetSubscription, SubscriptionEntityInfo, | 18 | + IWidgetSubscription, |
19 | + SubscriptionEntityInfo, | ||
19 | WidgetSubscriptionCallbacks, | 20 | WidgetSubscriptionCallbacks, |
20 | WidgetSubscriptionContext, | 21 | WidgetSubscriptionContext, |
21 | WidgetSubscriptionOptions | 22 | WidgetSubscriptionOptions |
@@ -48,6 +49,7 @@ import { deepClone, isDefined } from '@core/utils'; | @@ -48,6 +49,7 @@ import { deepClone, isDefined } from '@core/utils'; | ||
48 | import { AlarmSourceListener } from '@core/http/alarm.service'; | 49 | import { AlarmSourceListener } from '@core/http/alarm.service'; |
49 | import { DatasourceListener } from '@core/api/datasource.service'; | 50 | import { DatasourceListener } from '@core/api/datasource.service'; |
50 | import * as deepEqual from 'deep-equal'; | 51 | import * as deepEqual from 'deep-equal'; |
52 | +import { EntityId } from '@app/shared/models/id/entity-id'; | ||
51 | 53 | ||
52 | export class WidgetSubscription implements IWidgetSubscription { | 54 | export class WidgetSubscription implements IWidgetSubscription { |
53 | 55 | ||
@@ -339,7 +341,44 @@ export class WidgetSubscription implements IWidgetSubscription { | @@ -339,7 +341,44 @@ export class WidgetSubscription implements IWidgetSubscription { | ||
339 | } | 341 | } |
340 | 342 | ||
341 | getFirstEntityInfo(): SubscriptionEntityInfo { | 343 | getFirstEntityInfo(): SubscriptionEntityInfo { |
342 | - return undefined; | 344 | + let entityId: EntityId; |
345 | + let entityName: string; | ||
346 | + if (this.type === widgetType.rpc) { | ||
347 | + if (this.targetDeviceId) { | ||
348 | + entityId = { | ||
349 | + entityType: EntityType.DEVICE, | ||
350 | + id: this.targetDeviceId | ||
351 | + }; | ||
352 | + entityName = this.targetDeviceName; | ||
353 | + } | ||
354 | + } else if (this.type === widgetType.alarm) { | ||
355 | + if (this.alarmSource && this.alarmSource.entityType && this.alarmSource.entityId) { | ||
356 | + entityId = { | ||
357 | + entityType: this.alarmSource.entityType, | ||
358 | + id: this.alarmSource.entityId | ||
359 | + }; | ||
360 | + entityName = this.alarmSource.entityName; | ||
361 | + } | ||
362 | + } else { | ||
363 | + for (const datasource of this.datasources) { | ||
364 | + if (datasource && datasource.entityType && datasource.entityId) { | ||
365 | + entityId = { | ||
366 | + entityType: datasource.entityType, | ||
367 | + id: datasource.entityId | ||
368 | + }; | ||
369 | + entityName = datasource.entityName; | ||
370 | + break; | ||
371 | + } | ||
372 | + } | ||
373 | + } | ||
374 | + if (entityId) { | ||
375 | + return { | ||
376 | + entityId, | ||
377 | + entityName | ||
378 | + }; | ||
379 | + } else { | ||
380 | + return null; | ||
381 | + } | ||
343 | } | 382 | } |
344 | 383 | ||
345 | onAliasesChanged(aliasIds: Array<string>): boolean { | 384 | onAliasesChanged(aliasIds: Array<string>): boolean { |
@@ -26,6 +26,10 @@ import { | @@ -26,6 +26,10 @@ import { | ||
26 | ColorPickerDialogComponent, | 26 | ColorPickerDialogComponent, |
27 | ColorPickerDialogData | 27 | ColorPickerDialogData |
28 | } from '@shared/components/dialog/color-picker-dialog.component'; | 28 | } from '@shared/components/dialog/color-picker-dialog.component'; |
29 | +import { | ||
30 | + MaterialIconsDialogComponent, | ||
31 | + MaterialIconsDialogData | ||
32 | +} from '@shared/components/dialog/material-icons-dialog.component'; | ||
29 | 33 | ||
30 | @Injectable( | 34 | @Injectable( |
31 | { | 35 | { |
@@ -85,6 +89,17 @@ export class DialogService { | @@ -85,6 +89,17 @@ export class DialogService { | ||
85 | }).afterClosed(); | 89 | }).afterClosed(); |
86 | } | 90 | } |
87 | 91 | ||
92 | + materialIconPicker(icon: string): Observable<string> { | ||
93 | + return this.dialog.open<MaterialIconsDialogComponent, MaterialIconsDialogData, string>(MaterialIconsDialogComponent, | ||
94 | + { | ||
95 | + disableClose: true, | ||
96 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | ||
97 | + data: { | ||
98 | + icon | ||
99 | + } | ||
100 | + }).afterClosed(); | ||
101 | + } | ||
102 | + | ||
88 | private permissionDenied() { | 103 | private permissionDenied() { |
89 | this.alert( | 104 | this.alert( |
90 | this.translate.instant('access.permission-denied'), | 105 | this.translate.instant('access.permission-denied'), |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Inject, Injectable } from '@angular/core'; | 17 | +import { Inject, Injectable, NgZone } from '@angular/core'; |
18 | import { WINDOW } from '@core/services/window.service'; | 18 | import { WINDOW } from '@core/services/window.service'; |
19 | import { ExceptionData } from '@app/shared/models/error.models'; | 19 | import { ExceptionData } from '@app/shared/models/error.models'; |
20 | import { deepClone, deleteNullProperties, isDefined, isUndefined } from '@core/utils'; | 20 | import { deepClone, deleteNullProperties, isDefined, isUndefined } from '@core/utils'; |
@@ -28,6 +28,8 @@ import { alarmFields } from '@shared/models/alarm.models'; | @@ -28,6 +28,8 @@ import { alarmFields } from '@shared/models/alarm.models'; | ||
28 | import { materialColors } from '@app/shared/models/material.models'; | 28 | import { materialColors } from '@app/shared/models/material.models'; |
29 | import { WidgetInfo } from '@home/models/widget-component.models'; | 29 | import { WidgetInfo } from '@home/models/widget-component.models'; |
30 | import jsonSchemaDefaults from 'json-schema-defaults'; | 30 | import jsonSchemaDefaults from 'json-schema-defaults'; |
31 | +import * as materialIconsCodepoints from '!raw-loader!material-design-icons/iconfont/codepoints'; | ||
32 | +import { Observable, of, ReplaySubject } from 'rxjs'; | ||
31 | 33 | ||
32 | const varsRegex = /\$\{([^}]*)\}/g; | 34 | const varsRegex = /\$\{([^}]*)\}/g; |
33 | 35 | ||
@@ -58,6 +60,13 @@ const defaultAlarmFields: Array<string> = [ | @@ -58,6 +60,13 @@ const defaultAlarmFields: Array<string> = [ | ||
58 | alarmFields.status.keyName | 60 | alarmFields.status.keyName |
59 | ]; | 61 | ]; |
60 | 62 | ||
63 | +const commonMaterialIcons: Array<string> = [ 'more_horiz', 'more_vert', 'open_in_new', | ||
64 | + 'visibility', 'play_arrow', 'arrow_back', 'arrow_downward', | ||
65 | + 'arrow_forward', 'arrow_upwards', 'close', 'refresh', 'menu', 'show_chart', 'multiline_chart', 'pie_chart', 'insert_chart', 'people', | ||
66 | + 'person', 'domain', 'devices_other', 'now_widgets', 'dashboards', 'map', 'pin_drop', 'my_location', 'extension', 'search', | ||
67 | + 'settings', 'notifications', 'notifications_active', 'info', 'info_outline', 'warning', 'list', 'file_download', 'import_export', | ||
68 | + 'share', 'add', 'edit', 'done' ]; | ||
69 | + | ||
61 | @Injectable({ | 70 | @Injectable({ |
62 | providedIn: 'root' | 71 | providedIn: 'root' |
63 | }) | 72 | }) |
@@ -85,7 +94,10 @@ export class UtilsService { | @@ -85,7 +94,10 @@ export class UtilsService { | ||
85 | 94 | ||
86 | defaultAlarmDataKeys: Array<DataKey> = []; | 95 | defaultAlarmDataKeys: Array<DataKey> = []; |
87 | 96 | ||
97 | + materialIcons: Array<string> = []; | ||
98 | + | ||
88 | constructor(@Inject(WINDOW) private window: Window, | 99 | constructor(@Inject(WINDOW) private window: Window, |
100 | + private zone: NgZone, | ||
89 | private translate: TranslateService) { | 101 | private translate: TranslateService) { |
90 | let frame: Element = null; | 102 | let frame: Element = null; |
91 | try { | 103 | try { |
@@ -282,6 +294,31 @@ export class UtilsService { | @@ -282,6 +294,31 @@ export class UtilsService { | ||
282 | return datasources; | 294 | return datasources; |
283 | } | 295 | } |
284 | 296 | ||
297 | + public getMaterialIcons(): Observable<Array<string>> { | ||
298 | + if (this.materialIcons.length) { | ||
299 | + return of(this.materialIcons); | ||
300 | + } else { | ||
301 | + const materialIconsSubject = new ReplaySubject<Array<string>>(); | ||
302 | + this.zone.runOutsideAngular(() => { | ||
303 | + const codepointsArray = materialIconsCodepoints | ||
304 | + .split('\n') | ||
305 | + .filter((codepoint) => codepoint && codepoint.length); | ||
306 | + codepointsArray.forEach((codepoint) => { | ||
307 | + const values = codepoint.split(' '); | ||
308 | + if (values && values.length === 2) { | ||
309 | + this.materialIcons.push(values[0]); | ||
310 | + } | ||
311 | + }); | ||
312 | + materialIconsSubject.next(this.materialIcons); | ||
313 | + }); | ||
314 | + return materialIconsSubject.asObservable(); | ||
315 | + } | ||
316 | + } | ||
317 | + | ||
318 | + public getCommonMaterialIcons(): Array<string> { | ||
319 | + return commonMaterialIcons; | ||
320 | + } | ||
321 | + | ||
285 | public getMaterialColor(index: number) { | 322 | public getMaterialColor(index: number) { |
286 | const colorIndex = index % materialColors.length; | 323 | const colorIndex = index % materialColors.length; |
287 | return materialColors[colorIndex].value; | 324 | return materialColors[colorIndex].value; |
@@ -88,7 +88,7 @@ | @@ -88,7 +88,7 @@ | ||
88 | <tb-timewindow *ngIf="widget.hasTimewindow" | 88 | <tb-timewindow *ngIf="widget.hasTimewindow" |
89 | #timewindowComponent | 89 | #timewindowComponent |
90 | aggregation="{{widget.hasAggregation}}" | 90 | aggregation="{{widget.hasAggregation}}" |
91 | - [ngModel]="widget.widget.config.timewindow" | 91 | + [ngModel]="widgetComponent.widget.config.timewindow" |
92 | (ngModelChange)="widgetComponent.onTimewindowChanged($event)"> | 92 | (ngModelChange)="widgetComponent.onTimewindowChanged($event)"> |
93 | </tb-timewindow> | 93 | </tb-timewindow> |
94 | </div> | 94 | </div> |
@@ -15,12 +15,12 @@ | @@ -15,12 +15,12 @@ | ||
15 | /// | 15 | /// |
16 | 16 | ||
17 | import { | 17 | import { |
18 | - AfterViewInit, | 18 | + AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, |
19 | Component, | 19 | Component, |
20 | DoCheck, | 20 | DoCheck, |
21 | Input, | 21 | Input, |
22 | IterableDiffers, | 22 | IterableDiffers, |
23 | - KeyValueDiffers, | 23 | + KeyValueDiffers, NgZone, |
24 | OnChanges, | 24 | OnChanges, |
25 | OnInit, | 25 | OnInit, |
26 | SimpleChanges, | 26 | SimpleChanges, |
@@ -162,7 +162,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -162,7 +162,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
162 | private dialogService: DialogService, | 162 | private dialogService: DialogService, |
163 | private breakpointObserver: BreakpointObserver, | 163 | private breakpointObserver: BreakpointObserver, |
164 | private differs: IterableDiffers, | 164 | private differs: IterableDiffers, |
165 | - private kvDiffers: KeyValueDiffers) { | 165 | + private kvDiffers: KeyValueDiffers, |
166 | + private ngZone: NgZone) { | ||
166 | super(store); | 167 | super(store); |
167 | this.authUser = getCurrentAuthUser(store); | 168 | this.authUser = getCurrentAuthUser(store); |
168 | } | 169 | } |
@@ -259,20 +260,24 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -259,20 +260,24 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
259 | } | 260 | } |
260 | 261 | ||
261 | onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void { | 262 | onUpdateTimewindow(startTimeMs: number, endTimeMs: number, interval?: number): void { |
262 | - if (!this.originalDashboardTimewindow) { | ||
263 | - this.originalDashboardTimewindow = deepClone(this.dashboardTimewindow); | ||
264 | - } | ||
265 | - this.dashboardTimewindow = toHistoryTimewindow(this.dashboardTimewindow, | ||
266 | - startTimeMs, endTimeMs, interval, this.timeService); | ||
267 | - this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow); | 263 | + this.ngZone.run(() => { |
264 | + if (!this.originalDashboardTimewindow) { | ||
265 | + this.originalDashboardTimewindow = deepClone(this.dashboardTimewindow); | ||
266 | + } | ||
267 | + this.dashboardTimewindow = toHistoryTimewindow(this.dashboardTimewindow, | ||
268 | + startTimeMs, endTimeMs, interval, this.timeService); | ||
269 | + this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow); | ||
270 | + }); | ||
268 | } | 271 | } |
269 | 272 | ||
270 | onResetTimewindow(): void { | 273 | onResetTimewindow(): void { |
271 | - if (this.originalDashboardTimewindow) { | ||
272 | - this.dashboardTimewindow = deepClone(this.originalDashboardTimewindow); | ||
273 | - this.originalDashboardTimewindow = null; | ||
274 | - this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow); | ||
275 | - } | 274 | + this.ngZone.run(() => { |
275 | + if (this.originalDashboardTimewindow) { | ||
276 | + this.dashboardTimewindow = deepClone(this.originalDashboardTimewindow); | ||
277 | + this.originalDashboardTimewindow = null; | ||
278 | + this.dashboardTimewindowChangedSubject.next(this.dashboardTimewindow); | ||
279 | + } | ||
280 | + }); | ||
276 | } | 281 | } |
277 | 282 | ||
278 | isAutofillHeight(): boolean { | 283 | isAutofillHeight(): boolean { |
@@ -456,7 +461,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | @@ -456,7 +461,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo | ||
456 | this.gridsterOpts.draggable.enabled = this.isEdit; | 461 | this.gridsterOpts.draggable.enabled = this.isEdit; |
457 | } | 462 | } |
458 | 463 | ||
459 | - private notifyGridsterOptionsChanged() { | 464 | + public notifyGridsterOptionsChanged() { |
460 | if (this.gridster && this.gridster.options) { | 465 | if (this.gridster && this.gridster.options) { |
461 | this.gridster.optionsChanged(); | 466 | this.gridster.optionsChanged(); |
462 | } | 467 | } |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, EventEmitter, Input, Output } from '@angular/core'; | 17 | +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; |
18 | import { PageComponent } from '@shared/components/page.component'; | 18 | 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'; |
@@ -50,6 +50,12 @@ import { EntityAliasSelectComponent } from './alias/entity-alias-select.componen | @@ -50,6 +50,12 @@ import { EntityAliasSelectComponent } from './alias/entity-alias-select.componen | ||
50 | import { DataKeysComponent } from '@home/components/widget/data-keys.component'; | 50 | import { DataKeysComponent } from '@home/components/widget/data-keys.component'; |
51 | import { DataKeyConfigDialogComponent } from './widget/data-key-config-dialog.component'; | 51 | import { DataKeyConfigDialogComponent } from './widget/data-key-config-dialog.component'; |
52 | import { DataKeyConfigComponent } from './widget/data-key-config.component'; | 52 | import { DataKeyConfigComponent } from './widget/data-key-config.component'; |
53 | +import { LegendConfigPanelComponent } from './widget/legend-config-panel.component'; | ||
54 | +import { LegendConfigComponent } from './widget/legend-config.component'; | ||
55 | +import { ManageWidgetActionsComponent } from './widget/action/manage-widget-actions.component'; | ||
56 | +import { WidgetActionDialogComponent } from './widget/action/widget-action-dialog.component'; | ||
57 | +import { CustomActionPrettyResourcesTabsComponent } from './widget/action/custom-action-pretty-resources-tabs.component'; | ||
58 | +import { CustomActionPrettyEditorComponent } from './widget/action/custom-action-pretty-editor.component'; | ||
53 | 59 | ||
54 | @NgModule({ | 60 | @NgModule({ |
55 | entryComponents: [ | 61 | entryComponents: [ |
@@ -64,7 +70,9 @@ import { DataKeyConfigComponent } from './widget/data-key-config.component'; | @@ -64,7 +70,9 @@ import { DataKeyConfigComponent } from './widget/data-key-config.component'; | ||
64 | AliasesEntitySelectPanelComponent, | 70 | AliasesEntitySelectPanelComponent, |
65 | EntityAliasesDialogComponent, | 71 | EntityAliasesDialogComponent, |
66 | EntityAliasDialogComponent, | 72 | EntityAliasDialogComponent, |
67 | - DataKeyConfigDialogComponent | 73 | + DataKeyConfigDialogComponent, |
74 | + LegendConfigPanelComponent, | ||
75 | + WidgetActionDialogComponent | ||
68 | ], | 76 | ], |
69 | declarations: | 77 | declarations: |
70 | [ | 78 | [ |
@@ -99,7 +107,13 @@ import { DataKeyConfigComponent } from './widget/data-key-config.component'; | @@ -99,7 +107,13 @@ import { DataKeyConfigComponent } from './widget/data-key-config.component'; | ||
99 | EntityAliasSelectComponent, | 107 | EntityAliasSelectComponent, |
100 | DataKeysComponent, | 108 | DataKeysComponent, |
101 | DataKeyConfigComponent, | 109 | DataKeyConfigComponent, |
102 | - DataKeyConfigDialogComponent | 110 | + DataKeyConfigDialogComponent, |
111 | + LegendConfigPanelComponent, | ||
112 | + LegendConfigComponent, | ||
113 | + ManageWidgetActionsComponent, | ||
114 | + WidgetActionDialogComponent, | ||
115 | + CustomActionPrettyResourcesTabsComponent, | ||
116 | + CustomActionPrettyEditorComponent | ||
103 | ], | 117 | ], |
104 | imports: [ | 118 | imports: [ |
105 | CommonModule, | 119 | CommonModule, |
@@ -130,7 +144,12 @@ import { DataKeyConfigComponent } from './widget/data-key-config.component'; | @@ -130,7 +144,12 @@ import { DataKeyConfigComponent } from './widget/data-key-config.component'; | ||
130 | EntityAliasSelectComponent, | 144 | EntityAliasSelectComponent, |
131 | DataKeysComponent, | 145 | DataKeysComponent, |
132 | DataKeyConfigComponent, | 146 | DataKeyConfigComponent, |
133 | - DataKeyConfigDialogComponent | 147 | + DataKeyConfigDialogComponent, |
148 | + LegendConfigComponent, | ||
149 | + ManageWidgetActionsComponent, | ||
150 | + WidgetActionDialogComponent, | ||
151 | + CustomActionPrettyResourcesTabsComponent, | ||
152 | + CustomActionPrettyEditorComponent | ||
134 | ], | 153 | ], |
135 | providers: [ | 154 | providers: [ |
136 | WidgetComponentService | 155 | WidgetComponentService |
ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.html
0 → 100644
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<div class="tb-custom-action-pretty mat-elevation-z1" tb-fullscreen [fullscreen]="fullscreen"> | ||
19 | + <div fxLayout="row" fxLayoutAlign="end center" class="tb-action-expand-button" [ngClass]="{'tb-fullscreen-editor': fullscreen}"> | ||
20 | + <button mat-button fxHide.xs fxHide.sm | ||
21 | + matTooltip="{{ 'widget.toggle-fullscreen' | translate }}" | ||
22 | + matTooltipPosition="above" | ||
23 | + (click)="fullscreen = !fullscreen"> | ||
24 | + <mat-icon>{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> | ||
25 | + <span *ngIf="fullscreen" translate>widget.toggle-fullscreen</span> | ||
26 | + </button> | ||
27 | + </div> | ||
28 | + <div class="tb-custom-action-editor" [ngClass]="{'tb-fullscreen-editor': fullscreen}"> | ||
29 | + <div *ngIf="!fullscreen; else fullscreenEditor"> | ||
30 | + <tb-custom-action-pretty-resources-tabs [hasCustomFunction]="true" | ||
31 | + [action]="action" (actionUpdated)="onActionUpdated($event ? true : false)"> | ||
32 | + </tb-custom-action-pretty-resources-tabs> | ||
33 | + </div> | ||
34 | + <ng-template #fullscreenEditor> | ||
35 | + <div class="tb-fullscreen-panel tb-layout-fill" fxLayout="row"> | ||
36 | + <div #leftPanel class="tb-split tb-content"> | ||
37 | + <tb-custom-action-pretty-resources-tabs [hasCustomFunction]="false" | ||
38 | + [action]="action" (actionUpdated)="onActionUpdated($event ? true : false)"> | ||
39 | + </tb-custom-action-pretty-resources-tabs> | ||
40 | + </div> | ||
41 | + <div #rightPanel class="tb-split tb-content right-panel"> | ||
42 | + <tb-js-func | ||
43 | + [(ngModel)]="action.customFunction" | ||
44 | + (ngModelChange)="onActionUpdated()" | ||
45 | + [fillHeight]="true" | ||
46 | + [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams']" | ||
47 | + [validationArgs]="[]"> | ||
48 | + </tb-js-func> | ||
49 | + </div> | ||
50 | + </div> | ||
51 | + </ng-template> | ||
52 | + </div> | ||
53 | +</div> |
ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.scss
0 → 100644
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 | +.tb-custom-action-pretty { | ||
17 | + box-sizing: border-box; | ||
18 | + position: relative; | ||
19 | + padding: 8px; | ||
20 | + background-color: #fff; | ||
21 | + | ||
22 | + .tb-fullscreen-panel { | ||
23 | + .tb-custom-action-editor-container { | ||
24 | + height: calc(100% - 40px); | ||
25 | + } | ||
26 | + | ||
27 | + .right-panel { | ||
28 | + padding-top: 8px; | ||
29 | + padding-left: 3px; | ||
30 | + } | ||
31 | + | ||
32 | + tb-js-func .tb-js-func-panel { | ||
33 | + box-sizing: border-box; | ||
34 | + } | ||
35 | + | ||
36 | + mat-tab-group { | ||
37 | + .mat-tab-body-wrapper { | ||
38 | + height: 100%; | ||
39 | + mat-tab-body { | ||
40 | + height: 100%; | ||
41 | + & > div { | ||
42 | + height: 100%; | ||
43 | + } | ||
44 | + } | ||
45 | + } | ||
46 | + } | ||
47 | + } | ||
48 | + | ||
49 | + .tb-split { | ||
50 | + box-sizing: border-box; | ||
51 | + overflow-x: hidden; | ||
52 | + overflow-y: auto; | ||
53 | + } | ||
54 | + | ||
55 | + .tb-content { | ||
56 | + border: 1px solid #c0c0c0; | ||
57 | + } | ||
58 | + | ||
59 | + .gutter { | ||
60 | + background-color: #eee; | ||
61 | + background-repeat: no-repeat; | ||
62 | + background-position: 50%; | ||
63 | + } | ||
64 | + | ||
65 | + .gutter.gutter-horizontal { | ||
66 | + cursor: col-resize; | ||
67 | + background-image: url("../../../../../../assets/split.js/grips/vertical.png"); | ||
68 | + } | ||
69 | + | ||
70 | + .tb-split.tb-split-horizontal, | ||
71 | + .gutter.gutter-horizontal { | ||
72 | + float: left; | ||
73 | + height: 100%; | ||
74 | + } | ||
75 | + | ||
76 | + .tb-action-expand-button { | ||
77 | + position: absolute; | ||
78 | + right: 14px; | ||
79 | + z-index: 2; | ||
80 | + | ||
81 | + &.tb-fullscreen-editor { | ||
82 | + position: relative; | ||
83 | + right: 0; | ||
84 | + .mat-button { | ||
85 | + .mat-icon { | ||
86 | + margin-right: 5px; | ||
87 | + } | ||
88 | + } | ||
89 | + } | ||
90 | + | ||
91 | + .mat-button { | ||
92 | + min-width: 36px; | ||
93 | + padding: 0; | ||
94 | + .mat-icon { | ||
95 | + margin-right: 0; | ||
96 | + } | ||
97 | + } | ||
98 | + } | ||
99 | + | ||
100 | + .tb-custom-action-editor { | ||
101 | + &.tb-fullscreen-editor { | ||
102 | + height: 100%; | ||
103 | + } | ||
104 | + } | ||
105 | +} | ||
106 | + | ||
107 | + |
ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-editor.component.ts
0 → 100644
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 { | ||
18 | + AfterViewInit, | ||
19 | + ChangeDetectionStrategy, | ||
20 | + Component, | ||
21 | + ElementRef, | ||
22 | + forwardRef, | ||
23 | + Input, | ||
24 | + OnDestroy, | ||
25 | + OnInit, | ||
26 | + ViewChild, ViewEncapsulation, ViewChildren, QueryList, ComponentFactoryResolver | ||
27 | +} from '@angular/core'; | ||
28 | +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | ||
29 | +import { TranslateService } from '@ngx-translate/core'; | ||
30 | +import { PageComponent } from '@shared/components/page.component'; | ||
31 | +import { Store } from '@ngrx/store'; | ||
32 | +import { AppState } from '@core/core.state'; | ||
33 | +import { MatDialog } from '@angular/material/dialog'; | ||
34 | +import { DialogService } from '@core/services/dialog.service'; | ||
35 | +import { PageLink } from '@shared/models/page/page-link'; | ||
36 | +import { Direction, SortOrder } from '@shared/models/page/sort-order'; | ||
37 | +import { MatPaginator } from '@angular/material/paginator'; | ||
38 | +import { MatSort } from '@angular/material/sort'; | ||
39 | +import { combineLatest, fromEvent, merge } from 'rxjs'; | ||
40 | +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; | ||
41 | +import { | ||
42 | + WidgetActionDescriptorInfo, | ||
43 | + WidgetActionsData, | ||
44 | + WidgetActionsDatasource, | ||
45 | + WidgetActionCallbacks, toWidgetActionDescriptor | ||
46 | +} from '@home/components/widget/action/manage-widget-actions.component.models'; | ||
47 | +import { UtilsService } from '@core/services/utils.service'; | ||
48 | +import { EntityRelation, EntitySearchDirection, RelationTypeGroup } from '@shared/models/relation.models'; | ||
49 | +import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component'; | ||
50 | +import { CustomActionDescriptor, WidgetActionDescriptor, WidgetActionSource } from '@shared/models/widget.models'; | ||
51 | +import { | ||
52 | + WidgetActionDialogComponent, | ||
53 | + WidgetActionDialogData | ||
54 | +} from '@home/components/widget/action/widget-action-dialog.component'; | ||
55 | +import { deepClone } from '@core/utils'; | ||
56 | +import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; | ||
57 | +import { CustomActionPrettyResourcesTabsComponent } from '@home/components/widget/action/custom-action-pretty-resources-tabs.component'; | ||
58 | +import { MatTab, MatTabGroup } from '@angular/material/tabs'; | ||
59 | + | ||
60 | +@Component({ | ||
61 | + selector: 'tb-custom-action-pretty-editor', | ||
62 | + templateUrl: './custom-action-pretty-editor.component.html', | ||
63 | + styleUrls: ['./custom-action-pretty-editor.component.scss'], | ||
64 | + providers: [ | ||
65 | + { | ||
66 | + provide: NG_VALUE_ACCESSOR, | ||
67 | + useExisting: forwardRef(() => CustomActionPrettyEditorComponent), | ||
68 | + multi: true | ||
69 | + } | ||
70 | + ], | ||
71 | + encapsulation: ViewEncapsulation.None | ||
72 | +}) | ||
73 | +export class CustomActionPrettyEditorComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy, ControlValueAccessor { | ||
74 | + | ||
75 | + @Input() disabled: boolean; | ||
76 | + | ||
77 | + action: CustomActionDescriptor; | ||
78 | + | ||
79 | + fullscreen = false; | ||
80 | + | ||
81 | + @ViewChildren('leftPanel') | ||
82 | + leftPanelElmRef: QueryList<ElementRef<HTMLElement>>; | ||
83 | + | ||
84 | + @ViewChildren('rightPanel') | ||
85 | + rightPanelElmRef: QueryList<ElementRef<HTMLElement>>; | ||
86 | + | ||
87 | + private propagateChange = (_: any) => {}; | ||
88 | + | ||
89 | + constructor(protected store: Store<AppState>) { | ||
90 | + super(store); | ||
91 | + } | ||
92 | + | ||
93 | + ngOnInit(): void { | ||
94 | + } | ||
95 | + | ||
96 | + ngAfterViewInit(): void { | ||
97 | + combineLatest(this.leftPanelElmRef.changes, this.rightPanelElmRef.changes).subscribe(() => { | ||
98 | + if (this.leftPanelElmRef.length && this.rightPanelElmRef.length) { | ||
99 | + this.initSplitLayout(this.leftPanelElmRef.first.nativeElement, | ||
100 | + this.rightPanelElmRef.first.nativeElement); | ||
101 | + } | ||
102 | + }); | ||
103 | + } | ||
104 | + | ||
105 | + private initSplitLayout(leftPanel: any, rightPanel: any) { | ||
106 | + Split([leftPanel, rightPanel], { | ||
107 | + sizes: [50, 50], | ||
108 | + gutterSize: 8, | ||
109 | + cursor: 'col-resize' | ||
110 | + }); | ||
111 | + } | ||
112 | + | ||
113 | + ngOnDestroy(): void { | ||
114 | + } | ||
115 | + | ||
116 | + registerOnChange(fn: any): void { | ||
117 | + this.propagateChange = fn; | ||
118 | + } | ||
119 | + | ||
120 | + registerOnTouched(fn: any): void { | ||
121 | + } | ||
122 | + | ||
123 | + setDisabledState(isDisabled: boolean): void { | ||
124 | + this.disabled = isDisabled; | ||
125 | + } | ||
126 | + | ||
127 | + writeValue(obj: CustomActionDescriptor): void { | ||
128 | + this.action = obj; | ||
129 | + } | ||
130 | + | ||
131 | + public onActionUpdated(valid: boolean = true) { | ||
132 | + if (!valid) { | ||
133 | + this.propagateChange(null); | ||
134 | + } else { | ||
135 | + this.propagateChange(this.action); | ||
136 | + } | ||
137 | + } | ||
138 | +} |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<mat-tab-group selectedIndex="{{hasCustomFunction ? 3 : 2}}" dynamicHeight="true" style="width: 100%; height: 100%;"> | ||
19 | + <mat-tab label="{{ 'widget.resources' | translate }}" style="width: 100%; height: 100%;"> | ||
20 | + <div class="tb-custom-action-editor-container" style="background-color: #fff;"> | ||
21 | + <div class="mat-padding"> | ||
22 | + <div fxFlex fxLayout="row" style="max-height: 40px;" | ||
23 | + fxLayoutAlign="start center" | ||
24 | + *ngFor="let resource of action.customResources; let i = index" > | ||
25 | + <mat-form-field fxFlex class="mat-block resource-field" floatLabel="never" | ||
26 | + style="margin: 10px 0px 0px 0px; max-height: 40px;"> | ||
27 | + <input required matInput [(ngModel)]="resource.url" | ||
28 | + (ngModelChange)="notifyActionUpdated()" | ||
29 | + placeholder="{{ 'widget.resource-url' | translate }}"/> | ||
30 | + </mat-form-field> | ||
31 | + <button mat-button mat-icon-button color="primary" | ||
32 | + [disabled]="isLoading$ | async" | ||
33 | + type="button" | ||
34 | + (click)="removeResource(i)" | ||
35 | + matTooltip="{{'widget.remove-resource' | translate}}" | ||
36 | + matTooltipPosition="above"> | ||
37 | + <mat-icon>close</mat-icon> | ||
38 | + </button> | ||
39 | + </div> | ||
40 | + <div style="margin-top: 6px;"> | ||
41 | + <button mat-button mat-raised-button color="primary" | ||
42 | + [disabled]="isLoading$ | async" | ||
43 | + type="button" | ||
44 | + (click)="addResource()" | ||
45 | + matTooltip="{{'widget.add-resource' | translate}}" | ||
46 | + matTooltipPosition="above"> | ||
47 | + <span translate>action.add</span> | ||
48 | + </button> | ||
49 | + </div> | ||
50 | + </div> | ||
51 | + </div> | ||
52 | + </mat-tab> | ||
53 | + <mat-tab label="{{ 'widget.css' | translate }}" style="width: 100%; height: 100%;"> | ||
54 | + <div class="tb-custom-action-editor-container" tb-fullscreen [fullscreen]="cssFullscreen"> | ||
55 | + <div class="tb-editor-area-title-panel"> | ||
56 | + <button mat-button | ||
57 | + type="button" | ||
58 | + (click)="beautifyCss()"> | ||
59 | + {{ 'widget.tidy' | translate }} | ||
60 | + </button> | ||
61 | + <button mat-button | ||
62 | + type="button" | ||
63 | + mat-icon-button class="tb-mat-32" | ||
64 | + (click)="cssFullscreen = !cssFullscreen" | ||
65 | + matTooltip="{{(cssFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" | ||
66 | + matTooltipPosition="above"> | ||
67 | + <mat-icon>{{ cssFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> | ||
68 | + </button> | ||
69 | + </div> | ||
70 | + <div #cssInput class="css-panel"></div> | ||
71 | + </div> | ||
72 | + </mat-tab> | ||
73 | + <mat-tab label="{{ 'widget.html' | translate }}" style="width: 100%; height: 100%;"> | ||
74 | + <div class="tb-custom-action-editor-container" tb-fullscreen [fullscreen]="htmlFullscreen"> | ||
75 | + <div class="tb-editor-area-title-panel"> | ||
76 | + <button mat-button | ||
77 | + type="button" (click)="beautifyHtml()"> | ||
78 | + {{ 'widget.tidy' | translate }} | ||
79 | + </button> | ||
80 | + <button mat-button | ||
81 | + type="button" | ||
82 | + mat-icon-button class="tb-mat-32" | ||
83 | + (click)="htmlFullscreen = !htmlFullscreen" | ||
84 | + matTooltip="{{(htmlFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" | ||
85 | + matTooltipPosition="above"> | ||
86 | + <mat-icon>{{ htmlFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> | ||
87 | + </button> | ||
88 | + </div> | ||
89 | + <div #htmlInput class="html-panel"></div> | ||
90 | + </div> | ||
91 | + </mat-tab> | ||
92 | + <mat-tab *ngIf="hasCustomFunction" label="{{ 'widget.js' | translate }}" style="width: 100%; height: 100%;"> | ||
93 | + <tb-js-func | ||
94 | + [(ngModel)]="action.customFunction" | ||
95 | + (ngModelChange)="notifyActionUpdated()" | ||
96 | + [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams']" | ||
97 | + [validationArgs]="[]"> | ||
98 | + </tb-js-func> | ||
99 | + </mat-tab> | ||
100 | +</mat-tab-group> |
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 | +.tb-custom-action-editor-container { | ||
17 | + | ||
18 | + mat-form-field.resource-field { | ||
19 | + max-height: 40px; | ||
20 | + margin: 10px 0px 0px 0px; | ||
21 | + .mat-form-field-wrapper { | ||
22 | + padding-bottom: 0; | ||
23 | + .mat-form-field-flex { | ||
24 | + max-height: 40px; | ||
25 | + .mat-form-field-infix { | ||
26 | + border: 0; | ||
27 | + } | ||
28 | + } | ||
29 | + .mat-form-field-underline { | ||
30 | + bottom: 0; | ||
31 | + } | ||
32 | + } | ||
33 | + } | ||
34 | + | ||
35 | + .html-panel, | ||
36 | + .css-panel { | ||
37 | + width: 100%; | ||
38 | + min-width: 200px; | ||
39 | + height: 100%; | ||
40 | + min-height: 200px; | ||
41 | + } | ||
42 | + | ||
43 | + div.tb-editor-area-title-panel { | ||
44 | + position: absolute; | ||
45 | + top: 5px; | ||
46 | + right: 20px; | ||
47 | + z-index: 5; | ||
48 | + font-size: .8rem; | ||
49 | + font-weight: 500; | ||
50 | + | ||
51 | + label { | ||
52 | + padding: 4px; | ||
53 | + color: #00acc1; | ||
54 | + text-transform: uppercase; | ||
55 | + background: rgba(220, 220, 220, .35); | ||
56 | + border-radius: 5px; | ||
57 | + &:not(:last-child) { | ||
58 | + margin-right: 4px; | ||
59 | + } | ||
60 | + } | ||
61 | + | ||
62 | + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 { | ||
63 | + align-items: center; | ||
64 | + vertical-align: middle; | ||
65 | + min-width: 32px; | ||
66 | + min-height: 15px; | ||
67 | + padding: 4px; | ||
68 | + margin: 0; | ||
69 | + font-size: .8rem; | ||
70 | + line-height: 15px; | ||
71 | + color: #7b7b7b; | ||
72 | + background: rgba(220, 220, 220, .35); | ||
73 | + &:not(:last-child) { | ||
74 | + margin-right: 4px; | ||
75 | + } | ||
76 | + } | ||
77 | + } | ||
78 | + | ||
79 | +} |
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 { | ||
18 | + Component, | ||
19 | + ElementRef, | ||
20 | + EventEmitter, | ||
21 | + Input, | ||
22 | + OnChanges, | ||
23 | + OnDestroy, | ||
24 | + OnInit, | ||
25 | + Output, QueryList, | ||
26 | + SimpleChanges, | ||
27 | + ViewChild, ViewChildren, ViewEncapsulation | ||
28 | +} from '@angular/core'; | ||
29 | +import { TranslateService } from '@ngx-translate/core'; | ||
30 | +import { PageComponent } from '@shared/components/page.component'; | ||
31 | +import { Store } from '@ngrx/store'; | ||
32 | +import { AppState } from '@core/core.state'; | ||
33 | +import { CustomActionDescriptor } from '@shared/models/widget.models'; | ||
34 | +import * as ace from 'ace-builds'; | ||
35 | +import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; | ||
36 | +import { css_beautify, html_beautify } from 'js-beautify'; | ||
37 | +import { MatTab } from '@angular/material/tabs'; | ||
38 | +import { BehaviorSubject } from 'rxjs'; | ||
39 | + | ||
40 | +@Component({ | ||
41 | + selector: 'tb-custom-action-pretty-resources-tabs', | ||
42 | + templateUrl: './custom-action-pretty-resources-tabs.component.html', | ||
43 | + styleUrls: ['./custom-action-pretty-resources-tabs.component.scss'], | ||
44 | + encapsulation: ViewEncapsulation.None | ||
45 | +}) | ||
46 | +export class CustomActionPrettyResourcesTabsComponent extends PageComponent implements OnInit, OnChanges, OnDestroy { | ||
47 | + | ||
48 | + @Input() | ||
49 | + action: CustomActionDescriptor; | ||
50 | + | ||
51 | + @Input() | ||
52 | + hasCustomFunction: boolean; | ||
53 | + | ||
54 | + @Output() | ||
55 | + actionUpdated: EventEmitter<CustomActionDescriptor> = new EventEmitter<CustomActionDescriptor>(); | ||
56 | + | ||
57 | + @ViewChild('htmlInput', {static: true}) | ||
58 | + htmlInputElmRef: ElementRef; | ||
59 | + | ||
60 | + @ViewChild('cssInput', {static: true}) | ||
61 | + cssInputElmRef: ElementRef; | ||
62 | + | ||
63 | + htmlFullscreen = false; | ||
64 | + cssFullscreen = false; | ||
65 | + | ||
66 | + aceEditors: ace.Ace.Editor[] = []; | ||
67 | + editorsResizeCafs: {[editorId: string]: CancelAnimationFrame} = {}; | ||
68 | + aceResizeListeners: { element: any, resizeListener: any }[] = []; | ||
69 | + htmlEditor: ace.Ace.Editor; | ||
70 | + cssEditor: ace.Ace.Editor; | ||
71 | + setValuesPending = false; | ||
72 | + | ||
73 | + constructor(protected store: Store<AppState>, | ||
74 | + private translate: TranslateService, | ||
75 | + private raf: RafService) { | ||
76 | + super(store); | ||
77 | + } | ||
78 | + | ||
79 | + ngOnInit(): void { | ||
80 | + this.initAceEditors(); | ||
81 | + if (this.setValuesPending) { | ||
82 | + this.setAceEditorValues(); | ||
83 | + this.setValuesPending = false; | ||
84 | + } | ||
85 | + } | ||
86 | + | ||
87 | + ngOnDestroy(): void { | ||
88 | + this.aceResizeListeners.forEach((resizeListener) => { | ||
89 | + // @ts-ignore | ||
90 | + removeResizeListener(resizeListener.element, resizeListener.resizeListener); | ||
91 | + }); | ||
92 | + } | ||
93 | + | ||
94 | + ngOnChanges(changes: SimpleChanges): void { | ||
95 | + for (const propName of Object.keys(changes)) { | ||
96 | + const change = changes[propName]; | ||
97 | + if (propName === 'action') { | ||
98 | + if (this.aceEditors.length) { | ||
99 | + this.setAceEditorValues(); | ||
100 | + } else { | ||
101 | + this.setValuesPending = true; | ||
102 | + } | ||
103 | + } | ||
104 | + } | ||
105 | + } | ||
106 | + | ||
107 | + public notifyActionUpdated() { | ||
108 | + this.actionUpdated.emit(this.validate() ? this.action : null); | ||
109 | + } | ||
110 | + | ||
111 | + private validate(): boolean { | ||
112 | + for (const resource of this.action.customResources) { | ||
113 | + if (!resource.url) { | ||
114 | + return false; | ||
115 | + } | ||
116 | + } | ||
117 | + return true; | ||
118 | + } | ||
119 | + | ||
120 | + public addResource() { | ||
121 | + this.action.customResources.push({url: ''}); | ||
122 | + this.notifyActionUpdated(); | ||
123 | + } | ||
124 | + | ||
125 | + public removeResource(index: number) { | ||
126 | + if (index > -1) { | ||
127 | + if (this.action.customResources.splice(index, 1).length > 0) { | ||
128 | + this.notifyActionUpdated(); | ||
129 | + } | ||
130 | + } | ||
131 | + } | ||
132 | + | ||
133 | + public beautifyCss(): void { | ||
134 | + const res = css_beautify(this.action.customCss, {indent_size: 4}); | ||
135 | + if (this.action.customCss !== res) { | ||
136 | + this.action.customCss = res; | ||
137 | + this.cssEditor.setValue(this.action.customCss ? this.action.customCss : '', -1); | ||
138 | + this.notifyActionUpdated(); | ||
139 | + } | ||
140 | + } | ||
141 | + | ||
142 | + public beautifyHtml(): void { | ||
143 | + const res = html_beautify(this.action.customHtml, {indent_size: 4, wrap_line_length: 60}); | ||
144 | + if (this.action.customHtml !== res) { | ||
145 | + this.action.customHtml = res; | ||
146 | + this.htmlEditor.setValue(this.action.customHtml ? this.action.customHtml : '', -1); | ||
147 | + this.notifyActionUpdated(); | ||
148 | + } | ||
149 | + } | ||
150 | + | ||
151 | + private initAceEditors() { | ||
152 | + this.htmlEditor = this.createAceEditor(this.htmlInputElmRef, 'html'); | ||
153 | + this.htmlEditor.on('input', () => { | ||
154 | + const editorValue = this.htmlEditor.getValue(); | ||
155 | + if (this.action.customHtml !== editorValue) { | ||
156 | + this.action.customHtml = editorValue; | ||
157 | + this.notifyActionUpdated(); | ||
158 | + } | ||
159 | + }); | ||
160 | + this.cssEditor = this.createAceEditor(this.cssInputElmRef, 'css'); | ||
161 | + this.cssEditor.on('input', () => { | ||
162 | + const editorValue = this.cssEditor.getValue(); | ||
163 | + if (this.action.customCss !== editorValue) { | ||
164 | + this.action.customCss = editorValue; | ||
165 | + this.notifyActionUpdated(); | ||
166 | + } | ||
167 | + }); | ||
168 | + } | ||
169 | + | ||
170 | + private createAceEditor(editorElementRef: ElementRef, mode: string): ace.Ace.Editor { | ||
171 | + const editorElement = editorElementRef.nativeElement; | ||
172 | + let editorOptions: Partial<ace.Ace.EditorOptions> = { | ||
173 | + mode: `ace/mode/${mode}`, | ||
174 | + showGutter: true, | ||
175 | + showPrintMargin: true | ||
176 | + }; | ||
177 | + const advancedOptions = { | ||
178 | + enableSnippets: true, | ||
179 | + enableBasicAutocompletion: true, | ||
180 | + enableLiveAutocompletion: true | ||
181 | + }; | ||
182 | + editorOptions = {...editorOptions, ...advancedOptions}; | ||
183 | + const aceEditor = ace.edit(editorElement, editorOptions); | ||
184 | + aceEditor.session.setUseWrapMode(true); | ||
185 | + this.aceEditors.push(aceEditor); | ||
186 | + | ||
187 | + const resizeListener = this.onAceEditorResize.bind(this, aceEditor); | ||
188 | + | ||
189 | + // @ts-ignore | ||
190 | + addResizeListener(editorElement, resizeListener); | ||
191 | + this.aceResizeListeners.push({element: editorElement, resizeListener}); | ||
192 | + return aceEditor; | ||
193 | + } | ||
194 | + | ||
195 | + private setAceEditorValues() { | ||
196 | + this.htmlEditor.setValue(this.action.customHtml ? this.action.customHtml : '', -1); | ||
197 | + this.cssEditor.setValue(this.action.customCss ? this.action.customCss : '', -1); | ||
198 | + } | ||
199 | + | ||
200 | + private onAceEditorResize(aceEditor: ace.Ace.Editor) { | ||
201 | + if (this.editorsResizeCafs[aceEditor.id]) { | ||
202 | + this.editorsResizeCafs[aceEditor.id](); | ||
203 | + delete this.editorsResizeCafs[aceEditor.id]; | ||
204 | + } | ||
205 | + this.editorsResizeCafs[aceEditor.id] = this.raf.raf(() => { | ||
206 | + aceEditor.resize(); | ||
207 | + aceEditor.renderer.updateFull(); | ||
208 | + }); | ||
209 | + } | ||
210 | + | ||
211 | +} |
1 | +/*=======================================================================*/ | ||
2 | +/*========== There are two examples: for edit and add entity ==========*/ | ||
3 | +/*=======================================================================*/ | ||
4 | +/*======================== Edit entity example ========================*/ | ||
5 | +/*=======================================================================*/ | ||
6 | +/* | ||
7 | +.edit-entity-form md-input-container { | ||
8 | + padding-right: 10px; | ||
9 | +} | ||
10 | + | ||
11 | +.edit-entity-form .boolean-value-input { | ||
12 | + padding-left: 5px; | ||
13 | +} | ||
14 | + | ||
15 | +.edit-entity-form .boolean-value-input .checkbox-label { | ||
16 | + margin-bottom: 8px; | ||
17 | + color: rgba(0,0,0,0.54); | ||
18 | + font-size: 12px; | ||
19 | +} | ||
20 | + | ||
21 | +.relations-list .header { | ||
22 | + padding-right: 5px; | ||
23 | + padding-bottom: 5px; | ||
24 | + padding-left: 5px; | ||
25 | +} | ||
26 | + | ||
27 | +.relations-list .header .cell { | ||
28 | + padding-right: 5px; | ||
29 | + padding-left: 5px; | ||
30 | + font-size: 12px; | ||
31 | + font-weight: 700; | ||
32 | + color: rgba(0, 0, 0, .54); | ||
33 | + white-space: nowrap; | ||
34 | +} | ||
35 | + | ||
36 | +.relations-list .body { | ||
37 | + padding-right: 5px; | ||
38 | + padding-bottom: 15px; | ||
39 | + padding-left: 5px; | ||
40 | +} | ||
41 | + | ||
42 | +.relations-list .body .row { | ||
43 | + padding-top: 5px; | ||
44 | +} | ||
45 | + | ||
46 | +.relations-list .body .cell { | ||
47 | + padding-right: 5px; | ||
48 | + padding-left: 5px; | ||
49 | +} | ||
50 | + | ||
51 | +.relations-list .body md-autocomplete-wrap md-input-container { | ||
52 | + height: 30px; | ||
53 | +} | ||
54 | + | ||
55 | +.relations-list .body .md-button { | ||
56 | + margin: 0; | ||
57 | +} | ||
58 | + | ||
59 | +.relations-list.old-relations tb-entity-select tb-entity-autocomplete button { | ||
60 | + display: none; | ||
61 | +} | ||
62 | +*/ | ||
63 | +/*========================================================================*/ | ||
64 | +/*========================= Add entity example =========================*/ | ||
65 | +/*========================================================================*/ | ||
66 | +/* | ||
67 | +.add-entity-form md-input-container { | ||
68 | + padding-right: 10px; | ||
69 | +} | ||
70 | + | ||
71 | +.add-entity-form .boolean-value-input { | ||
72 | + padding-left: 5px; | ||
73 | +} | ||
74 | + | ||
75 | +.add-entity-form .boolean-value-input .checkbox-label { | ||
76 | + margin-bottom: 8px; | ||
77 | + color: rgba(0,0,0,0.54); | ||
78 | + font-size: 12px; | ||
79 | +} | ||
80 | + | ||
81 | +.relations-list .header { | ||
82 | + padding-right: 5px; | ||
83 | + padding-bottom: 5px; | ||
84 | + padding-left: 5px; | ||
85 | +} | ||
86 | + | ||
87 | +.relations-list .header .cell { | ||
88 | + padding-right: 5px; | ||
89 | + padding-left: 5px; | ||
90 | + font-size: 12px; | ||
91 | + font-weight: 700; | ||
92 | + color: rgba(0, 0, 0, .54); | ||
93 | + white-space: nowrap; | ||
94 | +} | ||
95 | + | ||
96 | +.relations-list .body { | ||
97 | + padding-right: 5px; | ||
98 | + padding-bottom: 15px; | ||
99 | + padding-left: 5px; | ||
100 | +} | ||
101 | + | ||
102 | +.relations-list .body .row { | ||
103 | + padding-top: 5px; | ||
104 | +} | ||
105 | + | ||
106 | +.relations-list .body .cell { | ||
107 | + padding-right: 5px; | ||
108 | + padding-left: 5px; | ||
109 | +} | ||
110 | + | ||
111 | +.relations-list .body md-autocomplete-wrap md-input-container { | ||
112 | + height: 30px; | ||
113 | +} | ||
114 | + | ||
115 | +.relations-list .body .md-button { | ||
116 | + margin: 0; | ||
117 | +} | ||
118 | +*/ |
1 | +<!--=======================================================================--> | ||
2 | +<!--===== There are two example templates: for edit and add entity =====--> | ||
3 | +<!--=======================================================================--> | ||
4 | +<!--======================== Edit entity example ========================--> | ||
5 | +<!--=======================================================================--> | ||
6 | +<!-- --> | ||
7 | +<!--<md-dialog aria-label="Edit entity">--> | ||
8 | +<!-- <form name="editEntityForm" class="edit-entity-form" ng-submit="vm.save()">--> | ||
9 | +<!-- <md-toolbar>--> | ||
10 | +<!-- <div class="md-toolbar-tools">--> | ||
11 | +<!-- <h2>Edit {{vm.entityType.toLowerCase()}} {{vm.entityName}}</h2>--> | ||
12 | +<!-- <span flex></span>--> | ||
13 | +<!-- <md-button class="md-icon-button" ng-click="vm.cancel()">--> | ||
14 | +<!-- <ng-md-icon icon="close" aria-label="Close"></ng-md-icon>--> | ||
15 | +<!-- </md-button>--> | ||
16 | +<!-- </div>--> | ||
17 | +<!-- </md-toolbar>--> | ||
18 | +<!-- <md-dialog-content>--> | ||
19 | +<!-- <div class="md-dialog-content">--> | ||
20 | +<!-- <div layout="row">--> | ||
21 | +<!-- <md-input-container flex class="md-block">--> | ||
22 | +<!-- <label>Entity Name</label>--> | ||
23 | +<!-- <input ng-model="vm.entityName" readonly>--> | ||
24 | +<!-- </md-input-container>--> | ||
25 | +<!-- <md-input-container flex class="md-block">--> | ||
26 | +<!-- <label>Entity Type</label>--> | ||
27 | +<!-- <input ng-model="vm.entityType" readonly>--> | ||
28 | +<!-- </md-input-container>--> | ||
29 | +<!-- <md-input-container flex class="md-block">--> | ||
30 | +<!-- <label>Type</label>--> | ||
31 | +<!-- <input ng-model="vm.type" readonly>--> | ||
32 | +<!-- </md-input-container>--> | ||
33 | +<!-- </div>--> | ||
34 | +<!-- <div layout="row">--> | ||
35 | +<!-- <md-input-container flex class="md-block">--> | ||
36 | +<!-- <label>Latitude</label>--> | ||
37 | +<!-- <input name="latitude" type="number" step="any" ng-model="vm.attributes.latitude">--> | ||
38 | +<!-- </md-input-container>--> | ||
39 | +<!-- <md-input-container flex class="md-block">--> | ||
40 | +<!-- <label>Longitude</label>--> | ||
41 | +<!-- <input name="longitude" type="number" step="any" ng-model="vm.attributes.longitude">--> | ||
42 | +<!-- </md-input-container>--> | ||
43 | +<!-- </div>--> | ||
44 | +<!-- <div layout="row">--> | ||
45 | +<!-- <md-input-container flex class="md-block">--> | ||
46 | +<!-- <label>Address</label>--> | ||
47 | +<!-- <input ng-model="vm.attributes.address">--> | ||
48 | +<!-- </md-input-container>--> | ||
49 | +<!-- <md-input-container flex class="md-block">--> | ||
50 | +<!-- <label>Owner</label>--> | ||
51 | +<!-- <input ng-model="vm.attributes.owner">--> | ||
52 | +<!-- </md-input-container>--> | ||
53 | +<!-- </div>--> | ||
54 | +<!-- <div layout="row">--> | ||
55 | +<!-- <md-input-container flex class="md-block">--> | ||
56 | +<!-- <label>Integer Value</label>--> | ||
57 | +<!-- <input name="integerNumber" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="vm.attributes.number">--> | ||
58 | +<!-- <div ng-messages="editEntityForm.integerNumber.$error">--> | ||
59 | +<!-- <div ng-message="pattern">Invalid integer value.</div>--> | ||
60 | +<!-- </div>--> | ||
61 | +<!-- </md-input-container>--> | ||
62 | +<!-- <div class="boolean-value-input" layout="column" layout-align="center start" flex>--> | ||
63 | +<!-- <label class="checkbox-label">Boolean Value</label>--> | ||
64 | +<!-- <md-checkbox ng-model="vm.attributes.booleanValue" style="margin-bottom: 40px;">--> | ||
65 | +<!-- {{ (vm.attributes.booleanValue ? "value.true" : "value.false") | translate }}--> | ||
66 | +<!-- </md-checkbox>--> | ||
67 | +<!-- </div>--> | ||
68 | +<!-- </div>--> | ||
69 | +<!-- <div class="relations-list old-relations">--> | ||
70 | +<!-- <div class="md-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div>--> | ||
71 | +<!-- <div class="body" ng-show="vm.relations.length">--> | ||
72 | +<!-- <div class="row" layout="row" layout-align="start center" ng-repeat="relation in vm.relations track by $index">--> | ||
73 | +<!-- <div class="md-whiteframe-1dp" flex layout="row" style="padding-left: 5px; margin-bottom: 3px;">--> | ||
74 | +<!-- <div flex layout="column">--> | ||
75 | +<!-- <div layout="row">--> | ||
76 | +<!-- <md-input-container class="md-block" style="min-width: 100px;">--> | ||
77 | +<!-- <label>Direction</label>--> | ||
78 | +<!-- <md-select ng-disabled="true" required ng-model="relation.direction">--> | ||
79 | +<!-- <md-option ng-repeat="direction in vm.entitySearchDirection" ng-value="direction">--> | ||
80 | +<!-- {{ ("relation.search-direction." + direction) | translate}}--> | ||
81 | +<!-- </md-option>--> | ||
82 | +<!-- </md-select>--> | ||
83 | +<!-- </md-input-container>--> | ||
84 | +<!-- <tb-relation-type-autocomplete ng-disabled="true" flex class="md-block"--> | ||
85 | +<!-- the-form="editEntityForm"--> | ||
86 | +<!-- ng-model="relation.relationType"--> | ||
87 | +<!-- tb-required="true">--> | ||
88 | +<!-- </tb-relation-type-autocomplete>--> | ||
89 | +<!-- </div>--> | ||
90 | +<!-- <div layout="row">--> | ||
91 | +<!-- <tb-entity-select flex class="md-block"--> | ||
92 | +<!-- the-form="editEntityForm"--> | ||
93 | +<!-- ng-disabled="true"--> | ||
94 | +<!-- tb-required="true"--> | ||
95 | +<!-- ng-model="relation.relatedEntity">--> | ||
96 | +<!-- </tb-entity-select>--> | ||
97 | +<!-- </div>--> | ||
98 | +<!-- </div>--> | ||
99 | +<!-- <div layout="column" layout-align="center center">--> | ||
100 | +<!-- <md-button class="md-icon-button md-primary" style="width: 40px; min-width: 40px;"--> | ||
101 | +<!-- ng-click="vm.removeOldRelation($index,relation)" aria-label="Remove">--> | ||
102 | +<!-- <md-tooltip md-direction="top">Remove relation</md-tooltip>--> | ||
103 | +<!-- <md-icon aria-label="Remove" class="material-icons">--> | ||
104 | +<!-- close--> | ||
105 | +<!-- </md-icon>--> | ||
106 | +<!-- </md-button>--> | ||
107 | +<!-- </div>--> | ||
108 | +<!-- </div>--> | ||
109 | +<!-- </div>--> | ||
110 | +<!-- </div>--> | ||
111 | +<!-- </div>--> | ||
112 | +<!-- <div class="relations-list">--> | ||
113 | +<!-- <div class="md-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">New Relations</div>--> | ||
114 | +<!-- <div class="body" ng-show="vm.newRelations.length">--> | ||
115 | +<!-- <div class="row" layout="row" layout-align="start center" ng-repeat="relation in vm.newRelations track by $index">--> | ||
116 | +<!-- <div class="md-whiteframe-1dp" flex layout="row" style="padding-left: 5px; margin-bottom: 3px;">--> | ||
117 | +<!-- <div flex layout="column">--> | ||
118 | +<!-- <div layout="row">--> | ||
119 | +<!-- <md-input-container class="md-block" style="min-width: 100px;">--> | ||
120 | +<!-- <label>Direction</label>--> | ||
121 | +<!-- <md-select name="direction" required ng-model="relation.direction">--> | ||
122 | +<!-- <md-option ng-repeat="direction in vm.entitySearchDirection" ng-value="direction">--> | ||
123 | +<!-- {{ ("relation.search-direction." + direction) | translate}}--> | ||
124 | +<!-- </md-option>--> | ||
125 | +<!-- </md-select>--> | ||
126 | +<!-- <div ng-messages="editEntityForm.direction.$error">--> | ||
127 | +<!-- <div ng-message="required">Relation direction is required.</div>--> | ||
128 | +<!-- </div>--> | ||
129 | +<!-- </md-input-container>--> | ||
130 | +<!-- <tb-relation-type-autocomplete flex class="md-block"--> | ||
131 | +<!-- the-form="editEntityForm"--> | ||
132 | +<!-- ng-model="relation.relationType"--> | ||
133 | +<!-- tb-required="true">--> | ||
134 | +<!-- </tb-relation-type-autocomplete>--> | ||
135 | +<!-- </div>--> | ||
136 | +<!-- <div layout="row">--> | ||
137 | +<!-- <tb-entity-select flex class="md-block"--> | ||
138 | +<!-- the-form="editEntityForm"--> | ||
139 | +<!-- tb-required="true"--> | ||
140 | +<!-- ng-model="relation.relatedEntity">--> | ||
141 | +<!-- </tb-entity-select>--> | ||
142 | +<!-- </div>--> | ||
143 | +<!-- </div>--> | ||
144 | +<!-- <div layout="column" layout-align="center center">--> | ||
145 | +<!-- <md-button class="md-icon-button md-primary" style="width: 40px; min-width: 40px;"--> | ||
146 | +<!-- ng-click="vm.removeRelation($index)" aria-label="Remove">--> | ||
147 | +<!-- <md-tooltip md-direction="top">Remove relation</md-tooltip>--> | ||
148 | +<!-- <md-icon aria-label="Remove" class="material-icons">--> | ||
149 | +<!-- close--> | ||
150 | +<!-- </md-icon>--> | ||
151 | +<!-- </md-button>--> | ||
152 | +<!-- </div>--> | ||
153 | +<!-- </div>--> | ||
154 | +<!-- </div>--> | ||
155 | +<!-- </div>--> | ||
156 | +<!-- <div>--> | ||
157 | +<!-- <md-button class="md-primary md-raised" ng-click="vm.addRelation()" aria-label="Add">--> | ||
158 | +<!-- <md-tooltip md-direction="top">Add Relation</md-tooltip>--> | ||
159 | +<!-- Add--> | ||
160 | +<!-- </md-button>--> | ||
161 | +<!-- </div> --> | ||
162 | +<!-- </div>--> | ||
163 | +<!-- </div>--> | ||
164 | +<!-- </md-dialog-content>--> | ||
165 | +<!-- <md-dialog-actions>--> | ||
166 | +<!-- <md-button type="submit" ng-disabled="editEntityForm.$invalid || !editEntityForm.$dirty" class="md-raised md-primary">Save</md-button>--> | ||
167 | +<!-- <md-button ng-click="vm.cancel()" class="md-primary">Cancel</md-button>--> | ||
168 | +<!-- </md-dialog-actions>--> | ||
169 | +<!-- </form>--> | ||
170 | +<!--</md-dialog>--> | ||
171 | +<!----> | ||
172 | +<!--========================================================================--> | ||
173 | +<!--========================= Add entity example =========================--> | ||
174 | +<!--========================================================================--> | ||
175 | +<!----> | ||
176 | +<!--<md-dialog aria-label="Add entity">--> | ||
177 | +<!-- <form name="addEntityForm" class="add-entity-form" ng-submit="vm.save()">--> | ||
178 | +<!-- <md-toolbar>--> | ||
179 | +<!-- <div class="md-toolbar-tools">--> | ||
180 | +<!-- <h2>Add entity</h2>--> | ||
181 | +<!-- <span flex></span>--> | ||
182 | +<!-- <md-button class="md-icon-button" ng-click="vm.cancel()">--> | ||
183 | +<!-- <ng-md-icon icon="close" aria-label="Close"></ng-md-icon>--> | ||
184 | +<!-- </md-button>--> | ||
185 | +<!-- </div>--> | ||
186 | +<!-- </md-toolbar>--> | ||
187 | +<!-- <md-dialog-content>--> | ||
188 | +<!-- <div class="md-dialog-content">--> | ||
189 | +<!-- <div layout="row">--> | ||
190 | +<!-- <md-input-container flex class="md-block">--> | ||
191 | +<!-- <label>Entity Name</label>--> | ||
192 | +<!-- <input ng-model="vm.entityName" name=entityName required>--> | ||
193 | +<!-- <div ng-messages="addEntityForm.entityName.$error">--> | ||
194 | +<!-- <div ng-message="required">Entity name is required.</div>--> | ||
195 | +<!-- </div>--> | ||
196 | +<!-- </md-input-container>--> | ||
197 | +<!-- <tb-entity-type-select class="md-block" style="min-width: 100px; width: 100px;"--> | ||
198 | +<!-- the-form="addEntityForm"--> | ||
199 | +<!-- tb-required="true"--> | ||
200 | +<!-- allowed-entity-types="vm.allowedEntityTypes"--> | ||
201 | +<!-- ng-model="vm.entityType">--> | ||
202 | +<!-- </tb-entity-type-select>--> | ||
203 | +<!-- <md-input-container flex class="md-block">--> | ||
204 | +<!-- <label>Entity Subtype</label>--> | ||
205 | +<!-- <input ng-model="vm.type" name=type required>--> | ||
206 | +<!-- <div ng-messages="addEntityForm.type.$error">--> | ||
207 | +<!-- <div ng-message="required">Entity subtype is required.</div>--> | ||
208 | +<!-- </div>--> | ||
209 | +<!-- </md-input-container>--> | ||
210 | +<!-- </div>--> | ||
211 | +<!-- <div layout="row">--> | ||
212 | +<!-- <md-input-container flex class="md-block">--> | ||
213 | +<!-- <label>Latitude</label>--> | ||
214 | +<!-- <input name="latitude" type="number" step="any" ng-model="vm.attributes.latitude">--> | ||
215 | +<!-- </md-input-container>--> | ||
216 | +<!-- <md-input-container flex class="md-block">--> | ||
217 | +<!-- <label>Longitude</label>--> | ||
218 | +<!-- <input name="longitude" type="number" step="any" ng-model="vm.attributes.longitude">--> | ||
219 | +<!-- </md-input-container>--> | ||
220 | +<!-- </div>--> | ||
221 | +<!-- <div layout="row">--> | ||
222 | +<!-- <md-input-container flex class="md-block">--> | ||
223 | +<!-- <label>Address</label>--> | ||
224 | +<!-- <input ng-model="vm.attributes.address">--> | ||
225 | +<!-- </md-input-container>--> | ||
226 | +<!-- <md-input-container flex class="md-block">--> | ||
227 | +<!-- <label>Owner</label>--> | ||
228 | +<!-- <input ng-model="vm.attributes.owner">--> | ||
229 | +<!-- </md-input-container>--> | ||
230 | +<!-- </div>--> | ||
231 | +<!-- <div layout="row">--> | ||
232 | +<!-- <md-input-container flex class="md-block">--> | ||
233 | +<!-- <label>Integer Value</label>--> | ||
234 | +<!-- <input name="integerNumber" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="vm.attributes.number">--> | ||
235 | +<!-- <div ng-messages="addEntityForm.integerNumber.$error">--> | ||
236 | +<!-- <div ng-message="pattern">Invalid integer value.</div>--> | ||
237 | +<!-- </div>--> | ||
238 | +<!-- </md-input-container>--> | ||
239 | +<!-- <div class="boolean-value-input" layout="column" layout-align="center start" flex>--> | ||
240 | +<!-- <label class="checkbox-label">Boolean Value</label>--> | ||
241 | +<!-- <md-checkbox ng-model="vm.attributes.booleanValue" style="margin-bottom: 40px;">--> | ||
242 | +<!-- {{ (vm.attributes.booleanValue ? "value.true" : "value.false") | translate }}--> | ||
243 | +<!-- </md-checkbox>--> | ||
244 | +<!-- </div>--> | ||
245 | +<!-- </div>--> | ||
246 | +<!-- <div class="relations-list">--> | ||
247 | +<!-- <div class="md-body-1" style="padding-bottom: 10px; color: rgba(0,0,0,0.57);">Relations</div>--> | ||
248 | +<!-- <div class="body" ng-show="vm.relations.length">--> | ||
249 | +<!-- <div class="row" layout="row" layout-align="start center" ng-repeat="relation in vm.relations track by $index">--> | ||
250 | +<!-- <div class="md-whiteframe-1dp" flex layout="row" style="padding-left: 5px;">--> | ||
251 | +<!-- <div flex layout="column">--> | ||
252 | +<!-- <div layout="row">--> | ||
253 | +<!-- <md-input-container class="md-block" style="min-width: 100px;">--> | ||
254 | +<!-- <label>Direction</label>--> | ||
255 | +<!-- <md-select name="direction" required ng-model="relation.direction">--> | ||
256 | +<!-- <md-option ng-repeat="direction in vm.entitySearchDirection" ng-value="direction">--> | ||
257 | +<!-- {{ ("relation.search-direction." + direction) | translate}}--> | ||
258 | +<!-- </md-option>--> | ||
259 | +<!-- </md-select>--> | ||
260 | +<!-- <div ng-messages="addEntityForm.direction.$error">--> | ||
261 | +<!-- <div ng-message="required">Relation direction is required.</div>--> | ||
262 | +<!-- </div>--> | ||
263 | +<!-- </md-input-container>--> | ||
264 | +<!-- <tb-relation-type-autocomplete flex class="md-block"--> | ||
265 | +<!-- the-form="addEntityForm"--> | ||
266 | +<!-- ng-model="relation.relationType"--> | ||
267 | +<!-- tb-required="true">--> | ||
268 | +<!-- </tb-relation-type-autocomplete>--> | ||
269 | +<!-- </div>--> | ||
270 | +<!-- <div layout="row">--> | ||
271 | +<!-- <tb-entity-select flex class="md-block"--> | ||
272 | +<!-- the-form="addEntityForm"--> | ||
273 | +<!-- tb-required="true"--> | ||
274 | +<!-- ng-model="relation.relatedEntity">--> | ||
275 | +<!-- </tb-entity-select>--> | ||
276 | +<!-- </div>--> | ||
277 | +<!-- </div>--> | ||
278 | +<!-- <div layout="column" layout-align="center center">--> | ||
279 | +<!-- <md-button class="md-icon-button md-primary" style="width: 40px; min-width: 40px;"--> | ||
280 | +<!-- ng-click="vm.removeRelation($index)" aria-label="Remove">--> | ||
281 | +<!-- <md-tooltip md-direction="top">Remove relation</md-tooltip>--> | ||
282 | +<!-- <md-icon aria-label="Remove" class="material-icons">--> | ||
283 | +<!-- close--> | ||
284 | +<!-- </md-icon>--> | ||
285 | +<!-- </md-button>--> | ||
286 | +<!-- </div>--> | ||
287 | +<!-- </div>--> | ||
288 | +<!-- </div>--> | ||
289 | +<!-- </div>--> | ||
290 | +<!-- <div>--> | ||
291 | +<!-- <md-button class="md-primary md-raised" ng-click="vm.addRelation()" aria-label="Add">--> | ||
292 | +<!-- <md-tooltip md-direction="top">Add Relation</md-tooltip>--> | ||
293 | +<!-- Add--> | ||
294 | +<!-- </md-button>--> | ||
295 | +<!-- </div> --> | ||
296 | +<!-- </div>--> | ||
297 | +<!-- </div>--> | ||
298 | +<!-- </md-dialog-content>--> | ||
299 | +<!-- <md-dialog-actions>--> | ||
300 | +<!-- <md-button type="submit" ng-disabled="addEntityForm.$invalid || !addEntityForm.$dirty" class="md-raised md-primary">Create</md-button>--> | ||
301 | +<!-- <md-button ng-click="vm.cancel()" class="md-primary">Cancel</md-button>--> | ||
302 | +<!-- </md-dialog-actions>--> | ||
303 | +<!-- </form>--> | ||
304 | +<!--</md-dialog>--> |
1 | +/*=======================================================================*/ | ||
2 | +/*===== There are three examples: for delete, edit and add entity =====*/ | ||
3 | +/*=======================================================================*/ | ||
4 | +/*======================= Delete entity example =======================*/ | ||
5 | +/*=======================================================================*/ | ||
6 | +// | ||
7 | +//var $injector = widgetContext.$scope.$injector; | ||
8 | +//var $mdDialog = $injector.get('$mdDialog'), | ||
9 | +// $document = $injector.get('$document'), | ||
10 | +// types = $injector.get('types'), | ||
11 | +// assetService = $injector.get('assetService'), | ||
12 | +// deviceService = $injector.get('deviceService') | ||
13 | +// $rootScope = $injector.get('$rootScope'), | ||
14 | +// $q = $injector.get('$q'); | ||
15 | +// | ||
16 | +//openDeleteEntityDialog(); | ||
17 | +// | ||
18 | +//function openDeleteEntityDialog() { | ||
19 | +// var title = 'Delete ' + entityId.entityType | ||
20 | +// .toLowerCase() + ' ' + | ||
21 | +// entityName; | ||
22 | +// var content = 'Are you sure you want to delete the ' + | ||
23 | +// entityId.entityType.toLowerCase() + ' ' + | ||
24 | +// entityName + '?'; | ||
25 | +// var confirm = $mdDialog.confirm() | ||
26 | +// .targetEvent($event) | ||
27 | +// .title(title) | ||
28 | +// .htmlContent(content) | ||
29 | +// .ariaLabel(title) | ||
30 | +// .cancel('Cancel') | ||
31 | +// .ok('Delete'); | ||
32 | +// $mdDialog.show(confirm).then(function() { | ||
33 | +// deleteEntity(); | ||
34 | +// }) | ||
35 | +//} | ||
36 | +// | ||
37 | +//function deleteEntity() { | ||
38 | +// deleteEntityPromise(entityId).then( | ||
39 | +// function success() { | ||
40 | +// updateAliasData(); | ||
41 | +// }, | ||
42 | +// function fail() { | ||
43 | +// showErrorDialog(); | ||
44 | +// } | ||
45 | +// ); | ||
46 | +//} | ||
47 | +// | ||
48 | +//function deleteEntityPromise(entityId) { | ||
49 | +// if (entityId.entityType == types.entityType.asset) { | ||
50 | +// return assetService.deleteAsset(entityId.id); | ||
51 | +// } else if (entityId.entityType == types.entityType.device) { | ||
52 | +// return deviceService.deleteDevice(entityId.id); | ||
53 | +// } | ||
54 | +//} | ||
55 | +// | ||
56 | +//function updateAliasData() { | ||
57 | +// var aliasIds = []; | ||
58 | +// for (var id in widgetContext.aliasController.resolvedAliases) { | ||
59 | +// aliasIds.push(id); | ||
60 | +// } | ||
61 | +// var tasks = []; | ||
62 | +// aliasIds.forEach(function(aliasId) { | ||
63 | +// widgetContext.aliasController.setAliasUnresolved(aliasId); | ||
64 | +// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId)); | ||
65 | +// }); | ||
66 | +// $q.all(tasks).then(function() { | ||
67 | +// $rootScope.$broadcast('entityAliasesChanged', aliasIds); | ||
68 | +// }); | ||
69 | +//} | ||
70 | +// | ||
71 | +//function showErrorDialog() { | ||
72 | +// var title = 'Error'; | ||
73 | +// var content = 'An error occurred while deleting the entity. Please try again.'; | ||
74 | +// var alert = $mdDialog.alert() | ||
75 | +// .title(title) | ||
76 | +// .htmlContent(content) | ||
77 | +// .ariaLabel(title) | ||
78 | +// .parent(angular.element($document[0].body)) | ||
79 | +// .targetEvent($event) | ||
80 | +// .multiple(true) | ||
81 | +// .clickOutsideToClose(true) | ||
82 | +// .ok('CLOSE'); | ||
83 | +// $mdDialog.show(alert); | ||
84 | +//} | ||
85 | +// | ||
86 | +/*=======================================================================*/ | ||
87 | +/*======================== Edit entity example ========================*/ | ||
88 | +/*=======================================================================*/ | ||
89 | +// | ||
90 | +//var $injector = widgetContext.$scope.$injector; | ||
91 | +//var $mdDialog = $injector.get('$mdDialog'), | ||
92 | +// $document = $injector.get('$document'), | ||
93 | +// $q = $injector.get('$q'), | ||
94 | +// types = $injector.get('types'), | ||
95 | +// $rootScope = $injector.get('$rootScope'), | ||
96 | +// entityService = $injector.get('entityService'), | ||
97 | +// attributeService = $injector.get('attributeService'), | ||
98 | +// entityRelationService = $injector.get('entityRelationService'); | ||
99 | +// | ||
100 | +//openEditEntityDialog(); | ||
101 | +// | ||
102 | +//function openEditEntityDialog() { | ||
103 | +// $mdDialog.show({ | ||
104 | +// controller: ['$scope','$mdDialog', EditEntityDialogController], | ||
105 | +// controllerAs: 'vm', | ||
106 | +// template: htmlTemplate, | ||
107 | +// locals: { | ||
108 | +// entityId: entityId | ||
109 | +// }, | ||
110 | +// parent: angular.element($document[0].body), | ||
111 | +// targetEvent: $event, | ||
112 | +// multiple: true, | ||
113 | +// clickOutsideToClose: false | ||
114 | +// }); | ||
115 | +//} | ||
116 | +// | ||
117 | +//function EditEntityDialogController($scope,$mdDialog) { | ||
118 | +// var vm = this; | ||
119 | +// vm.entityId = entityId; | ||
120 | +// vm.entityName = entityName; | ||
121 | +// vm.entityType = entityId.entityType; | ||
122 | +// vm.allowedEntityTypes = [types.entityType.asset, types.entityType.device]; | ||
123 | +// vm.allowedRelatedEntityTypes = []; | ||
124 | +// vm.entitySearchDirection = types.entitySearchDirection; | ||
125 | +// vm.attributes = {}; | ||
126 | +// vm.serverAttributes = {}; | ||
127 | +// vm.relations = []; | ||
128 | +// vm.newRelations = []; | ||
129 | +// vm.relationsToDelete = []; | ||
130 | +// getEntityInfo(); | ||
131 | +// | ||
132 | +// vm.addRelation = function() { | ||
133 | +// var relation = { | ||
134 | +// direction: null, | ||
135 | +// relationType: null, | ||
136 | +// relatedEntity: null | ||
137 | +// }; | ||
138 | +// vm.newRelations.push(relation); | ||
139 | +// $scope.editEntityForm.$setDirty(); | ||
140 | +// }; | ||
141 | +// vm.removeRelation = function(index) { | ||
142 | +// if (index > -1) { | ||
143 | +// vm.newRelations.splice(index, 1); | ||
144 | +// $scope.editEntityForm.$setDirty(); | ||
145 | +// } | ||
146 | +// }; | ||
147 | +// vm.removeOldRelation = function(index, relation) { | ||
148 | +// if (index > -1) { | ||
149 | +// vm.relations.splice(index, 1); | ||
150 | +// vm.relationsToDelete.push(relation); | ||
151 | +// $scope.editEntityForm.$setDirty(); | ||
152 | +// } | ||
153 | +// }; | ||
154 | +// vm.save = function() { | ||
155 | +// saveAttributes(); | ||
156 | +// saveRelations(); | ||
157 | +// $scope.editEntityForm.$setPristine(); | ||
158 | +// }; | ||
159 | +// vm.cancel = function() { | ||
160 | +// $mdDialog.hide(); | ||
161 | +// }; | ||
162 | +// | ||
163 | +// function getEntityAttributes(attributes) { | ||
164 | +// for (var i = 0; i < attributes.length; i++) { | ||
165 | +// vm.attributes[attributes[i].key] = attributes[i].value; | ||
166 | +// } | ||
167 | +// vm.serverAttributes = angular.copy(vm.attributes); | ||
168 | +// } | ||
169 | +// | ||
170 | +// function getEntityRelations(relations) { | ||
171 | +// var relationsFrom = relations[0]; | ||
172 | +// var relationsTo = relations[1]; | ||
173 | +// for (var i=0; i < relationsFrom.length; i++) { | ||
174 | +// var relation = { | ||
175 | +// direction: types.entitySearchDirection.from, | ||
176 | +// relationType: relationsFrom[i].type, | ||
177 | +// relatedEntity: relationsFrom[i].to | ||
178 | +// }; | ||
179 | +// vm.relations.push(relation); | ||
180 | +// } | ||
181 | +// for (var i=0; i < relationsTo.length; i++) { | ||
182 | +// var relation = { | ||
183 | +// direction: types.entitySearchDirection.to, | ||
184 | +// relationType: relationsTo[i].type, | ||
185 | +// relatedEntity: relationsTo[i].from | ||
186 | +// }; | ||
187 | +// vm.relations.push(relation); | ||
188 | +// } | ||
189 | +// } | ||
190 | +// | ||
191 | +// function getEntityInfo() { | ||
192 | +// entityService.getEntity(entityId.entityType, entityId.id).then( | ||
193 | +// function(entity) { | ||
194 | +// vm.entity = entity; | ||
195 | +// vm.type = vm.entity.type; | ||
196 | +// }); | ||
197 | +// attributeService.getEntityAttributesValues(entityId.entityType, entityId.id, 'SERVER_SCOPE').then( | ||
198 | +// function(data){ | ||
199 | +// if (data.length) { | ||
200 | +// getEntityAttributes(data); | ||
201 | +// } | ||
202 | +// }); | ||
203 | +// $q.all([entityRelationService.findInfoByFrom(entityId.id, entityId.entityType), entityRelationService.findInfoByTo(entityId.id, entityId.entityType)]).then( | ||
204 | +// function(relations){ | ||
205 | +// getEntityRelations(relations); | ||
206 | +// }); | ||
207 | +// } | ||
208 | +// | ||
209 | +// function saveAttributes() { | ||
210 | +// var attributesArray = []; | ||
211 | +// for (var key in vm.attributes) { | ||
212 | +// if (vm.attributes[key] !== vm.serverAttributes[key]) { | ||
213 | +// attributesArray.push({key: key, value: vm.attributes[key]}); | ||
214 | +// } | ||
215 | +// } | ||
216 | +// if (attributesArray.length > 0) { | ||
217 | +// attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \"SERVER_SCOPE\", attributesArray); | ||
218 | +// } | ||
219 | +// } | ||
220 | +// | ||
221 | +// function saveRelations() { | ||
222 | +// var tasks = []; | ||
223 | +// for (var i=0; i < vm.newRelations.length; i++) { | ||
224 | +// var relation = { | ||
225 | +// type: vm.newRelations[i].relationType | ||
226 | +// }; | ||
227 | +// if (vm.newRelations[i].direction == types.entitySearchDirection.from) { | ||
228 | +// relation.to = vm.newRelations[i].relatedEntity; | ||
229 | +// relation.from = entityId; | ||
230 | +// } else { | ||
231 | +// relation.to = entityId; | ||
232 | +// relation.from = vm.newRelations[i].relatedEntity; | ||
233 | +// } | ||
234 | +// tasks.push(entityRelationService.saveRelation(relation)); | ||
235 | +// } | ||
236 | +// for (var i=0; i < vm.relationsToDelete.length; i++) { | ||
237 | +// var relation = { | ||
238 | +// type: vm.relationsToDelete[i].relationType | ||
239 | +// }; | ||
240 | +// if (vm.relationsToDelete[i].direction == types.entitySearchDirection.from) { | ||
241 | +// relation.to = vm.relationsToDelete[i].relatedEntity; | ||
242 | +// relation.from = entityId; | ||
243 | +// } else { | ||
244 | +// relation.to = entityId; | ||
245 | +// relation.from = vm.relationsToDelete[i].relatedEntity; | ||
246 | +// } | ||
247 | +// tasks.push(entityRelationService.deleteRelation(relation.from.id, relation.from.entityType, relation.type, relation.to.id, relation.to.entityType)); | ||
248 | +// } | ||
249 | +// $q.all(tasks).then(function(){ | ||
250 | +// vm.relations = vm.relations.concat(vm.newRelations); | ||
251 | +// vm.newRelations = []; | ||
252 | +// vm.relationsToDelete = []; | ||
253 | +// updateAliasData(); | ||
254 | +// }); | ||
255 | +// } | ||
256 | +// | ||
257 | +// function updateAliasData() { | ||
258 | +// var aliasIds = []; | ||
259 | +// for (var id in widgetContext.aliasController.resolvedAliases) { | ||
260 | +// aliasIds.push(id); | ||
261 | +// } | ||
262 | +// var tasks = []; | ||
263 | +// aliasIds.forEach(function(aliasId) { | ||
264 | +// widgetContext.aliasController.setAliasUnresolved(aliasId); | ||
265 | +// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId)); | ||
266 | +// }); | ||
267 | +// $q.all(tasks).then(function() { | ||
268 | +// $rootScope.$broadcast('entityAliasesChanged', aliasIds); | ||
269 | +// }); | ||
270 | +// } | ||
271 | +//} | ||
272 | +// | ||
273 | +/*========================================================================*/ | ||
274 | +/*========================= Add entity example =========================*/ | ||
275 | +/*========================================================================*/ | ||
276 | +// | ||
277 | +//var $injector = widgetContext.$scope.$injector; | ||
278 | +//var $mdDialog = $injector.get('$mdDialog'), | ||
279 | +// $document = $injector.get('$document'), | ||
280 | +// $q = $injector.get('$q'), | ||
281 | +// $rootScope = $injector.get('$rootScope'), | ||
282 | +// types = $injector.get('types'), | ||
283 | +// assetService = $injector.get('assetService'), | ||
284 | +// deviceService = $injector.get('deviceService'), | ||
285 | +// attributeService = $injector.get('attributeService'), | ||
286 | +// entityRelationService = $injector.get('entityRelationService'); | ||
287 | +// | ||
288 | +//openAddEntityDialog(); | ||
289 | +// | ||
290 | +//function openAddEntityDialog() { | ||
291 | +// $mdDialog.show({ | ||
292 | +// controller: ['$scope','$mdDialog', AddEntityDialogController], | ||
293 | +// controllerAs: 'vm', | ||
294 | +// template: htmlTemplate, | ||
295 | +// locals: { | ||
296 | +// entityId: entityId | ||
297 | +// }, | ||
298 | +// parent: angular.element($document[0].body), | ||
299 | +// targetEvent: $event, | ||
300 | +// multiple: true, | ||
301 | +// clickOutsideToClose: false | ||
302 | +// }); | ||
303 | +//} | ||
304 | +// | ||
305 | +//function AddEntityDialogController($scope, $mdDialog) { | ||
306 | +// var vm = this; | ||
307 | +// vm.allowedEntityTypes = [types.entityType.asset, types.entityType.device]; | ||
308 | +// vm.allowedRelatedEntityTypes = []; | ||
309 | +// vm.entitySearchDirection = types.entitySearchDirection; | ||
310 | +// vm.attributes = {}; | ||
311 | +// vm.relations = []; | ||
312 | +// | ||
313 | +// vm.addRelation = function() { | ||
314 | +// var relation = { | ||
315 | +// direction: null, | ||
316 | +// relationType: null, | ||
317 | +// relatedEntity: null | ||
318 | +// }; | ||
319 | +// vm.relations.push(relation); | ||
320 | +// }; | ||
321 | +// vm.removeRelation = function(index) { | ||
322 | +// if (index > -1) { | ||
323 | +// vm.relations.splice(index, 1); | ||
324 | +// } | ||
325 | +// }; | ||
326 | +// vm.save = function() { | ||
327 | +// $scope.addEntityForm.$setPristine(); | ||
328 | +// saveEntityPromise().then( | ||
329 | +// function (entity) { | ||
330 | +// saveAttributes(entity.id); | ||
331 | +// saveRelations(entity.id); | ||
332 | +// $mdDialog.hide(); | ||
333 | +// } | ||
334 | +// ); | ||
335 | +// }; | ||
336 | +// vm.cancel = function() { | ||
337 | +// $mdDialog.hide(); | ||
338 | +// }; | ||
339 | +// | ||
340 | +// | ||
341 | +// function saveEntityPromise() { | ||
342 | +// var entity = { | ||
343 | +// name: vm.entityName, | ||
344 | +// type: vm.type | ||
345 | +// }; | ||
346 | +// if (vm.entityType == types.entityType.asset) { | ||
347 | +// return assetService.saveAsset(entity); | ||
348 | +// } else if (vm.entityType == types.entityType.device) { | ||
349 | +// return deviceService.saveDevice(entity); | ||
350 | +// } | ||
351 | +// } | ||
352 | +// | ||
353 | +// function saveAttributes(entityId) { | ||
354 | +// var attributesArray = []; | ||
355 | +// for (var key in vm.attributes) { | ||
356 | +// attributesArray.push({key: key, value: vm.attributes[key]}); | ||
357 | +// } | ||
358 | +// if (attributesArray.length > 0) { | ||
359 | +// attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \"SERVER_SCOPE\", attributesArray); | ||
360 | +// } | ||
361 | +// } | ||
362 | +// | ||
363 | +// function saveRelations(entityId) { | ||
364 | +// var tasks = []; | ||
365 | +// for (var i=0; i < vm.relations.length; i++) { | ||
366 | +// var relation = { | ||
367 | +// type: vm.relations[i].relationType | ||
368 | +// }; | ||
369 | +// if (vm.relations[i].direction == types.entitySearchDirection.from) { | ||
370 | +// relation.to = vm.relations[i].relatedEntity; | ||
371 | +// relation.from = entityId; | ||
372 | +// } else { | ||
373 | +// relation.to = entityId; | ||
374 | +// relation.from = vm.relations[i].relatedEntity; | ||
375 | +// } | ||
376 | +// tasks.push(entityRelationService.saveRelation(relation)); | ||
377 | +// } | ||
378 | +// $q.all(tasks).then(function(){ | ||
379 | +// updateAliasData(); | ||
380 | +// }); | ||
381 | +// } | ||
382 | +// | ||
383 | +// function updateAliasData() { | ||
384 | +// var aliasIds = []; | ||
385 | +// for (var id in widgetContext.aliasController.resolvedAliases) { | ||
386 | +// aliasIds.push(id); | ||
387 | +// } | ||
388 | +// var tasks = []; | ||
389 | +// aliasIds.forEach(function(aliasId) { | ||
390 | +// widgetContext.aliasController.setAliasUnresolved(aliasId); | ||
391 | +// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId)); | ||
392 | +// }); | ||
393 | +// $q.all(tasks).then(function() { | ||
394 | +// $rootScope.$broadcast('entityAliasesChanged', aliasIds); | ||
395 | +// }); | ||
396 | +// } | ||
397 | +//} |
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html
0 → 100644
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<div class="mat-padding tb-entity-table tb-absolute-fill"> | ||
19 | + <div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content"> | ||
20 | + <mat-toolbar class="mat-table-toolbar" [fxShow]="!textSearchMode"> | ||
21 | + <div class="mat-toolbar-tools"> | ||
22 | + <span class="tb-entity-table-title" translate>widget-config.actions</span> | ||
23 | + <span fxFlex></span> | ||
24 | + <button mat-button mat-icon-button [disabled]="isLoading$ | async" | ||
25 | + (click)="addAction($event)" | ||
26 | + matTooltip="{{ 'widget-config.add-action' | translate }}" | ||
27 | + matTooltipPosition="above"> | ||
28 | + <mat-icon>add</mat-icon> | ||
29 | + </button> | ||
30 | + <button mat-button mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()" | ||
31 | + matTooltip="{{ 'action.search' | translate }}" | ||
32 | + matTooltipPosition="above"> | ||
33 | + <mat-icon>search</mat-icon> | ||
34 | + </button> | ||
35 | + </div> | ||
36 | + </mat-toolbar> | ||
37 | + <mat-toolbar class="mat-table-toolbar" [fxShow]="textSearchMode"> | ||
38 | + <div class="mat-toolbar-tools"> | ||
39 | + <button mat-button mat-icon-button | ||
40 | + matTooltip="{{ 'widget-config.search-actions' | translate }}" | ||
41 | + matTooltipPosition="above"> | ||
42 | + <mat-icon>search</mat-icon> | ||
43 | + </button> | ||
44 | + <mat-form-field fxFlex> | ||
45 | + <mat-label> </mat-label> | ||
46 | + <input #searchInput matInput | ||
47 | + [(ngModel)]="pageLink.textSearch" | ||
48 | + placeholder="{{ 'widget-config.search-actions' | translate }}"/> | ||
49 | + </mat-form-field> | ||
50 | + <button mat-button mat-icon-button (click)="exitFilterMode()" | ||
51 | + matTooltip="{{ 'action.close' | translate }}" | ||
52 | + matTooltipPosition="above"> | ||
53 | + <mat-icon>close</mat-icon> | ||
54 | + </button> | ||
55 | + </div> | ||
56 | + </mat-toolbar> | ||
57 | + <div fxFlex class="table-container"> | ||
58 | + <mat-table [dataSource]="dataSource" | ||
59 | + matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="(pageLink.sortOrder.direction + '').toLowerCase()" matSortDisableClear> | ||
60 | + <ng-container matColumnDef="actionSourceName"> | ||
61 | + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'widget-config.action-source' | translate }} </mat-header-cell> | ||
62 | + <mat-cell *matCellDef="let action"> | ||
63 | + {{ action.actionSourceName }} | ||
64 | + </mat-cell> | ||
65 | + </ng-container> | ||
66 | + <ng-container matColumnDef="name"> | ||
67 | + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'widget-config.action-name' | translate }} </mat-header-cell> | ||
68 | + <mat-cell *matCellDef="let action"> | ||
69 | + {{ action.name }} | ||
70 | + </mat-cell> | ||
71 | + </ng-container> | ||
72 | + <ng-container matColumnDef="icon"> | ||
73 | + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'widget-config.action-icon' | translate }} </mat-header-cell> | ||
74 | + <mat-cell *matCellDef="let action"> | ||
75 | + <mat-icon>{{ action.icon }}</mat-icon> | ||
76 | + </mat-cell> | ||
77 | + </ng-container> | ||
78 | + <ng-container matColumnDef="typeName"> | ||
79 | + <mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'widget-config.action-type' | translate }} </mat-header-cell> | ||
80 | + <mat-cell *matCellDef="let action"> | ||
81 | + {{ action.typeName }} | ||
82 | + </mat-cell> | ||
83 | + </ng-container> | ||
84 | + <ng-container matColumnDef="actions" stickyEnd> | ||
85 | + <mat-header-cell *matHeaderCellDef [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }"> | ||
86 | + </mat-header-cell> | ||
87 | + <mat-cell *matCellDef="let action" [ngStyle]="{ minWidth: '80px', maxWidth: '80px' }"> | ||
88 | + <div fxFlex fxLayout="row" fxLayoutAlign="end"> | ||
89 | + <button mat-button mat-icon-button [disabled]="isLoading$ | async" | ||
90 | + matTooltip="{{ 'widget-config.edit-action' | translate }}" | ||
91 | + matTooltipPosition="above" | ||
92 | + (click)="editAction($event, action)"> | ||
93 | + <mat-icon>edit</mat-icon> | ||
94 | + </button> | ||
95 | + <button mat-button mat-icon-button [disabled]="isLoading$ | async" | ||
96 | + matTooltip="{{ 'widget-config.delete-action' | translate }}" | ||
97 | + matTooltipPosition="above" | ||
98 | + (click)="deleteAction($event, action)"> | ||
99 | + <mat-icon>delete</mat-icon> | ||
100 | + </button> | ||
101 | + </div> | ||
102 | + </mat-cell> | ||
103 | + </ng-container> | ||
104 | + <mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> | ||
105 | + <mat-row [ngClass]="{'mat-row-select': true}" | ||
106 | + *matRowDef="let action; columns: displayedColumns;"></mat-row> | ||
107 | + </mat-table> | ||
108 | + <span [fxShow]="dataSource.isEmpty() | async" | ||
109 | + fxLayoutAlign="center center" | ||
110 | + class="no-data-found" translate>{{ 'widget-config.no-actions-text' }}</span> | ||
111 | + </div> | ||
112 | + <mat-divider></mat-divider> | ||
113 | + <mat-paginator [length]="dataSource.total() | async" | ||
114 | + [pageIndex]="pageLink.page" | ||
115 | + [pageSize]="pageLink.pageSize" | ||
116 | + [pageSizeOptions]="[10, 20, 30]"></mat-paginator> | ||
117 | + </div> | ||
118 | +</div> |
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.models.ts
0 → 100644
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 { WidgetActionDescriptor, WidgetActionSource, | ||
18 | + widgetActionTypeTranslationMap, CustomActionDescriptor } from '@app/shared/models/widget.models'; | ||
19 | +import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections'; | ||
20 | +import { EntityRelationInfo, EntitySearchDirection } from '@shared/models/relation.models'; | ||
21 | +import { BehaviorSubject, Observable, of, ReplaySubject } from 'rxjs'; | ||
22 | +import { emptyPageData, PageData } from '@shared/models/page/page-data'; | ||
23 | +import { SelectionModel } from '@angular/cdk/collections'; | ||
24 | +import { EntityRelationService } from '@core/http/entity-relation.service'; | ||
25 | +import { TranslateService } from '@ngx-translate/core'; | ||
26 | +import { EntityId } from '@shared/models/id/entity-id'; | ||
27 | +import { PageLink } from '@shared/models/page/page-link'; | ||
28 | +import { catchError, map, publishReplay, refCount, share, take, tap } from 'rxjs/operators'; | ||
29 | +import { entityTypeTranslations } from '@shared/models/entity-type.models'; | ||
30 | +import { UtilsService } from '@core/services/utils.service'; | ||
31 | +import { deepClone, isUndefined } from '@core/utils'; | ||
32 | + | ||
33 | +import * as customSampleJs from '!raw-loader!./custom-sample-js.raw'; | ||
34 | +import * as customSampleCss from '!raw-loader!./custom-sample-css.raw'; | ||
35 | +import * as customSampleHtml from '!raw-loader!./custom-sample-html.raw'; | ||
36 | + | ||
37 | +export interface WidgetActionCallbacks { | ||
38 | + fetchDashboardStates: (query: string) => Array<string>; | ||
39 | +} | ||
40 | + | ||
41 | +export interface WidgetActionsData { | ||
42 | + actionsMap: {[actionSourceId: string]: Array<WidgetActionDescriptor>}; | ||
43 | + actionSources: {[actionSourceId: string]: WidgetActionSource}; | ||
44 | +} | ||
45 | + | ||
46 | +export interface WidgetActionDescriptorInfo extends WidgetActionDescriptor { | ||
47 | + actionSourceId?: string; | ||
48 | + actionSourceName?: string; | ||
49 | + typeName?: string; | ||
50 | +} | ||
51 | + | ||
52 | +export function toWidgetActionDescriptor(action: WidgetActionDescriptorInfo): WidgetActionDescriptor { | ||
53 | + const copy = deepClone(action); | ||
54 | + delete copy.actionSourceId; | ||
55 | + delete copy.actionSourceName; | ||
56 | + delete copy.typeName; | ||
57 | + return copy; | ||
58 | +} | ||
59 | + | ||
60 | +export function toCustomAction(action: WidgetActionDescriptorInfo): CustomActionDescriptor { | ||
61 | + let result: CustomActionDescriptor; | ||
62 | + if (!action || (isUndefined(action.customFunction) && isUndefined(action.customHtml) && isUndefined(action.customCss))) { | ||
63 | + result = { | ||
64 | + customHtml: customSampleHtml, | ||
65 | + customCss: customSampleCss, | ||
66 | + customFunction: customSampleJs | ||
67 | + }; | ||
68 | + } else { | ||
69 | + result = { | ||
70 | + customHtml: action.customHtml, | ||
71 | + customCss: action.customCss, | ||
72 | + customFunction: action.customFunction | ||
73 | + }; | ||
74 | + } | ||
75 | + result.customResources = action ? deepClone(action.customResources) : []; | ||
76 | + return result; | ||
77 | +} | ||
78 | + | ||
79 | +export class WidgetActionsDatasource implements DataSource<WidgetActionDescriptorInfo> { | ||
80 | + | ||
81 | + private actionsSubject = new BehaviorSubject<WidgetActionDescriptorInfo[]>([]); | ||
82 | + private pageDataSubject = new BehaviorSubject<PageData<WidgetActionDescriptorInfo>>(emptyPageData<WidgetActionDescriptorInfo>()); | ||
83 | + | ||
84 | + public pageData$ = this.pageDataSubject.asObservable(); | ||
85 | + | ||
86 | + private allActions: Observable<Array<WidgetActionDescriptorInfo>>; | ||
87 | + | ||
88 | + private actionsMap: {[actionSourceId: string]: Array<WidgetActionDescriptor>}; | ||
89 | + private actionSources: {[actionSourceId: string]: WidgetActionSource}; | ||
90 | + | ||
91 | + constructor(private translate: TranslateService, | ||
92 | + private utils: UtilsService) {} | ||
93 | + | ||
94 | + connect(collectionViewer: CollectionViewer): Observable<WidgetActionDescriptorInfo[] | ReadonlyArray<WidgetActionDescriptorInfo>> { | ||
95 | + return this.actionsSubject.asObservable(); | ||
96 | + } | ||
97 | + | ||
98 | + disconnect(collectionViewer: CollectionViewer): void { | ||
99 | + this.actionsSubject.complete(); | ||
100 | + this.pageDataSubject.complete(); | ||
101 | + } | ||
102 | + | ||
103 | + setActions(actionsData: WidgetActionsData) { | ||
104 | + this.actionsMap = actionsData.actionsMap; | ||
105 | + this.actionSources = actionsData.actionSources; | ||
106 | + } | ||
107 | + | ||
108 | + loadActions(pageLink: PageLink, reload: boolean = false): Observable<PageData<WidgetActionDescriptorInfo>> { | ||
109 | + if (reload) { | ||
110 | + this.allActions = null; | ||
111 | + } | ||
112 | + const result = new ReplaySubject<PageData<WidgetActionDescriptorInfo>>(); | ||
113 | + this.fetchActions(pageLink).pipe( | ||
114 | + catchError(() => of(emptyPageData<WidgetActionDescriptorInfo>())), | ||
115 | + ).subscribe( | ||
116 | + (pageData) => { | ||
117 | + this.actionsSubject.next(pageData.data); | ||
118 | + this.pageDataSubject.next(pageData); | ||
119 | + result.next(pageData); | ||
120 | + } | ||
121 | + ); | ||
122 | + return result; | ||
123 | + } | ||
124 | + | ||
125 | + fetchActions(pageLink: PageLink): Observable<PageData<WidgetActionDescriptorInfo>> { | ||
126 | + return this.getAllActions().pipe( | ||
127 | + map((data) => pageLink.filterData(data)) | ||
128 | + ); | ||
129 | + } | ||
130 | + | ||
131 | + getAllActions(): Observable<Array<WidgetActionDescriptorInfo>> { | ||
132 | + if (!this.allActions) { | ||
133 | + const actions: WidgetActionDescriptorInfo[] = []; | ||
134 | + for (const actionSourceId of Object.keys(this.actionsMap)) { | ||
135 | + const descriptors = this.actionsMap[actionSourceId]; | ||
136 | + descriptors.forEach((descriptor) => { | ||
137 | + actions.push(this.toWidgetActionDescriptorInfo(actionSourceId, descriptor)); | ||
138 | + }); | ||
139 | + } | ||
140 | + this.allActions = of(actions).pipe( | ||
141 | + publishReplay(1), | ||
142 | + refCount() | ||
143 | + ); | ||
144 | + } | ||
145 | + return this.allActions; | ||
146 | + } | ||
147 | + | ||
148 | + private toWidgetActionDescriptorInfo(actionSourceId: string, action: WidgetActionDescriptor): WidgetActionDescriptorInfo { | ||
149 | + const actionSource = this.actionSources[actionSourceId]; | ||
150 | + const actionSourceName = actionSource ? this.utils.customTranslation(actionSource.name, actionSource.name) : actionSourceId; | ||
151 | + const typeName = this.translate.instant(widgetActionTypeTranslationMap.get(action.type)); | ||
152 | + return { actionSourceId, actionSourceName, typeName, ...action}; | ||
153 | + } | ||
154 | + | ||
155 | + isEmpty(): Observable<boolean> { | ||
156 | + return this.actionsSubject.pipe( | ||
157 | + map((actions) => !actions.length) | ||
158 | + ); | ||
159 | + } | ||
160 | + | ||
161 | + total(): Observable<number> { | ||
162 | + return this.pageDataSubject.pipe( | ||
163 | + map((pageData) => pageData.totalElements) | ||
164 | + ); | ||
165 | + } | ||
166 | + | ||
167 | +} |
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss
0 → 100644
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 | +:host { | ||
17 | + width: 100%; | ||
18 | + height: 100%; | ||
19 | + .tb-entity-table { | ||
20 | + .tb-entity-table-content { | ||
21 | + width: 100%; | ||
22 | + height: 100%; | ||
23 | + background: #fff; | ||
24 | + | ||
25 | + .tb-entity-table-title { | ||
26 | + padding-right: 20px; | ||
27 | + white-space: nowrap; | ||
28 | + overflow: hidden; | ||
29 | + text-overflow: ellipsis; | ||
30 | + } | ||
31 | + | ||
32 | + .table-container { | ||
33 | + overflow: auto; | ||
34 | + } | ||
35 | + } | ||
36 | + } | ||
37 | +} | ||
38 | + | ||
39 | +:host ::ng-deep { | ||
40 | + .mat-sort-header-sorted .mat-sort-header-arrow { | ||
41 | + opacity: 1 !important; | ||
42 | + } | ||
43 | +} |
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 { | ||
18 | + AfterViewInit, | ||
19 | + ChangeDetectionStrategy, | ||
20 | + Component, | ||
21 | + ElementRef, | ||
22 | + forwardRef, | ||
23 | + Input, | ||
24 | + OnDestroy, | ||
25 | + OnInit, | ||
26 | + ViewChild | ||
27 | +} from '@angular/core'; | ||
28 | +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | ||
29 | +import { TranslateService } from '@ngx-translate/core'; | ||
30 | +import { PageComponent } from '@shared/components/page.component'; | ||
31 | +import { Store } from '@ngrx/store'; | ||
32 | +import { AppState } from '@core/core.state'; | ||
33 | +import { MatDialog } from '@angular/material/dialog'; | ||
34 | +import { DialogService } from '@core/services/dialog.service'; | ||
35 | +import { PageLink } from '@shared/models/page/page-link'; | ||
36 | +import { Direction, SortOrder } from '@shared/models/page/sort-order'; | ||
37 | +import { MatPaginator } from '@angular/material/paginator'; | ||
38 | +import { MatSort } from '@angular/material/sort'; | ||
39 | +import { fromEvent, merge } from 'rxjs'; | ||
40 | +import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; | ||
41 | +import { | ||
42 | + WidgetActionDescriptorInfo, | ||
43 | + WidgetActionsData, | ||
44 | + WidgetActionsDatasource, | ||
45 | + WidgetActionCallbacks, toWidgetActionDescriptor | ||
46 | +} from '@home/components/widget/action/manage-widget-actions.component.models'; | ||
47 | +import { UtilsService } from '@core/services/utils.service'; | ||
48 | +import { EntityRelation, EntitySearchDirection, RelationTypeGroup } from '@shared/models/relation.models'; | ||
49 | +import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component'; | ||
50 | +import { WidgetActionDescriptor, WidgetActionSource } from '@shared/models/widget.models'; | ||
51 | +import { | ||
52 | + WidgetActionDialogComponent, | ||
53 | + WidgetActionDialogData | ||
54 | +} from '@home/components/widget/action/widget-action-dialog.component'; | ||
55 | +import { deepClone } from '@core/utils'; | ||
56 | + | ||
57 | +@Component({ | ||
58 | + selector: 'tb-manage-widget-actions', | ||
59 | + templateUrl: './manage-widget-actions.component.html', | ||
60 | + styleUrls: ['./manage-widget-actions.component.scss'], | ||
61 | + providers: [ | ||
62 | + { | ||
63 | + provide: NG_VALUE_ACCESSOR, | ||
64 | + useExisting: forwardRef(() => ManageWidgetActionsComponent), | ||
65 | + multi: true | ||
66 | + } | ||
67 | + ] | ||
68 | +}) | ||
69 | +export class ManageWidgetActionsComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy, ControlValueAccessor { | ||
70 | + | ||
71 | + @Input() disabled: boolean; | ||
72 | + | ||
73 | + @Input() callbacks: WidgetActionCallbacks; | ||
74 | + | ||
75 | + innerValue: WidgetActionsData; | ||
76 | + | ||
77 | + displayedColumns: string[]; | ||
78 | + pageLink: PageLink; | ||
79 | + textSearchMode = false; | ||
80 | + dataSource: WidgetActionsDatasource; | ||
81 | + | ||
82 | + viewsInited = false; | ||
83 | + dirtyValue = false; | ||
84 | + | ||
85 | + @ViewChild('searchInput', {static: false}) searchInputField: ElementRef; | ||
86 | + | ||
87 | + @ViewChild(MatPaginator, {static: false}) paginator: MatPaginator; | ||
88 | + @ViewChild(MatSort, {static: false}) sort: MatSort; | ||
89 | + | ||
90 | + private propagateChange = (_: any) => {}; | ||
91 | + | ||
92 | + constructor(protected store: Store<AppState>, | ||
93 | + private translate: TranslateService, | ||
94 | + private utils: UtilsService, | ||
95 | + private dialog: MatDialog, | ||
96 | + private dialogs: DialogService) { | ||
97 | + super(store); | ||
98 | + const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC }; | ||
99 | + this.pageLink = new PageLink(10, 0, null, sortOrder); | ||
100 | + this.dataSource = new WidgetActionsDatasource(this.translate, this.utils); | ||
101 | + this.displayedColumns = ['actionSourceName', 'name', 'icon', 'typeName', 'actions']; | ||
102 | + } | ||
103 | + | ||
104 | + ngOnInit(): void { | ||
105 | + } | ||
106 | + | ||
107 | + ngOnDestroy(): void { | ||
108 | + } | ||
109 | + | ||
110 | + ngAfterViewInit() { | ||
111 | + | ||
112 | + fromEvent(this.searchInputField.nativeElement, 'keyup') | ||
113 | + .pipe( | ||
114 | + debounceTime(150), | ||
115 | + distinctUntilChanged(), | ||
116 | + tap(() => { | ||
117 | + this.paginator.pageIndex = 0; | ||
118 | + this.updateData(); | ||
119 | + }) | ||
120 | + ) | ||
121 | + .subscribe(); | ||
122 | + | ||
123 | + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); | ||
124 | + | ||
125 | + merge(this.sort.sortChange, this.paginator.page) | ||
126 | + .pipe( | ||
127 | + tap(() => this.updateData()) | ||
128 | + ) | ||
129 | + .subscribe(); | ||
130 | + | ||
131 | + this.viewsInited = true; | ||
132 | + if (this.dirtyValue) { | ||
133 | + this.dirtyValue = false; | ||
134 | + this.updateData(true); | ||
135 | + } | ||
136 | + | ||
137 | + } | ||
138 | + | ||
139 | + updateData(reload: boolean = false) { | ||
140 | + this.pageLink.page = this.paginator.pageIndex; | ||
141 | + this.pageLink.pageSize = this.paginator.pageSize; | ||
142 | + this.pageLink.sortOrder.property = this.sort.active; | ||
143 | + this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; | ||
144 | + this.dataSource.loadActions(this.pageLink, reload); | ||
145 | + } | ||
146 | + | ||
147 | + addAction($event: Event) { | ||
148 | + this.openWidgetActionDialog($event); | ||
149 | + } | ||
150 | + | ||
151 | + editAction($event: Event, action: WidgetActionDescriptorInfo) { | ||
152 | + this.openWidgetActionDialog($event, action); | ||
153 | + } | ||
154 | + | ||
155 | + openWidgetActionDialog($event: Event, action: WidgetActionDescriptorInfo = null) { | ||
156 | + if ($event) { | ||
157 | + $event.stopPropagation(); | ||
158 | + } | ||
159 | + const isAdd = action === null; | ||
160 | + let prevActionSourceId = null; | ||
161 | + if (!isAdd) { | ||
162 | + prevActionSourceId = action.actionSourceId; | ||
163 | + } | ||
164 | + const availableActionSources: {[actionSourceId: string]: WidgetActionSource} = {}; | ||
165 | + for (const id of Object.keys(this.innerValue.actionSources)) { | ||
166 | + const actionSource = this.innerValue.actionSources[id]; | ||
167 | + if (actionSource.multiple) { | ||
168 | + availableActionSources[id] = actionSource; | ||
169 | + } else { | ||
170 | + if (!isAdd && action.actionSourceId === id) { | ||
171 | + availableActionSources[id] = actionSource; | ||
172 | + } else { | ||
173 | + const existing = this.innerValue.actionsMap[id]; | ||
174 | + if (!existing || !existing.length) { | ||
175 | + availableActionSources[id] = actionSource; | ||
176 | + } | ||
177 | + } | ||
178 | + } | ||
179 | + } | ||
180 | + | ||
181 | + const actionsData: WidgetActionsData = { | ||
182 | + actionsMap: this.innerValue.actionsMap, | ||
183 | + actionSources: availableActionSources | ||
184 | + }; | ||
185 | + | ||
186 | + this.dialog.open<WidgetActionDialogComponent, WidgetActionDialogData, | ||
187 | + WidgetActionDescriptorInfo>(WidgetActionDialogComponent, { | ||
188 | + disableClose: true, | ||
189 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | ||
190 | + data: { | ||
191 | + isAdd, | ||
192 | + callbacks: this.callbacks, | ||
193 | + actionsData, | ||
194 | + action: deepClone(action) | ||
195 | + } | ||
196 | + }).afterClosed().subscribe( | ||
197 | + (res) => { | ||
198 | + if (res) { | ||
199 | + this.saveAction(res, isAdd, prevActionSourceId); | ||
200 | + } | ||
201 | + } | ||
202 | + ); | ||
203 | + } | ||
204 | + | ||
205 | + private saveAction(actionInfo: WidgetActionDescriptorInfo, isAdd: boolean, prevActionSourceId: string) { | ||
206 | + const actionSourceId = actionInfo.actionSourceId; | ||
207 | + const action = toWidgetActionDescriptor(actionInfo); | ||
208 | + if (isAdd) { | ||
209 | + const targetActions = this.getOrCreateTargetActions(actionSourceId); | ||
210 | + targetActions.push(action); | ||
211 | + } else { | ||
212 | + if (actionSourceId !== prevActionSourceId) { | ||
213 | + let targetActions = this.getOrCreateTargetActions(prevActionSourceId); | ||
214 | + const targetIndex = targetActions.findIndex((targetAction) => targetAction.id === action.id); | ||
215 | + if (targetIndex > -1) { | ||
216 | + targetActions.splice(targetIndex, 1); | ||
217 | + } | ||
218 | + targetActions = this.getOrCreateTargetActions(actionSourceId); | ||
219 | + targetActions.push(action); | ||
220 | + } else { | ||
221 | + const targetActions = this.getOrCreateTargetActions(actionSourceId); | ||
222 | + const targetIndex = targetActions.findIndex((targetAction) => targetAction.id === action.id); | ||
223 | + if (targetIndex > -1) { | ||
224 | + targetActions[targetIndex] = action; | ||
225 | + } | ||
226 | + } | ||
227 | + } | ||
228 | + this.onActionsUpdated(); | ||
229 | + } | ||
230 | + | ||
231 | + private getOrCreateTargetActions(actionSourceId: string): Array<WidgetActionDescriptor> { | ||
232 | + const actionsMap = this.innerValue.actionsMap; | ||
233 | + let targetActions = actionsMap[actionSourceId]; | ||
234 | + if (!targetActions) { | ||
235 | + targetActions = []; | ||
236 | + actionsMap[actionSourceId] = targetActions; | ||
237 | + } | ||
238 | + return targetActions; | ||
239 | + } | ||
240 | + | ||
241 | + deleteAction($event: Event, action: WidgetActionDescriptorInfo) { | ||
242 | + if ($event) { | ||
243 | + $event.stopPropagation(); | ||
244 | + } | ||
245 | + const title = this.translate.instant('widget-config.delete-action-title'); | ||
246 | + const content = this.translate.instant('widget-config.delete-action-text', {actionName: action.name}); | ||
247 | + this.dialogs.confirm(title, content, | ||
248 | + this.translate.instant('action.no'), | ||
249 | + this.translate.instant('action.yes'), true).subscribe( | ||
250 | + (res) => { | ||
251 | + if (res) { | ||
252 | + const targetActions = this.getOrCreateTargetActions(action.actionSourceId); | ||
253 | + const targetIndex = targetActions.findIndex((targetAction) => targetAction.id === action.id); | ||
254 | + if (targetIndex > -1) { | ||
255 | + targetActions.splice(targetIndex, 1); | ||
256 | + this.onActionsUpdated(); | ||
257 | + } | ||
258 | + } | ||
259 | + }); | ||
260 | + } | ||
261 | + | ||
262 | + enterFilterMode() { | ||
263 | + this.textSearchMode = true; | ||
264 | + this.pageLink.textSearch = ''; | ||
265 | + setTimeout(() => { | ||
266 | + this.searchInputField.nativeElement.focus(); | ||
267 | + this.searchInputField.nativeElement.setSelectionRange(0, 0); | ||
268 | + }, 10); | ||
269 | + } | ||
270 | + | ||
271 | + exitFilterMode() { | ||
272 | + this.textSearchMode = false; | ||
273 | + this.pageLink.textSearch = null; | ||
274 | + this.paginator.pageIndex = 0; | ||
275 | + this.updateData(); | ||
276 | + } | ||
277 | + | ||
278 | + resetSortAndFilter(update: boolean = true) { | ||
279 | + this.pageLink.textSearch = null; | ||
280 | + this.paginator.pageIndex = 0; | ||
281 | + const sortable = this.sort.sortables.get('actionSourceName'); | ||
282 | + this.sort.active = sortable.id; | ||
283 | + this.sort.direction = 'asc'; | ||
284 | + if (update) { | ||
285 | + this.updateData(true); | ||
286 | + } | ||
287 | + } | ||
288 | + | ||
289 | + registerOnChange(fn: any): void { | ||
290 | + this.propagateChange = fn; | ||
291 | + } | ||
292 | + | ||
293 | + registerOnTouched(fn: any): void { | ||
294 | + } | ||
295 | + | ||
296 | + setDisabledState(isDisabled: boolean): void { | ||
297 | + this.disabled = isDisabled; | ||
298 | + } | ||
299 | + | ||
300 | + writeValue(obj: WidgetActionsData): void { | ||
301 | + this.innerValue = obj; | ||
302 | + setTimeout(() => { | ||
303 | + this.dataSource.setActions(this.innerValue); | ||
304 | + if (this.viewsInited) { | ||
305 | + this.resetSortAndFilter(true); | ||
306 | + } else { | ||
307 | + this.dirtyValue = true; | ||
308 | + } | ||
309 | + }, 0); | ||
310 | + } | ||
311 | + | ||
312 | + private onActionsUpdated() { | ||
313 | + this.updateData(true); | ||
314 | + this.propagateChange(this.innerValue); | ||
315 | + } | ||
316 | +} |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<form #widgetActionForm="ngForm" [formGroup]="widgetActionFormGroup" (ngSubmit)="save()" style="min-width: 600px;"> | ||
19 | + <mat-toolbar fxLayout="row" color="primary"> | ||
20 | + <h2>{{ (isAdd ? 'widget-config.add-action' : 'widget-config.edit-action' ) | translate }}</h2> | ||
21 | + <span fxFlex></span> | ||
22 | + <button mat-button mat-icon-button | ||
23 | + (click)="cancel()" | ||
24 | + type="button"> | ||
25 | + <mat-icon class="material-icons">close</mat-icon> | ||
26 | + </button> | ||
27 | + </mat-toolbar> | ||
28 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | ||
29 | + </mat-progress-bar> | ||
30 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | ||
31 | + <div mat-dialog-content> | ||
32 | + <fieldset [disabled]="isLoading$ | async"> | ||
33 | + <mat-form-field class="mat-block"> | ||
34 | + <mat-label translate>widget-config.action-source</mat-label> | ||
35 | + <mat-select required matInput formControlName="actionSourceId"> | ||
36 | + <mat-option *ngFor="let actionSourceItem of data.actionsData.actionSources | keyvalue" [value]="actionSourceItem.key"> | ||
37 | + {{ actionSourceName(actionSourceItem.value) }} | ||
38 | + </mat-option> | ||
39 | + </mat-select> | ||
40 | + <mat-error *ngIf="widgetActionFormGroup.get('actionSourceId').hasError('required')"> | ||
41 | + {{ 'widget-config.action-source-required' | translate }} | ||
42 | + </mat-error> | ||
43 | + </mat-form-field> | ||
44 | + <mat-form-field class="mat-block" style="padding-bottom: 20px;"> | ||
45 | + <mat-label translate>widget-config.action-name</mat-label> | ||
46 | + <input required matInput formControlName="name"> | ||
47 | + <mat-error *ngIf="widgetActionFormGroup.get('name').hasError('required')"> | ||
48 | + {{ 'widget-config.action-name-required' | translate }} | ||
49 | + </mat-error> | ||
50 | + <mat-error *ngIf="widgetActionFormGroup.get('name').hasError('actionNameNotUnique')" | ||
51 | + [innerHTML]="'widget-config.action-name-not-unique' | translate"> | ||
52 | + </mat-error> | ||
53 | + </mat-form-field> | ||
54 | + <tb-material-icon-select | ||
55 | + formControlName="icon"> | ||
56 | + </tb-material-icon-select> | ||
57 | + <mat-form-field class="mat-block"> | ||
58 | + <mat-label translate>widget-config.action-type</mat-label> | ||
59 | + <mat-select required matInput formControlName="type"> | ||
60 | + <mat-option *ngFor="let actionType of widgetActionTypes" [value]="actionType"> | ||
61 | + {{ widgetActionTypeTranslations.get(actionType) | translate }} | ||
62 | + </mat-option> | ||
63 | + </mat-select> | ||
64 | + <mat-error *ngIf="widgetActionFormGroup.get('type').hasError('required')"> | ||
65 | + {{ 'widget-config.action-type-required' | translate }} | ||
66 | + </mat-error> | ||
67 | + </mat-form-field> | ||
68 | + <section fxLayout="column" [formGroup]="actionTypeFormGroup" [ngSwitch]="widgetActionFormGroup.get('type').value"> | ||
69 | + <ng-template [ngSwitchCase]="widgetActionType.openDashboard"> | ||
70 | + <div fxLayout="column" style="padding-bottom: 20px;"> | ||
71 | + <div class="mat-caption tb-required" | ||
72 | + style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>widget-action.target-dashboard</div> | ||
73 | + <tb-dashboard-autocomplete | ||
74 | + formControlName="targetDashboardId" | ||
75 | + required | ||
76 | + [selectFirstDashboard]="true" | ||
77 | + ></tb-dashboard-autocomplete> | ||
78 | + </div> | ||
79 | + </ng-template> | ||
80 | + <ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState || | ||
81 | + widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState || | ||
82 | + widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ? | ||
83 | + widgetActionFormGroup.get('type').value : ''"> | ||
84 | + <mat-form-field class="mat-block"> | ||
85 | + <input matInput type="text" placeholder="{{ 'widget-action.target-dashboard-state' | translate }}" | ||
86 | + #dashboardStateInput | ||
87 | + formControlName="targetDashboardStateId" | ||
88 | + [required]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState" | ||
89 | + [matAutocomplete]="targetDashboardStateAutocomplete"> | ||
90 | + <button *ngIf="actionTypeFormGroup.get('targetDashboardStateId').value" | ||
91 | + type="button" | ||
92 | + matSuffix mat-button mat-icon-button aria-label="Clear" | ||
93 | + (click)="clearTargetDashboardState()"> | ||
94 | + <mat-icon class="material-icons">close</mat-icon> | ||
95 | + </button> | ||
96 | + <mat-autocomplete | ||
97 | + class="tb-autocomplete" | ||
98 | + #targetDashboardStateAutocomplete="matAutocomplete"> | ||
99 | + <mat-option *ngFor="let state of filteredDashboardStates | async" [value]="state"> | ||
100 | + <span [innerHTML]="state | highlight:targetDashboardStateSearchText"></span> | ||
101 | + </mat-option> | ||
102 | + </mat-autocomplete> | ||
103 | + <mat-error *ngIf="actionTypeFormGroup.get('targetDashboardStateId').hasError('required')"> | ||
104 | + {{ 'widget-action.target-dashboard-state-required' | translate }} | ||
105 | + </mat-error> | ||
106 | + </mat-form-field> | ||
107 | + </ng-template> | ||
108 | + <ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState || | ||
109 | + widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ? | ||
110 | + widgetActionFormGroup.get('type').value : ''"> | ||
111 | + <mat-checkbox formControlName="openRightLayout"> | ||
112 | + {{ 'widget-action.open-right-layout' | translate }} | ||
113 | + </mat-checkbox> | ||
114 | + </ng-template> | ||
115 | + <ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState || | ||
116 | + widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState || | ||
117 | + widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ? | ||
118 | + widgetActionFormGroup.get('type').value : ''"> | ||
119 | + <div fxFlex fxLayout="column"> | ||
120 | + <mat-checkbox formControlName="setEntityId"> | ||
121 | + {{ 'widget-action.set-entity-from-widget' | translate }} | ||
122 | + </mat-checkbox> | ||
123 | + <mat-form-field *ngIf="actionTypeFormGroup.get('setEntityId').value" | ||
124 | + floatLabel="always" | ||
125 | + class="mat-block"> | ||
126 | + <mat-label translate>alias.state-entity-parameter-name</mat-label> | ||
127 | + <input matInput | ||
128 | + placeholder="{{ 'alias.default-entity-parameter-name' | translate }}" | ||
129 | + formControlName="stateEntityParamName"> | ||
130 | + </mat-form-field> | ||
131 | + </div> | ||
132 | + </ng-template> | ||
133 | + <ng-template [ngSwitchCase]="widgetActionType.custom"> | ||
134 | + <tb-js-func | ||
135 | + formControlName="customFunction" | ||
136 | + [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams']" | ||
137 | + [validationArgs]="[]" | ||
138 | + ></tb-js-func> | ||
139 | + </ng-template> | ||
140 | + <ng-template [ngSwitchCase]="widgetActionType.customPretty"> | ||
141 | + <tb-custom-action-pretty-editor | ||
142 | + formControlName="customAction"> | ||
143 | + </tb-custom-action-pretty-editor> | ||
144 | + </ng-template> | ||
145 | + </section> | ||
146 | + </fieldset> | ||
147 | + </div> | ||
148 | + <div mat-dialog-actions fxLayout="row"> | ||
149 | + <span fxFlex></span> | ||
150 | + <button mat-button mat-raised-button color="primary" | ||
151 | + type="submit" | ||
152 | + [disabled]="(isLoading$ | async) || widgetActionFormGroup.invalid || actionTypeFormGroup.invalid || (!widgetActionFormGroup.dirty && !actionTypeFormGroup.dirty)"> | ||
153 | + {{ (isAdd ? 'action.add' : 'action.save') | translate }} | ||
154 | + </button> | ||
155 | + <button mat-button color="primary" | ||
156 | + style="margin-right: 20px;" | ||
157 | + type="button" | ||
158 | + [disabled]="(isLoading$ | async)" | ||
159 | + (click)="cancel()" cdkFocusInitial> | ||
160 | + {{ 'action.cancel' | translate }} | ||
161 | + </button> | ||
162 | + </div> | ||
163 | +</form> |
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, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core'; | ||
18 | +import { ErrorStateMatcher, MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; | ||
19 | +import { Store } from '@ngrx/store'; | ||
20 | +import { AppState } from '@core/core.state'; | ||
21 | +import { | ||
22 | + FormBuilder, | ||
23 | + FormControl, | ||
24 | + FormGroup, | ||
25 | + FormGroupDirective, | ||
26 | + NgForm, | ||
27 | + ValidatorFn, | ||
28 | + Validators | ||
29 | +} from '@angular/forms'; | ||
30 | +import { Observable, of } from 'rxjs'; | ||
31 | +import { Router } from '@angular/router'; | ||
32 | +import { DialogComponent } from '@app/shared/components/dialog.component'; | ||
33 | +import { | ||
34 | + toCustomAction, | ||
35 | + WidgetActionCallbacks, | ||
36 | + WidgetActionDescriptorInfo, | ||
37 | + WidgetActionsData | ||
38 | +} from '@home/components/widget/action/manage-widget-actions.component.models'; | ||
39 | +import { UtilsService } from '@core/services/utils.service'; | ||
40 | +import { WidgetActionSource, WidgetActionType, widgetActionTypeTranslationMap } from '@shared/models/widget.models'; | ||
41 | +import { map, mergeMap, startWith, tap } from 'rxjs/operators'; | ||
42 | +import { DashboardService } from '@core/http/dashboard.service'; | ||
43 | +import { Dashboard } from '@shared/models/dashboard.models'; | ||
44 | +import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; | ||
45 | + | ||
46 | +export interface WidgetActionDialogData { | ||
47 | + isAdd: boolean; | ||
48 | + callbacks: WidgetActionCallbacks; | ||
49 | + actionsData: WidgetActionsData; | ||
50 | + action?: WidgetActionDescriptorInfo; | ||
51 | +} | ||
52 | + | ||
53 | +@Component({ | ||
54 | + selector: 'tb-widget-action-dialog', | ||
55 | + templateUrl: './widget-action-dialog.component.html', | ||
56 | + providers: [{provide: ErrorStateMatcher, useExisting: WidgetActionDialogComponent}], | ||
57 | + styleUrls: [] | ||
58 | +}) | ||
59 | +export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDialogComponent, | ||
60 | + WidgetActionDescriptorInfo> implements OnInit, ErrorStateMatcher { | ||
61 | + | ||
62 | + @ViewChild('dashboardStateInput', {static: false}) dashboardStateInput: ElementRef; | ||
63 | + | ||
64 | + widgetActionFormGroup: FormGroup; | ||
65 | + actionTypeFormGroup: FormGroup; | ||
66 | + | ||
67 | + isAdd: boolean; | ||
68 | + action: WidgetActionDescriptorInfo; | ||
69 | + | ||
70 | + widgetActionTypes = Object.keys(WidgetActionType); | ||
71 | + widgetActionTypeTranslations = widgetActionTypeTranslationMap; | ||
72 | + widgetActionType = WidgetActionType; | ||
73 | + | ||
74 | + filteredDashboardStates: Observable<Array<string>>; | ||
75 | + targetDashboardStateSearchText = ''; | ||
76 | + selectedDashboardStateIds: Observable<Array<string>>; | ||
77 | + | ||
78 | + submitted = false; | ||
79 | + | ||
80 | + constructor(protected store: Store<AppState>, | ||
81 | + protected router: Router, | ||
82 | + private utils: UtilsService, | ||
83 | + private dashboardService: DashboardService, | ||
84 | + private dashboardUtils: DashboardUtilsService, | ||
85 | + @Inject(MAT_DIALOG_DATA) public data: WidgetActionDialogData, | ||
86 | + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, | ||
87 | + public dialogRef: MatDialogRef<WidgetActionDialogComponent, WidgetActionDescriptorInfo>, | ||
88 | + public fb: FormBuilder) { | ||
89 | + super(store, router, dialogRef); | ||
90 | + this.isAdd = data.isAdd; | ||
91 | + if (this.isAdd) { | ||
92 | + this.action = { | ||
93 | + id: this.utils.guid(), | ||
94 | + name: '', | ||
95 | + icon: 'more_horiz', | ||
96 | + type: null | ||
97 | + }; | ||
98 | + } else { | ||
99 | + this.action = this.data.action; | ||
100 | + } | ||
101 | + } | ||
102 | + | ||
103 | + ngOnInit(): void { | ||
104 | + this.widgetActionFormGroup = this.fb.group({}); | ||
105 | + this.widgetActionFormGroup.addControl('actionSourceId', | ||
106 | + this.fb.control(this.action.actionSourceId, [Validators.required])); | ||
107 | + this.widgetActionFormGroup.addControl('name', | ||
108 | + this.fb.control(this.action.name, [this.validateActionName(), Validators.required])); | ||
109 | + this.widgetActionFormGroup.addControl('icon', | ||
110 | + this.fb.control(this.action.icon, [Validators.required])); | ||
111 | + this.widgetActionFormGroup.addControl('type', | ||
112 | + this.fb.control(this.action.type, [Validators.required])); | ||
113 | + this.updateActionTypeFormGroup(this.action.type, this.action); | ||
114 | + this.widgetActionFormGroup.get('type').valueChanges.subscribe((type: WidgetActionType) => { | ||
115 | + this.updateActionTypeFormGroup(type); | ||
116 | + }); | ||
117 | + this.widgetActionFormGroup.get('actionSourceId').valueChanges.subscribe(() => { | ||
118 | + this.widgetActionFormGroup.get('name').updateValueAndValidity(); | ||
119 | + }); | ||
120 | + } | ||
121 | + | ||
122 | + private updateActionTypeFormGroup(type?: WidgetActionType, action?: WidgetActionDescriptorInfo) { | ||
123 | + this.actionTypeFormGroup = this.fb.group({}); | ||
124 | + if (type) { | ||
125 | + switch (type) { | ||
126 | + case WidgetActionType.openDashboard: | ||
127 | + case WidgetActionType.openDashboardState: | ||
128 | + case WidgetActionType.updateDashboardState: | ||
129 | + this.actionTypeFormGroup.addControl( | ||
130 | + 'targetDashboardStateId', | ||
131 | + this.fb.control(action ? action.targetDashboardStateId : null, | ||
132 | + type === WidgetActionType.openDashboardState ? [Validators.required] : []) | ||
133 | + ); | ||
134 | + this.actionTypeFormGroup.addControl( | ||
135 | + 'setEntityId', | ||
136 | + this.fb.control(action ? action.setEntityId : true, []) | ||
137 | + ); | ||
138 | + this.actionTypeFormGroup.addControl( | ||
139 | + 'stateEntityParamName', | ||
140 | + this.fb.control(action ? action.stateEntityParamName : null, []) | ||
141 | + ); | ||
142 | + if (type === WidgetActionType.openDashboard) { | ||
143 | + this.actionTypeFormGroup.addControl( | ||
144 | + 'targetDashboardId', | ||
145 | + this.fb.control(action ? action.targetDashboardId : null, | ||
146 | + [Validators.required]) | ||
147 | + ); | ||
148 | + this.setupSelectedDashboardStateIds(action ? action.targetDashboardId : null); | ||
149 | + } else { | ||
150 | + this.actionTypeFormGroup.addControl( | ||
151 | + 'openRightLayout', | ||
152 | + this.fb.control(action ? action.openRightLayout : false, []) | ||
153 | + ); | ||
154 | + } | ||
155 | + this.setupFilteredDashboardStates(); | ||
156 | + break; | ||
157 | + case WidgetActionType.custom: | ||
158 | + this.actionTypeFormGroup.addControl( | ||
159 | + 'customFunction', | ||
160 | + this.fb.control(action ? action.customFunction : null, []) | ||
161 | + ); | ||
162 | + break; | ||
163 | + case WidgetActionType.customPretty: | ||
164 | + this.actionTypeFormGroup.addControl( | ||
165 | + 'customAction', | ||
166 | + this.fb.control(toCustomAction(action), [Validators.required]) | ||
167 | + ); | ||
168 | + break; | ||
169 | + } | ||
170 | + } | ||
171 | + } | ||
172 | + | ||
173 | + private setupSelectedDashboardStateIds(targetDashboardId?: string) { | ||
174 | + this.selectedDashboardStateIds = | ||
175 | + this.actionTypeFormGroup.get('targetDashboardId').valueChanges.pipe( | ||
176 | + // startWith<string>(targetDashboardId), | ||
177 | + tap(() => { | ||
178 | + this.targetDashboardStateSearchText = ''; | ||
179 | + }), | ||
180 | + mergeMap((dashboardId) => { | ||
181 | + if (dashboardId) { | ||
182 | + return this.dashboardService.getDashboard(dashboardId); | ||
183 | + } else { | ||
184 | + return of(null); | ||
185 | + } | ||
186 | + }), | ||
187 | + map((dashboard: Dashboard) => { | ||
188 | + if (dashboard) { | ||
189 | + dashboard = this.dashboardUtils.validateAndUpdateDashboard(dashboard); | ||
190 | + const states = dashboard.configuration.states; | ||
191 | + return Object.keys(states); | ||
192 | + } else { | ||
193 | + return []; | ||
194 | + } | ||
195 | + }) | ||
196 | + ); | ||
197 | + } | ||
198 | + | ||
199 | + private setupFilteredDashboardStates() { | ||
200 | + this.targetDashboardStateSearchText = ''; | ||
201 | + this.filteredDashboardStates = this.actionTypeFormGroup.get('targetDashboardStateId').valueChanges | ||
202 | + .pipe( | ||
203 | + startWith(''), | ||
204 | + map(value => value ? value : ''), | ||
205 | + mergeMap(name => this.fetchDashboardStates(name) ) | ||
206 | + ); | ||
207 | + } | ||
208 | + | ||
209 | + private fetchDashboardStates(searchText?: string): Observable<Array<string>> { | ||
210 | + this.targetDashboardStateSearchText = searchText; | ||
211 | + if (this.widgetActionFormGroup.get('type').value === WidgetActionType.openDashboard) { | ||
212 | + return this.selectedDashboardStateIds.pipe( | ||
213 | + map(stateIds => { | ||
214 | + const result = searchText ? stateIds.filter(this.createFilterForDashboardState(searchText)) : stateIds; | ||
215 | + if (result && result.length) { | ||
216 | + return result; | ||
217 | + } else { | ||
218 | + return [searchText]; | ||
219 | + } | ||
220 | + }) | ||
221 | + ); | ||
222 | + } else { | ||
223 | + return of(this.data.callbacks.fetchDashboardStates(searchText)); | ||
224 | + } | ||
225 | + } | ||
226 | + | ||
227 | + private createFilterForDashboardState(query: string): (stateId: string) => boolean { | ||
228 | + const lowercaseQuery = query.toLowerCase(); | ||
229 | + return stateId => stateId.toLowerCase().indexOf(lowercaseQuery) === 0; | ||
230 | + } | ||
231 | + | ||
232 | + public clearTargetDashboardState(value: string = '') { | ||
233 | + this.dashboardStateInput.nativeElement.value = value; | ||
234 | + this.actionTypeFormGroup.get('targetDashboardStateId').patchValue(value, {emitEvent: true}); | ||
235 | + setTimeout(() => { | ||
236 | + this.dashboardStateInput.nativeElement.blur(); | ||
237 | + this.dashboardStateInput.nativeElement.focus(); | ||
238 | + }, 0); | ||
239 | + } | ||
240 | + | ||
241 | + private validateActionName(): ValidatorFn { | ||
242 | + return (c: FormControl) => { | ||
243 | + const newName = c.value; | ||
244 | + const valid = this.checkActionName(newName, this.widgetActionFormGroup.get('actionSourceId').value); | ||
245 | + return !valid ? { | ||
246 | + actionNameNotUnique: true | ||
247 | + } : null; | ||
248 | + }; | ||
249 | + } | ||
250 | + | ||
251 | + private checkActionName(name: string, actionSourceId: string): boolean { | ||
252 | + let actionNameIsUnique = true; | ||
253 | + if (name && actionSourceId) { | ||
254 | + const sourceActions = this.data.actionsData.actionsMap[actionSourceId]; | ||
255 | + if (sourceActions) { | ||
256 | + const result = sourceActions.filter((sourceAction) => sourceAction.name === name); | ||
257 | + if (result && result.length && result[0].id !== this.action.id) { | ||
258 | + actionNameIsUnique = false; | ||
259 | + } | ||
260 | + } | ||
261 | + } | ||
262 | + return actionNameIsUnique; | ||
263 | + } | ||
264 | + | ||
265 | + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { | ||
266 | + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); | ||
267 | + const customErrorState = !!(control && control.invalid && this.submitted); | ||
268 | + return originalErrorState || customErrorState; | ||
269 | + } | ||
270 | + | ||
271 | + public actionSourceName(actionSource: WidgetActionSource): string { | ||
272 | + if (actionSource) { | ||
273 | + return this.utils.customTranslation(actionSource.name, actionSource.name); | ||
274 | + } else { | ||
275 | + return ''; | ||
276 | + } | ||
277 | + } | ||
278 | + | ||
279 | + cancel(): void { | ||
280 | + this.dialogRef.close(null); | ||
281 | + } | ||
282 | + | ||
283 | + save(): void { | ||
284 | + this.submitted = true; | ||
285 | + const type: WidgetActionType = this.widgetActionFormGroup.get('type').value; | ||
286 | + let result: WidgetActionDescriptorInfo; | ||
287 | + if (type === WidgetActionType.customPretty) { | ||
288 | + result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.get('customAction').value}; | ||
289 | + } else { | ||
290 | + result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.value}; | ||
291 | + } | ||
292 | + result.id = this.action.id; | ||
293 | + this.dialogRef.close(result); | ||
294 | + } | ||
295 | +} |
@@ -16,7 +16,7 @@ | @@ -16,7 +16,7 @@ | ||
16 | 16 | ||
17 | import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; | 17 | import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; |
18 | import { | 18 | import { |
19 | - AfterViewInit, | 19 | + AfterViewInit, ChangeDetectionStrategy, |
20 | Component, | 20 | Component, |
21 | ElementRef, | 21 | ElementRef, |
22 | forwardRef, | 22 | forwardRef, |
@@ -413,7 +413,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie | @@ -413,7 +413,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie | ||
413 | let fetchObservable: Observable<Array<DataKey>> = null; | 413 | let fetchObservable: Observable<Array<DataKey>> = null; |
414 | if (this.datasourceType === DatasourceType.function || this.widgetType === widgetType.alarm) { | 414 | if (this.datasourceType === DatasourceType.function || this.widgetType === widgetType.alarm) { |
415 | const dataKeyFilter = this.createDataKeyFilter(this.searchText); | 415 | const dataKeyFilter = this.createDataKeyFilter(this.searchText); |
416 | - const targetKeysList = this.datasourceType === DatasourceType.function ? this.functionTypeKeys : this.alarmKeys; | 416 | + const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys; |
417 | fetchObservable = of(targetKeysList.filter(dataKeyFilter)); | 417 | fetchObservable = of(targetKeysList.filter(dataKeyFilter)); |
418 | } else { | 418 | } else { |
419 | if (this.entityAliasId) { | 419 | if (this.entityAliasId) { |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<form [formGroup]="legendConfigForm"> | ||
19 | + <fieldset [disabled]="(isLoading$ | async)"> | ||
20 | + <div class="mat-content" style="height: 100%;"> | ||
21 | + <div class="mat-padding"> | ||
22 | + <section fxLayout="column"> | ||
23 | + <mat-form-field> | ||
24 | + <mat-label translate>legend.direction</mat-label> | ||
25 | + <mat-select matInput formControlName="direction" style="min-width: 150px;"> | ||
26 | + <mat-option *ngFor="let direction of legendDirections" [value]="direction"> | ||
27 | + {{ legendDirectionTranslations.get(direction) | translate }} | ||
28 | + </mat-option> | ||
29 | + </mat-select> | ||
30 | + </mat-form-field> | ||
31 | + <mat-form-field> | ||
32 | + <mat-label translate>legend.position</mat-label> | ||
33 | + <mat-select matInput formControlName="position" style="min-width: 150px;"> | ||
34 | + <mat-option *ngFor="let pos of legendPositions" [value]="pos" | ||
35 | + [disabled]="legendConfigForm.get('direction').value === legendDirection.row && | ||
36 | + (pos === legendPosition.left || pos === legendPosition.right)"> | ||
37 | + {{ legendPositionTranslations.get(pos) | translate }} | ||
38 | + </mat-option> | ||
39 | + </mat-select> | ||
40 | + </mat-form-field> | ||
41 | + <mat-checkbox fxFlex formControlName="showMin"> | ||
42 | + {{ 'legend.show-min' | translate }} | ||
43 | + </mat-checkbox> | ||
44 | + <mat-checkbox fxFlex formControlName="showMax"> | ||
45 | + {{ 'legend.show-max' | translate }} | ||
46 | + </mat-checkbox> | ||
47 | + <mat-checkbox fxFlex formControlName="showAvg"> | ||
48 | + {{ 'legend.show-avg' | translate }} | ||
49 | + </mat-checkbox> | ||
50 | + <mat-checkbox fxFlex formControlName="showTotal"> | ||
51 | + {{ 'legend.show-total' | translate }} | ||
52 | + </mat-checkbox> | ||
53 | + </section> | ||
54 | + </div> | ||
55 | + </div> | ||
56 | + </fieldset> | ||
57 | +</form> |
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 | +:host { | ||
17 | + width: 100%; | ||
18 | + height: 100%; | ||
19 | + form, | ||
20 | + fieldset { | ||
21 | + height: 100%; | ||
22 | + } | ||
23 | + | ||
24 | + .mat-content { | ||
25 | + overflow: hidden; | ||
26 | + background-color: #fff; | ||
27 | + } | ||
28 | + | ||
29 | + .mat-padding { | ||
30 | + padding: 16px; | ||
31 | + } | ||
32 | +} |
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, Inject, InjectionToken, OnInit, ViewContainerRef } from '@angular/core'; | ||
18 | +import { Overlay, OverlayRef } from '@angular/cdk/overlay'; | ||
19 | +import { PageComponent } from '@shared/components/page.component'; | ||
20 | +import { Store } from '@ngrx/store'; | ||
21 | +import { AppState } from '@core/core.state'; | ||
22 | +import { FormBuilder, FormGroup } from '@angular/forms'; | ||
23 | +import { | ||
24 | + LegendConfig, | ||
25 | + LegendDirection, | ||
26 | + legendDirectionTranslationMap, | ||
27 | + LegendPosition, | ||
28 | + legendPositionTranslationMap | ||
29 | +} from '@shared/models/widget.models'; | ||
30 | + | ||
31 | +export const LEGEND_CONFIG_PANEL_DATA = new InjectionToken<any>('LegendConfigPanelData'); | ||
32 | + | ||
33 | +export interface LegendConfigPanelData { | ||
34 | + legendConfig: LegendConfig; | ||
35 | + legendConfigUpdated: (legendConfig: LegendConfig) => void; | ||
36 | +} | ||
37 | + | ||
38 | +@Component({ | ||
39 | + selector: 'tb-legend-config-panel', | ||
40 | + templateUrl: './legend-config-panel.component.html', | ||
41 | + styleUrls: ['./legend-config-panel.component.scss'] | ||
42 | +}) | ||
43 | +export class LegendConfigPanelComponent extends PageComponent implements OnInit { | ||
44 | + | ||
45 | + legendConfigForm: FormGroup; | ||
46 | + | ||
47 | + legendDirection = LegendDirection; | ||
48 | + | ||
49 | + legendDirections = Object.keys(LegendDirection); | ||
50 | + | ||
51 | + legendDirectionTranslations = legendDirectionTranslationMap; | ||
52 | + | ||
53 | + legendPosition = LegendPosition; | ||
54 | + | ||
55 | + legendPositions = Object.keys(LegendPosition); | ||
56 | + | ||
57 | + legendPositionTranslations = legendPositionTranslationMap; | ||
58 | + | ||
59 | + constructor(@Inject(LEGEND_CONFIG_PANEL_DATA) public data: LegendConfigPanelData, | ||
60 | + public overlayRef: OverlayRef, | ||
61 | + protected store: Store<AppState>, | ||
62 | + public fb: FormBuilder, | ||
63 | + private overlay: Overlay, | ||
64 | + public viewContainerRef: ViewContainerRef) { | ||
65 | + super(store); | ||
66 | + } | ||
67 | + | ||
68 | + ngOnInit(): void { | ||
69 | + this.legendConfigForm = this.fb.group({ | ||
70 | + direction: [this.data.legendConfig.direction, []], | ||
71 | + position: [this.data.legendConfig.position, []], | ||
72 | + showMin: [this.data.legendConfig.showMin, []], | ||
73 | + showMax: [this.data.legendConfig.showMax, []], | ||
74 | + showAvg: [this.data.legendConfig.showAvg, []], | ||
75 | + showTotal: [this.data.legendConfig.showTotal, []] | ||
76 | + }); | ||
77 | + this.legendConfigForm.get('direction').valueChanges.subscribe((direction: LegendDirection) => { | ||
78 | + this.onDirectionChanged(direction); | ||
79 | + }); | ||
80 | + this.onDirectionChanged(this.data.legendConfig.direction); | ||
81 | + this.legendConfigForm.valueChanges.subscribe(() => { | ||
82 | + this.update(); | ||
83 | + }); | ||
84 | + } | ||
85 | + | ||
86 | + private onDirectionChanged(direction: LegendDirection) { | ||
87 | + if (direction === LegendDirection.row) { | ||
88 | + let position: LegendPosition = this.legendConfigForm.get('position').value; | ||
89 | + if (position !== LegendPosition.bottom && position !== LegendPosition.top) { | ||
90 | + position = LegendPosition.bottom; | ||
91 | + } | ||
92 | + this.legendConfigForm.patchValue( | ||
93 | + { | ||
94 | + position, | ||
95 | + showMin: false, | ||
96 | + showMax: false, | ||
97 | + showAvg: false, | ||
98 | + showTotal: false | ||
99 | + }, {emitEvent: false} | ||
100 | + ); | ||
101 | + this.legendConfigForm.get('showMin').disable({emitEvent: false}); | ||
102 | + this.legendConfigForm.get('showMax').disable({emitEvent: false}); | ||
103 | + this.legendConfigForm.get('showAvg').disable({emitEvent: false}); | ||
104 | + this.legendConfigForm.get('showTotal').disable({emitEvent: false}); | ||
105 | + } else { | ||
106 | + this.legendConfigForm.get('showMin').enable({emitEvent: false}); | ||
107 | + this.legendConfigForm.get('showMax').enable({emitEvent: false}); | ||
108 | + this.legendConfigForm.get('showAvg').enable({emitEvent: false}); | ||
109 | + this.legendConfigForm.get('showTotal').enable({emitEvent: false}); | ||
110 | + } | ||
111 | + } | ||
112 | + | ||
113 | + update() { | ||
114 | + const newLegendConfig: LegendConfig = this.legendConfigForm.value; | ||
115 | + this.data.legendConfigUpdated(newLegendConfig); | ||
116 | + } | ||
117 | + | ||
118 | +} |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<button cdkOverlayOrigin #legendConfigPanelOrigin="cdkOverlayOrigin" [disabled]="disabled" | ||
19 | + mat-button mat-raised-button color="primary" (click)="openEditMode($event)"> | ||
20 | + <mat-icon class="material-icons">toc</mat-icon> | ||
21 | + <span translate>legend.settings</span> | ||
22 | +</button> |
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 { | ||
18 | + Component, | ||
19 | + forwardRef, Inject, | ||
20 | + Input, | ||
21 | + OnDestroy, | ||
22 | + OnInit, | ||
23 | + ViewChild, | ||
24 | + ViewContainerRef | ||
25 | +} from '@angular/core'; | ||
26 | +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | ||
27 | +import { TranslateService } from '@ngx-translate/core'; | ||
28 | +import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe'; | ||
29 | +import { | ||
30 | + HistoryWindowType, | ||
31 | + Timewindow, | ||
32 | + TimewindowType, | ||
33 | + initModelFromDefaultTimewindow, cloneSelectedTimewindow | ||
34 | +} from '@shared/models/time/time.models'; | ||
35 | +import { DatePipe } from '@angular/common'; | ||
36 | +import { | ||
37 | + Overlay, | ||
38 | + CdkOverlayOrigin, | ||
39 | + OverlayConfig, | ||
40 | + OverlayPositionBuilder, ConnectedPosition, PositionStrategy, OverlayRef | ||
41 | +} from '@angular/cdk/overlay'; | ||
42 | +import { | ||
43 | + TIMEWINDOW_PANEL_DATA, | ||
44 | + TimewindowPanelComponent, | ||
45 | + TimewindowPanelData | ||
46 | +} from '@shared/components/time/timewindow-panel.component'; | ||
47 | +import { ComponentPortal, PortalInjector } from '@angular/cdk/portal'; | ||
48 | +import { MediaBreakpoints } from '@shared/models/constants'; | ||
49 | +import { BreakpointObserver } from '@angular/cdk/layout'; | ||
50 | +import { DOCUMENT } from '@angular/common'; | ||
51 | +import { WINDOW } from '@core/services/window.service'; | ||
52 | +import { TimeService } from '@core/services/time.service'; | ||
53 | +import { TooltipPosition } from '@angular/material/typings/tooltip'; | ||
54 | +import { deepClone } from '@core/utils'; | ||
55 | +import { LegendConfig } from '@shared/models/widget.models'; | ||
56 | +import { | ||
57 | + LEGEND_CONFIG_PANEL_DATA, | ||
58 | + LegendConfigPanelComponent, | ||
59 | + LegendConfigPanelData | ||
60 | +} from '@home/components/widget/legend-config-panel.component'; | ||
61 | + | ||
62 | +@Component({ | ||
63 | + selector: 'tb-legend-config', | ||
64 | + templateUrl: './legend-config.component.html', | ||
65 | + styleUrls: [], | ||
66 | + providers: [ | ||
67 | + { | ||
68 | + provide: NG_VALUE_ACCESSOR, | ||
69 | + useExisting: forwardRef(() => LegendConfigComponent), | ||
70 | + multi: true | ||
71 | + } | ||
72 | + ] | ||
73 | +}) | ||
74 | +export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAccessor { | ||
75 | + | ||
76 | + @Input() disabled: boolean; | ||
77 | + | ||
78 | + @ViewChild('legendConfigPanelOrigin', {static: false}) legendConfigPanelOrigin: CdkOverlayOrigin; | ||
79 | + | ||
80 | + innerValue: LegendConfig; | ||
81 | + | ||
82 | + private propagateChange = (_: any) => {}; | ||
83 | + | ||
84 | + constructor(private overlay: Overlay, | ||
85 | + public viewContainerRef: ViewContainerRef, | ||
86 | + public breakpointObserver: BreakpointObserver, | ||
87 | + @Inject(DOCUMENT) private document: Document, | ||
88 | + @Inject(WINDOW) private window: Window) { | ||
89 | + } | ||
90 | + | ||
91 | + ngOnInit(): void { | ||
92 | + } | ||
93 | + | ||
94 | + ngOnDestroy(): void { | ||
95 | + } | ||
96 | + | ||
97 | + openEditMode() { | ||
98 | + if (this.disabled) { | ||
99 | + return; | ||
100 | + } | ||
101 | + const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']); | ||
102 | + const position = this.overlay.position(); | ||
103 | + const config = new OverlayConfig({ | ||
104 | + panelClass: 'tb-legend-config-panel', | ||
105 | + backdropClass: 'cdk-overlay-transparent-backdrop', | ||
106 | + hasBackdrop: isGtSm, | ||
107 | + }); | ||
108 | + if (isGtSm) { | ||
109 | + config.minWidth = '220px'; | ||
110 | + config.maxHeight = '300px'; | ||
111 | + const panelHeight = 220; | ||
112 | + const panelWidth = 220; | ||
113 | + const el = this.legendConfigPanelOrigin.elementRef.nativeElement; | ||
114 | + const offset = el.getBoundingClientRect(); | ||
115 | + const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0; | ||
116 | + const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0; | ||
117 | + const bottomY = offset.bottom - scrollTop; | ||
118 | + const leftX = offset.left - scrollLeft; | ||
119 | + let originX; | ||
120 | + let originY; | ||
121 | + let overlayX; | ||
122 | + let overlayY; | ||
123 | + const wHeight = this.document.documentElement.clientHeight; | ||
124 | + const wWidth = this.document.documentElement.clientWidth; | ||
125 | + if (bottomY + panelHeight > wHeight) { | ||
126 | + originY = 'top'; | ||
127 | + overlayY = 'bottom'; | ||
128 | + } else { | ||
129 | + originY = 'bottom'; | ||
130 | + overlayY = 'top'; | ||
131 | + } | ||
132 | + if (leftX + panelWidth > wWidth) { | ||
133 | + originX = 'end'; | ||
134 | + overlayX = 'end'; | ||
135 | + } else { | ||
136 | + originX = 'start'; | ||
137 | + overlayX = 'start'; | ||
138 | + } | ||
139 | + const connectedPosition: ConnectedPosition = { | ||
140 | + originX, | ||
141 | + originY, | ||
142 | + overlayX, | ||
143 | + overlayY | ||
144 | + }; | ||
145 | + config.positionStrategy = position.flexibleConnectedTo(this.legendConfigPanelOrigin.elementRef) | ||
146 | + .withPositions([connectedPosition]); | ||
147 | + } else { | ||
148 | + config.minWidth = '100%'; | ||
149 | + config.minHeight = '100%'; | ||
150 | + config.positionStrategy = position.global().top('0%').left('0%') | ||
151 | + .right('0%').bottom('0%'); | ||
152 | + } | ||
153 | + | ||
154 | + const overlayRef = this.overlay.create(config); | ||
155 | + | ||
156 | + overlayRef.backdropClick().subscribe(() => { | ||
157 | + overlayRef.dispose(); | ||
158 | + }); | ||
159 | + | ||
160 | + const injector = this._createLegendConfigPanelInjector( | ||
161 | + overlayRef, | ||
162 | + { | ||
163 | + legendConfig: deepClone(this.innerValue), | ||
164 | + legendConfigUpdated: this.legendConfigUpdated.bind(this) | ||
165 | + } | ||
166 | + ); | ||
167 | + | ||
168 | + overlayRef.attach(new ComponentPortal(LegendConfigPanelComponent, this.viewContainerRef, injector)); | ||
169 | + } | ||
170 | + | ||
171 | + private _createLegendConfigPanelInjector(overlayRef: OverlayRef, data: LegendConfigPanelData): PortalInjector { | ||
172 | + const injectionTokens = new WeakMap<any, any>([ | ||
173 | + [LEGEND_CONFIG_PANEL_DATA, data], | ||
174 | + [OverlayRef, overlayRef] | ||
175 | + ]); | ||
176 | + return new PortalInjector(this.viewContainerRef.injector, injectionTokens); | ||
177 | + } | ||
178 | + | ||
179 | + registerOnChange(fn: any): void { | ||
180 | + this.propagateChange = fn; | ||
181 | + } | ||
182 | + | ||
183 | + registerOnTouched(fn: any): void { | ||
184 | + } | ||
185 | + | ||
186 | + setDisabledState(isDisabled: boolean): void { | ||
187 | + this.disabled = isDisabled; | ||
188 | + } | ||
189 | + | ||
190 | + writeValue(obj: LegendConfig): void { | ||
191 | + this.innerValue = obj; | ||
192 | + } | ||
193 | + | ||
194 | + private legendConfigUpdated(legendConfig: LegendConfig) { | ||
195 | + this.innerValue = legendConfig; | ||
196 | + this.propagateChange(this.innerValue); | ||
197 | + } | ||
198 | +} |
@@ -181,6 +181,188 @@ | @@ -181,6 +181,188 @@ | ||
181 | </tb-entity-alias-select> | 181 | </tb-entity-alias-select> |
182 | </div> | 182 | </div> |
183 | </mat-expansion-panel> | 183 | </mat-expansion-panel> |
184 | + <mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.alarm && | ||
185 | + modelValue?.isDataEnabled" [expanded]="true"> | ||
186 | + <mat-expansion-panel-header> | ||
187 | + <mat-panel-title> | ||
188 | + {{ 'widget-config.alarm-source' | translate }} | ||
189 | + </mat-panel-title> | ||
190 | + </mat-expansion-panel-header> | ||
191 | + <div [formGroup]="alarmSourceSettings" style="padding: 0 5px;"> | ||
192 | + <section fxFlex | ||
193 | + fxLayout="column" | ||
194 | + fxLayoutAlign="center" | ||
195 | + fxLayout.gt-sm="row" | ||
196 | + fxLayoutAlign.gt-sm="start center"> | ||
197 | + <mat-form-field class="tb-datasource-type"> | ||
198 | + <mat-select matInput formControlName="type"> | ||
199 | + <mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType"> | ||
200 | + {{ datasourceTypesTranslations.get(datasourceType) | translate }} | ||
201 | + </mat-option> | ||
202 | + </mat-select> | ||
203 | + </mat-form-field> | ||
204 | + <section class="tb-datasource" [ngSwitch]="alarmSourceSettings.get('type').value"> | ||
205 | + <ng-template [ngSwitchCase]="datasourceType.function"> | ||
206 | + <mat-form-field floatLabel="always" [fxShow]="widgetType !== widgetTypes.alarm" | ||
207 | + class="tb-datasource-name" style="min-width: 200px;"> | ||
208 | + <mat-label></mat-label> | ||
209 | + <input matInput | ||
210 | + placeholder="{{ 'datasource.name' | translate }}" | ||
211 | + formControlName="name"> | ||
212 | + </mat-form-field> | ||
213 | + </ng-template> | ||
214 | + <ng-template [ngSwitchCase]="datasourceType.entity"> | ||
215 | + <tb-entity-alias-select | ||
216 | + [tbRequired]="alarmSourceSettings.get('type').value === datasourceType.entity" | ||
217 | + [aliasController]="aliasController" | ||
218 | + formControlName="entityAliasId" | ||
219 | + [callbacks]="widgetConfigCallbacks"> | ||
220 | + </tb-entity-alias-select> | ||
221 | + </ng-template> | ||
222 | + </section> | ||
223 | + <tb-data-keys class="tb-data-keys" fxFlex | ||
224 | + [widgetType]="widgetType" | ||
225 | + [datasourceType]="alarmSourceSettings.get('type').value" | ||
226 | + [aliasController]="aliasController" | ||
227 | + [datakeySettingsSchema]="modelValue?.dataKeySettingsSchema" | ||
228 | + [callbacks]="widgetConfigCallbacks" | ||
229 | + [entityAliasId]="alarmSourceSettings.get('entityAliasId').value" | ||
230 | + formControlName="dataKeys"> | ||
231 | + </tb-data-keys> | ||
232 | + </section> | ||
233 | + </div> | ||
234 | + </mat-expansion-panel> | ||
235 | + </div> | ||
236 | + </mat-tab> | ||
237 | + <mat-tab label="{{ 'widget-config.settings' | translate }}"> | ||
238 | + <div class="mat-content mat-padding" | ||
239 | + fxLayout="column" fxLayoutGap="8px"> | ||
240 | + <div [formGroup]="widgetSettings" fxLayout="column" fxLayoutGap="8px"> | ||
241 | + <span translate>widget-config.general-settings</span> | ||
242 | + <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center"> | ||
243 | + <mat-form-field fxFlex class="mat-block"> | ||
244 | + <mat-label translate>widget-config.title</mat-label> | ||
245 | + <input matInput formControlName="title"> | ||
246 | + </mat-form-field> | ||
247 | + <div fxFlex [fxShow]="widgetSettings.get('showTitle').value"> | ||
248 | + <tb-json-object-edit | ||
249 | + [editorStyle]="{minHeight: '100px'}" | ||
250 | + required | ||
251 | + label="{{ 'widget-config.title-style' | translate }}" | ||
252 | + formControlName="titleStyle" | ||
253 | + ></tb-json-object-edit> | ||
254 | + </div> | ||
255 | + </div> | ||
256 | + <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center" | ||
257 | + fxLayoutGap="8px"> | ||
258 | + <div fxLayout="row" fxFlex> | ||
259 | + <mat-checkbox formControlName="showTitleIcon"> | ||
260 | + {{ 'widget-config.display-icon' | translate }} | ||
261 | + </mat-checkbox> | ||
262 | + </div> | ||
263 | + <div fxFlex> | ||
264 | + <tb-material-icon-select | ||
265 | + formControlName="titleIcon"> | ||
266 | + </tb-material-icon-select> | ||
267 | + </div> | ||
268 | + <tb-color-input fxFlex | ||
269 | + label="{{'widget-config.icon-color' | translate}}" | ||
270 | + icon="format_color_fill" | ||
271 | + openOnInput | ||
272 | + formControlName="iconColor"> | ||
273 | + </tb-color-input> | ||
274 | + <mat-form-field fxFlex> | ||
275 | + <mat-label translate>widget-config.icon-size</mat-label> | ||
276 | + <input matInput formControlName="iconSize"> | ||
277 | + </mat-form-field> | ||
278 | + </div> | ||
279 | + <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center" | ||
280 | + fxLayoutGap="8px"> | ||
281 | + <div fxLayout="row"> | ||
282 | + <mat-checkbox formControlName="showTitle"> | ||
283 | + {{ 'widget-config.display-title' | translate }} | ||
284 | + </mat-checkbox> | ||
285 | + </div> | ||
286 | + <div fxLayout="row"> | ||
287 | + <mat-checkbox formControlName="dropShadow"> | ||
288 | + {{ 'widget-config.drop-shadow' | translate }} | ||
289 | + </mat-checkbox> | ||
290 | + </div> | ||
291 | + <div fxLayout="row"> | ||
292 | + <mat-checkbox formControlName="enableFullscreen"> | ||
293 | + {{ 'widget-config.enable-fullscreen' | translate }} | ||
294 | + </mat-checkbox> | ||
295 | + </div> | ||
296 | + <div fxFlex> | ||
297 | + <tb-json-object-edit | ||
298 | + [editorStyle]="{minHeight: '100px'}" | ||
299 | + required | ||
300 | + label="{{ 'widget-config.widget-style' | translate }}" | ||
301 | + formControlName="widgetStyle" | ||
302 | + ></tb-json-object-edit> | ||
303 | + </div> | ||
304 | + </div> | ||
305 | + <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center" | ||
306 | + fxLayoutGap="8px"> | ||
307 | + <tb-color-input fxFlex | ||
308 | + label="{{'widget-config.background-color' | translate}}" | ||
309 | + icon="format_color_fill" | ||
310 | + openOnInput | ||
311 | + formControlName="backgroundColor"> | ||
312 | + </tb-color-input> | ||
313 | + <tb-color-input fxFlex | ||
314 | + label="{{'widget-config.text-color' | translate}}" | ||
315 | + icon="format_color_fill" | ||
316 | + openOnInput | ||
317 | + formControlName="color"> | ||
318 | + </tb-color-input> | ||
319 | + <mat-form-field fxFlex> | ||
320 | + <mat-label translate>widget-config.padding</mat-label> | ||
321 | + <input matInput formControlName="padding"> | ||
322 | + </mat-form-field> | ||
323 | + <mat-form-field fxFlex> | ||
324 | + <mat-label translate>widget-config.margin</mat-label> | ||
325 | + <input matInput formControlName="margin"> | ||
326 | + </mat-form-field> | ||
327 | + </div> | ||
328 | + <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center" | ||
329 | + fxLayoutGap="8px"> | ||
330 | + <mat-form-field fxFlex> | ||
331 | + <mat-label translate>widget-config.units</mat-label> | ||
332 | + <input matInput formControlName="units"> | ||
333 | + </mat-form-field> | ||
334 | + <mat-form-field fxFlex> | ||
335 | + <mat-label translate>widget-config.decimals</mat-label> | ||
336 | + <input matInput formControlName="decimals" type="number" min="0" max="15" step="1"> | ||
337 | + </mat-form-field> | ||
338 | + </div> | ||
339 | + <div [fxShow]="widgetType === widgetTypes.timeseries || | ||
340 | + widgetType === widgetTypes.latest" | ||
341 | + fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center" | ||
342 | + fxLayoutGap="8px"> | ||
343 | + <mat-checkbox fxFlex formControlName="showLegend"> | ||
344 | + {{ 'widget-config.display-legend' | translate }} | ||
345 | + </mat-checkbox> | ||
346 | + <section fxFlex fxLayout="row" fxLayoutAlign="start center" style="margin-bottom: 16px;"> | ||
347 | + <tb-legend-config formControlName="legendConfig"> | ||
348 | + </tb-legend-config> | ||
349 | + </section> | ||
350 | + </div> | ||
351 | + </div> | ||
352 | + <div [formGroup]="layoutSettings" fxLayout="column" fxLayoutGap="8px"> | ||
353 | + <span translate>widget-config.mobile-mode-settings</span> | ||
354 | + <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-sm="row" fxLayoutAlign.gt-sm="start center" | ||
355 | + fxLayoutGap="8px"> | ||
356 | + <mat-form-field fxFlex> | ||
357 | + <mat-label translate>widget-config.order</mat-label> | ||
358 | + <input matInput formControlName="mobileOrder" type="number" step="1"> | ||
359 | + </mat-form-field> | ||
360 | + <mat-form-field fxFlex> | ||
361 | + <mat-label translate>widget-config.height</mat-label> | ||
362 | + <input matInput formControlName="mobileHeight" type="number" min="1" max="10" step="1"> | ||
363 | + </mat-form-field> | ||
364 | + </div> | ||
365 | + </div> | ||
184 | </div> | 366 | </div> |
185 | </mat-tab> | 367 | </mat-tab> |
186 | <mat-tab *ngIf="displayAdvanced()" label="{{ 'widget-config.advanced' | translate }}"> | 368 | <mat-tab *ngIf="displayAdvanced()" label="{{ 'widget-config.advanced' | translate }}"> |
@@ -191,6 +373,10 @@ | @@ -191,6 +373,10 @@ | ||
191 | </tb-json-form> | 373 | </tb-json-form> |
192 | </div> | 374 | </div> |
193 | </mat-tab> | 375 | </mat-tab> |
194 | - <mat-tab label="{{ 'widget-config.actions' | translate }}"> | 376 | + <mat-tab label="{{ 'widget-config.actions' | translate }}" [formGroup]="actionsSettings"> |
377 | + <tb-manage-widget-actions | ||
378 | + [callbacks]="widgetConfigCallbacks" | ||
379 | + formControlName="actionsData"> | ||
380 | + </tb-manage-widget-actions> | ||
195 | </mat-tab> | 381 | </mat-tab> |
196 | </mat-tab-group> | 382 | </mat-tab-group> |
@@ -16,5 +16,6 @@ | @@ -16,5 +16,6 @@ | ||
16 | 16 | ||
17 | import { EntityAliasSelectCallbacks } from '../alias/entity-alias-select.component.models'; | 17 | import { EntityAliasSelectCallbacks } from '../alias/entity-alias-select.component.models'; |
18 | import { DataKeysCallbacks } from './data-keys.component.models'; | 18 | import { DataKeysCallbacks } from './data-keys.component.models'; |
19 | +import { WidgetActionCallbacks } from './action/manage-widget-actions.component.models'; | ||
19 | 20 | ||
20 | -export type WidgetConfigCallbacks = EntityAliasSelectCallbacks & DataKeysCallbacks; | 21 | +export type WidgetConfigCallbacks = EntityAliasSelectCallbacks & DataKeysCallbacks & WidgetActionCallbacks; |
@@ -21,6 +21,9 @@ | @@ -21,6 +21,9 @@ | ||
21 | .tb-advanced-widget-config { | 21 | .tb-advanced-widget-config { |
22 | height: 100%; | 22 | height: 100%; |
23 | } | 23 | } |
24 | + .tb-advanced-widget-config { | ||
25 | + height: 100%; | ||
26 | + } | ||
24 | .tb-datasource-type { | 27 | .tb-datasource-type { |
25 | min-width: 110px; | 28 | min-width: 110px; |
26 | } | 29 | } |
@@ -45,6 +48,13 @@ | @@ -45,6 +48,13 @@ | ||
45 | 48 | ||
46 | :host ::ng-deep { | 49 | :host ::ng-deep { |
47 | .tb-widget-config { | 50 | .tb-widget-config { |
51 | + .mat-tab-body-wrapper { | ||
52 | + position: absolute; | ||
53 | + top: 49px; | ||
54 | + left: 0; | ||
55 | + right: 0; | ||
56 | + bottom: 0; | ||
57 | + } | ||
48 | .mat-tab-body.mat-tab-body-active { | 58 | .mat-tab-body.mat-tab-body-active { |
49 | .mat-tab-body-content > div { | 59 | .mat-tab-body-content > div { |
50 | height: 100%; | 60 | height: 100%; |
@@ -14,19 +14,17 @@ | @@ -14,19 +14,17 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, forwardRef, Input, OnInit } from '@angular/core'; | 17 | +import { ChangeDetectionStrategy, Component, forwardRef, Input, OnInit } from '@angular/core'; |
18 | import { PageComponent } from '@shared/components/page.component'; | 18 | 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 { | 21 | import { |
22 | DataKey, | 22 | DataKey, |
23 | Datasource, | 23 | Datasource, |
24 | - DatasourceType, datasourceTypeTranslationMap, | ||
25 | - LegendConfig, | 24 | + DatasourceType, |
25 | + datasourceTypeTranslationMap, defaultLegendConfig, | ||
26 | WidgetActionDescriptor, | 26 | WidgetActionDescriptor, |
27 | - WidgetActionSource, | ||
28 | - widgetType, | ||
29 | - WidgetTypeParameters | 27 | + widgetType |
30 | } from '@shared/models/widget.models'; | 28 | } from '@shared/models/widget.models'; |
31 | import { | 29 | import { |
32 | AbstractControl, | 30 | AbstractControl, |
@@ -37,7 +35,7 @@ import { | @@ -37,7 +35,7 @@ import { | ||
37 | FormGroup, | 35 | FormGroup, |
38 | NG_VALIDATORS, | 36 | NG_VALIDATORS, |
39 | NG_VALUE_ACCESSOR, | 37 | NG_VALUE_ACCESSOR, |
40 | - Validator, ValidatorFn, | 38 | + Validator, |
41 | Validators | 39 | Validators |
42 | } from '@angular/forms'; | 40 | } from '@angular/forms'; |
43 | import { WidgetConfigComponentData } from '@home/models/widget-component.models'; | 41 | import { WidgetConfigComponentData } from '@home/models/widget-component.models'; |
@@ -55,10 +53,12 @@ import { | @@ -55,10 +53,12 @@ import { | ||
55 | EntityAliasDialogComponent, | 53 | EntityAliasDialogComponent, |
56 | EntityAliasDialogData | 54 | EntityAliasDialogData |
57 | } from '@home/components/alias/entity-alias-dialog.component'; | 55 | } from '@home/components/alias/entity-alias-dialog.component'; |
58 | -import { tap, mergeMap, map, catchError } from 'rxjs/operators'; | 56 | +import { catchError, map, mergeMap, tap } from 'rxjs/operators'; |
59 | import { MatDialog } from '@angular/material/dialog'; | 57 | import { MatDialog } from '@angular/material/dialog'; |
60 | import { EntityService } from '@core/http/entity.service'; | 58 | import { EntityService } from '@core/http/entity.service'; |
61 | import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; | 59 | import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; |
60 | +import { WidgetActionsData } from './action/manage-widget-actions.component.models'; | ||
61 | +import { Dashboard } from '@shared/models/dashboard.models'; | ||
62 | 62 | ||
63 | const emptySettingsSchema = { | 63 | const emptySettingsSchema = { |
64 | type: 'object', | 64 | type: 'object', |
@@ -106,6 +106,9 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | @@ -106,6 +106,9 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | ||
106 | @Input() | 106 | @Input() |
107 | functionsOnly: boolean; | 107 | functionsOnly: boolean; |
108 | 108 | ||
109 | + @Input() | ||
110 | + dashboardStates: Array<string>; | ||
111 | + | ||
109 | @Input() disabled: boolean; | 112 | @Input() disabled: boolean; |
110 | 113 | ||
111 | widgetType: widgetType; | 114 | widgetType: widgetType; |
@@ -117,34 +120,13 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | @@ -117,34 +120,13 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | ||
117 | widgetConfigCallbacks: WidgetConfigCallbacks = { | 120 | widgetConfigCallbacks: WidgetConfigCallbacks = { |
118 | createEntityAlias: this.createEntityAlias.bind(this), | 121 | createEntityAlias: this.createEntityAlias.bind(this), |
119 | generateDataKey: this.generateDataKey.bind(this), | 122 | generateDataKey: this.generateDataKey.bind(this), |
120 | - fetchEntityKeys: this.fetchEntityKeys.bind(this) | 123 | + fetchEntityKeys: this.fetchEntityKeys.bind(this), |
124 | + fetchDashboardStates: this.fetchDashboardStates.bind(this) | ||
121 | }; | 125 | }; |
122 | 126 | ||
123 | widgetEditMode = this.utils.widgetEditMode; | 127 | widgetEditMode = this.utils.widgetEditMode; |
124 | 128 | ||
125 | selectedTab: number; | 129 | selectedTab: number; |
126 | - title: string; | ||
127 | - showTitleIcon: boolean; | ||
128 | - titleIcon: string; | ||
129 | - iconColor: string; | ||
130 | - iconSize: string; | ||
131 | - showTitle: boolean; | ||
132 | - dropShadow: boolean; | ||
133 | - enableFullscreen: boolean; | ||
134 | - backgroundColor: string; | ||
135 | - color: string; | ||
136 | - padding: string; | ||
137 | - margin: string; | ||
138 | - widgetStyle: string; | ||
139 | - titleStyle: string; | ||
140 | - units: string; | ||
141 | - decimals: number; | ||
142 | - showLegend: boolean; | ||
143 | - legendConfig: LegendConfig; | ||
144 | - actions: {[actionSourceId: string]: Array<WidgetActionDescriptor>}; | ||
145 | - alarmSource: Datasource; | ||
146 | - mobileOrder: number; | ||
147 | - mobileHeight: number; | ||
148 | 130 | ||
149 | private modelValue: WidgetConfigComponentData; | 131 | private modelValue: WidgetConfigComponentData; |
150 | 132 | ||
@@ -152,11 +134,19 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | @@ -152,11 +134,19 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | ||
152 | 134 | ||
153 | public dataSettings: FormGroup; | 135 | public dataSettings: FormGroup; |
154 | public targetDeviceSettings: FormGroup; | 136 | public targetDeviceSettings: FormGroup; |
137 | + public alarmSourceSettings: FormGroup; | ||
138 | + public widgetSettings: FormGroup; | ||
139 | + public layoutSettings: FormGroup; | ||
155 | public advancedSettings: FormGroup; | 140 | public advancedSettings: FormGroup; |
141 | + public actionsSettings: FormGroup; | ||
156 | 142 | ||
157 | private dataSettingsChangesSubscription: Subscription; | 143 | private dataSettingsChangesSubscription: Subscription; |
158 | private targetDeviceSettingsSubscription: Subscription; | 144 | private targetDeviceSettingsSubscription: Subscription; |
145 | + private alarmSourceSettingsSubscription: Subscription; | ||
146 | + private widgetSettingsSubscription: Subscription; | ||
147 | + private layoutSettingsSubscription: Subscription; | ||
159 | private advancedSettingsSubscription: Subscription; | 148 | private advancedSettingsSubscription: Subscription; |
149 | + private actionsSettingsSubscription: Subscription; | ||
160 | 150 | ||
161 | constructor(protected store: Store<AppState>, | 151 | constructor(protected store: Store<AppState>, |
162 | private utils: UtilsService, | 152 | private utils: UtilsService, |
@@ -173,6 +163,47 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | @@ -173,6 +163,47 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | ||
173 | } else { | 163 | } else { |
174 | this.datasourceTypes = [DatasourceType.function, DatasourceType.entity]; | 164 | this.datasourceTypes = [DatasourceType.function, DatasourceType.entity]; |
175 | } | 165 | } |
166 | + this.widgetSettings = this.fb.group({ | ||
167 | + title: [null, []], | ||
168 | + showTitleIcon: [null, []], | ||
169 | + titleIcon: [null, []], | ||
170 | + iconColor: [null, []], | ||
171 | + iconSize: [null, []], | ||
172 | + showTitle: [null, []], | ||
173 | + dropShadow: [null, []], | ||
174 | + enableFullscreen: [null, []], | ||
175 | + backgroundColor: [null, []], | ||
176 | + color: [null, []], | ||
177 | + padding: [null, []], | ||
178 | + margin: [null, []], | ||
179 | + widgetStyle: [null, []], | ||
180 | + titleStyle: [null, []], | ||
181 | + units: [null, []], | ||
182 | + decimals: [null, [Validators.min(0), Validators.max(15), Validators.pattern(/^\d*$/)]], | ||
183 | + showLegend: [null, []], | ||
184 | + legendConfig: [null, []] | ||
185 | + }); | ||
186 | + this.widgetSettings.get('showTitleIcon').valueChanges.subscribe((value: boolean) => { | ||
187 | + if (value) { | ||
188 | + this.widgetSettings.get('titleIcon').enable({emitEvent: false}); | ||
189 | + } else { | ||
190 | + this.widgetSettings.get('titleIcon').disable({emitEvent: false}); | ||
191 | + } | ||
192 | + }); | ||
193 | + this.widgetSettings.get('showLegend').valueChanges.subscribe((value: boolean) => { | ||
194 | + if (value) { | ||
195 | + this.widgetSettings.get('legendConfig').enable({emitEvent: false}); | ||
196 | + } else { | ||
197 | + this.widgetSettings.get('legendConfig').disable({emitEvent: false}); | ||
198 | + } | ||
199 | + }); | ||
200 | + this.layoutSettings = this.fb.group({ | ||
201 | + mobileOrder: [null, [Validators.pattern(/^-?[0-9]+$/)]], | ||
202 | + mobileHeight: [null, [Validators.min(1), Validators.max(10), Validators.pattern(/^\d*$/)]] | ||
203 | + }); | ||
204 | + this.actionsSettings = this.fb.group({ | ||
205 | + actionsData: [null, []] | ||
206 | + }); | ||
176 | } | 207 | } |
177 | 208 | ||
178 | private removeChangeSubscriptions() { | 209 | private removeChangeSubscriptions() { |
@@ -184,10 +215,26 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | @@ -184,10 +215,26 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | ||
184 | this.targetDeviceSettingsSubscription.unsubscribe(); | 215 | this.targetDeviceSettingsSubscription.unsubscribe(); |
185 | this.targetDeviceSettingsSubscription = null; | 216 | this.targetDeviceSettingsSubscription = null; |
186 | } | 217 | } |
218 | + if (this.alarmSourceSettingsSubscription) { | ||
219 | + this.alarmSourceSettingsSubscription.unsubscribe(); | ||
220 | + this.alarmSourceSettingsSubscription = null; | ||
221 | + } | ||
222 | + if (this.widgetSettingsSubscription) { | ||
223 | + this.widgetSettingsSubscription.unsubscribe(); | ||
224 | + this.widgetSettingsSubscription = null; | ||
225 | + } | ||
226 | + if (this.layoutSettingsSubscription) { | ||
227 | + this.layoutSettingsSubscription.unsubscribe(); | ||
228 | + this.layoutSettingsSubscription = null; | ||
229 | + } | ||
187 | if (this.advancedSettingsSubscription) { | 230 | if (this.advancedSettingsSubscription) { |
188 | this.advancedSettingsSubscription.unsubscribe(); | 231 | this.advancedSettingsSubscription.unsubscribe(); |
189 | this.advancedSettingsSubscription = null; | 232 | this.advancedSettingsSubscription = null; |
190 | } | 233 | } |
234 | + if (this.actionsSettingsSubscription) { | ||
235 | + this.actionsSettingsSubscription.unsubscribe(); | ||
236 | + this.actionsSettingsSubscription = null; | ||
237 | + } | ||
191 | } | 238 | } |
192 | 239 | ||
193 | private createChangeSubscriptions() { | 240 | private createChangeSubscriptions() { |
@@ -197,14 +244,27 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | @@ -197,14 +244,27 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | ||
197 | this.targetDeviceSettingsSubscription = this.targetDeviceSettings.valueChanges.subscribe( | 244 | this.targetDeviceSettingsSubscription = this.targetDeviceSettings.valueChanges.subscribe( |
198 | () => this.updateTargetDeviceSettings() | 245 | () => this.updateTargetDeviceSettings() |
199 | ); | 246 | ); |
247 | + this.alarmSourceSettingsSubscription = this.alarmSourceSettings.valueChanges.subscribe( | ||
248 | + () => this.updateAlarmSourceSettings() | ||
249 | + ); | ||
250 | + this.widgetSettingsSubscription = this.widgetSettings.valueChanges.subscribe( | ||
251 | + () => this.updateWidgetSettings() | ||
252 | + ); | ||
253 | + this.layoutSettingsSubscription = this.layoutSettings.valueChanges.subscribe( | ||
254 | + () => this.updateLayoutSettings() | ||
255 | + ); | ||
200 | this.advancedSettingsSubscription = this.advancedSettings.valueChanges.subscribe( | 256 | this.advancedSettingsSubscription = this.advancedSettings.valueChanges.subscribe( |
201 | () => this.updateAdvancedSettings() | 257 | () => this.updateAdvancedSettings() |
202 | ); | 258 | ); |
259 | + this.actionsSettingsSubscription = this.actionsSettings.valueChanges.subscribe( | ||
260 | + () => this.updateActionSettings() | ||
261 | + ); | ||
203 | } | 262 | } |
204 | 263 | ||
205 | private buildForms() { | 264 | private buildForms() { |
206 | this.dataSettings = this.fb.group({}); | 265 | this.dataSettings = this.fb.group({}); |
207 | this.targetDeviceSettings = this.fb.group({}); | 266 | this.targetDeviceSettings = this.fb.group({}); |
267 | + this.alarmSourceSettings = this.fb.group({}); | ||
208 | this.advancedSettings = this.fb.group({}); | 268 | this.advancedSettings = this.fb.group({}); |
209 | if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { | 269 | if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { |
210 | this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(null)); | 270 | this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(null)); |
@@ -235,6 +295,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | @@ -235,6 +295,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | ||
235 | this.targetDeviceSettings.addControl('targetDeviceAliasId', | 295 | this.targetDeviceSettings.addControl('targetDeviceAliasId', |
236 | this.fb.control(null, | 296 | this.fb.control(null, |
237 | this.widgetEditMode ? [] : [Validators.required])); | 297 | this.widgetEditMode ? [] : [Validators.required])); |
298 | + } else if (this.widgetType === widgetType.alarm) { | ||
299 | + this.alarmSourceSettings = this.buildDatasourceForm(); | ||
238 | } | 300 | } |
239 | } | 301 | } |
240 | this.advancedSettings.addControl('settings', | 302 | this.advancedSettings.addControl('settings', |
@@ -264,31 +326,54 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | @@ -264,31 +326,54 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | ||
264 | const layout = this.modelValue.layout; | 326 | const layout = this.modelValue.layout; |
265 | if (config) { | 327 | if (config) { |
266 | this.selectedTab = 0; | 328 | this.selectedTab = 0; |
267 | - this.title = config.title; | ||
268 | - this.showTitleIcon = isDefined(config.showTitleIcon) ? config.showTitleIcon : false; | ||
269 | - this.titleIcon = isDefined(config.titleIcon) ? config.titleIcon : ''; | ||
270 | - this.iconColor = isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)'; | ||
271 | - this.iconSize = isDefined(config.iconSize) ? config.iconSize : '24px'; | ||
272 | - this.showTitle = config.showTitle; | ||
273 | - this.dropShadow = isDefined(config.dropShadow) ? config.dropShadow : true; | ||
274 | - this.enableFullscreen = isDefined(config.enableFullscreen) ? config.enableFullscreen : true; | ||
275 | - this.backgroundColor = config.backgroundColor; | ||
276 | - this.color = config.color; | ||
277 | - this.padding = config.padding; | ||
278 | - this.margin = config.margin; | ||
279 | - this.widgetStyle = | ||
280 | - JSON.stringify(isDefined(config.widgetStyle) ? config.widgetStyle : {}, null, 2); | ||
281 | - this.titleStyle = | ||
282 | - JSON.stringify(isDefined(config.titleStyle) ? config.titleStyle : { | ||
283 | - fontSize: '16px', | ||
284 | - fontWeight: 400 | ||
285 | - }, null, 2); | ||
286 | - this.units = config.units; | ||
287 | - this.decimals = config.decimals; | ||
288 | - this.actions = config.actions; | ||
289 | - if (!this.actions) { | ||
290 | - this.actions = {}; | 329 | + this.widgetSettings.patchValue({ |
330 | + title: config.title, | ||
331 | + showTitleIcon: isDefined(config.showTitleIcon) ? config.showTitleIcon : false, | ||
332 | + titleIcon: isDefined(config.titleIcon) ? config.titleIcon : '', | ||
333 | + iconColor: isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)', | ||
334 | + iconSize: isDefined(config.iconSize) ? config.iconSize : '24px', | ||
335 | + showTitle: config.showTitle, | ||
336 | + dropShadow: isDefined(config.dropShadow) ? config.dropShadow : true, | ||
337 | + enableFullscreen: isDefined(config.enableFullscreen) ? config.enableFullscreen : true, | ||
338 | + backgroundColor: config.backgroundColor, | ||
339 | + color: config.color, | ||
340 | + padding: config.padding, | ||
341 | + margin: config.margin, | ||
342 | + widgetStyle: isDefined(config.widgetStyle) ? config.widgetStyle : {}, | ||
343 | + titleStyle: isDefined(config.titleStyle) ? config.titleStyle : { | ||
344 | + fontSize: '16px', | ||
345 | + fontWeight: 400 | ||
346 | + }, | ||
347 | + units: config.units, | ||
348 | + decimals: config.decimals, | ||
349 | + showLegend: isDefined(config.showLegend) ? config.showLegend : | ||
350 | + this.widgetType === widgetType.timeseries, | ||
351 | + legendConfig: config.legendConfig || defaultLegendConfig(this.widgetType) | ||
352 | + }, | ||
353 | + {emitEvent: false} | ||
354 | + ); | ||
355 | + const showTitleIcon: boolean = this.widgetSettings.get('showTitleIcon').value; | ||
356 | + if (showTitleIcon) { | ||
357 | + this.widgetSettings.get('titleIcon').enable({emitEvent: false}); | ||
358 | + } else { | ||
359 | + this.widgetSettings.get('titleIcon').disable({emitEvent: false}); | ||
360 | + } | ||
361 | + const showLegend: boolean = this.widgetSettings.get('showLegend').value; | ||
362 | + if (showLegend) { | ||
363 | + this.widgetSettings.get('legendConfig').enable({emitEvent: false}); | ||
364 | + } else { | ||
365 | + this.widgetSettings.get('legendConfig').disable({emitEvent: false}); | ||
291 | } | 366 | } |
367 | + const actionsData: WidgetActionsData = { | ||
368 | + actionsMap: config.actions || {}, | ||
369 | + actionSources: this.modelValue.actionSources || {} | ||
370 | + }; | ||
371 | + this.actionsSettings.patchValue( | ||
372 | + { | ||
373 | + actionsData | ||
374 | + }, | ||
375 | + {emitEvent: false} | ||
376 | + ); | ||
292 | if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { | 377 | if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { |
293 | const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ? | 378 | const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ? |
294 | config.useDashboardTimewindow : true; | 379 | config.useDashboardTimewindow : true; |
@@ -346,29 +431,42 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | @@ -346,29 +431,42 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | ||
346 | { alarmsPollingInterval: isDefined(config.alarmsPollingInterval) ? | 431 | { alarmsPollingInterval: isDefined(config.alarmsPollingInterval) ? |
347 | config.alarmsPollingInterval : 5}, {emitEvent: false} | 432 | config.alarmsPollingInterval : 5}, {emitEvent: false} |
348 | ); | 433 | ); |
349 | - if (config.alarmSource) { | ||
350 | - this.alarmSource = config.alarmSource; | ||
351 | - } else { | ||
352 | - this.alarmSource = null; | ||
353 | - } | 434 | + this.alarmSourceSettings.patchValue( |
435 | + config.alarmSource, {emitEvent: false} | ||
436 | + ); | ||
437 | + const alarmSourceType: DatasourceType = this.alarmSourceSettings.get('type').value; | ||
438 | + this.alarmSourceSettings.get('entityAliasId').setValidators( | ||
439 | + alarmSourceType === DatasourceType.entity ? [Validators.required] : [] | ||
440 | + ); | ||
441 | + this.alarmSourceSettings.get('entityAliasId').updateValueAndValidity({emitEvent: false}); | ||
354 | } | 442 | } |
355 | } | 443 | } |
356 | 444 | ||
357 | this.updateSchemaForm(config.settings); | 445 | this.updateSchemaForm(config.settings); |
358 | 446 | ||
359 | if (layout) { | 447 | if (layout) { |
360 | - this.mobileOrder = layout.mobileOrder; | ||
361 | - this.mobileHeight = layout.mobileHeight; | 448 | + this.layoutSettings.patchValue( |
449 | + { | ||
450 | + mobileOrder: layout.mobileOrder, | ||
451 | + mobileHeight: layout.mobileHeight | ||
452 | + }, | ||
453 | + {emitEvent: false} | ||
454 | + ); | ||
362 | } else { | 455 | } else { |
363 | - this.mobileOrder = undefined; | ||
364 | - this.mobileHeight = undefined; | 456 | + this.layoutSettings.patchValue( |
457 | + { | ||
458 | + mobileOrder: null, | ||
459 | + mobileHeight: null | ||
460 | + }, | ||
461 | + {emitEvent: false} | ||
462 | + ); | ||
365 | } | 463 | } |
366 | } | 464 | } |
367 | this.createChangeSubscriptions(); | 465 | this.createChangeSubscriptions(); |
368 | } | 466 | } |
369 | } | 467 | } |
370 | 468 | ||
371 | - private buildDatasourceForm(datasource?: Datasource): AbstractControl { | 469 | + private buildDatasourceForm(datasource?: Datasource): FormGroup { |
372 | const dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional; | 470 | const dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional; |
373 | const datasourceFormGroup = this.fb.group( | 471 | const datasourceFormGroup = this.fb.group( |
374 | { | 472 | { |
@@ -427,6 +525,34 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | @@ -427,6 +525,34 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | ||
427 | } | 525 | } |
428 | } | 526 | } |
429 | 527 | ||
528 | + private updateAlarmSourceSettings() { | ||
529 | + if (this.modelValue) { | ||
530 | + if (this.modelValue.config) { | ||
531 | + const alarmSource: Datasource = this.alarmSourceSettings.value; | ||
532 | + this.modelValue.config.alarmSource = alarmSource; | ||
533 | + } | ||
534 | + this.propagateChange(this.modelValue); | ||
535 | + } | ||
536 | + } | ||
537 | + | ||
538 | + private updateWidgetSettings() { | ||
539 | + if (this.modelValue) { | ||
540 | + if (this.modelValue.config) { | ||
541 | + Object.assign(this.modelValue.config, this.widgetSettings.value); | ||
542 | + } | ||
543 | + this.propagateChange(this.modelValue); | ||
544 | + } | ||
545 | + } | ||
546 | + | ||
547 | + private updateLayoutSettings() { | ||
548 | + if (this.modelValue) { | ||
549 | + if (this.modelValue.layout) { | ||
550 | + Object.assign(this.modelValue.layout, this.layoutSettings.value); | ||
551 | + } | ||
552 | + this.propagateChange(this.modelValue); | ||
553 | + } | ||
554 | + } | ||
555 | + | ||
430 | private updateAdvancedSettings() { | 556 | private updateAdvancedSettings() { |
431 | if (this.modelValue) { | 557 | if (this.modelValue) { |
432 | if (this.modelValue.config) { | 558 | if (this.modelValue.config) { |
@@ -437,6 +563,16 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | @@ -437,6 +563,16 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | ||
437 | } | 563 | } |
438 | } | 564 | } |
439 | 565 | ||
566 | + private updateActionSettings() { | ||
567 | + if (this.modelValue) { | ||
568 | + if (this.modelValue.config) { | ||
569 | + const actions = (this.actionsSettings.get('actionsData').value as WidgetActionsData).actionsMap; | ||
570 | + this.modelValue.config.actions = actions; | ||
571 | + } | ||
572 | + this.propagateChange(this.modelValue); | ||
573 | + } | ||
574 | + } | ||
575 | + | ||
440 | public displayAdvanced(): boolean { | 576 | public displayAdvanced(): boolean { |
441 | return this.modelValue.settingsSchema && this.modelValue.settingsSchema.schema; | 577 | return this.modelValue.settingsSchema && this.modelValue.settingsSchema.schema; |
442 | } | 578 | } |
@@ -596,6 +732,21 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | @@ -596,6 +732,21 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | ||
596 | ); | 732 | ); |
597 | } | 733 | } |
598 | 734 | ||
735 | + private fetchDashboardStates(query: string): Array<string> { | ||
736 | + const stateIds = Object.keys(this.dashboardStates); | ||
737 | + const result = query ? stateIds.filter(this.createFilterForDashboardState(query)) : stateIds; | ||
738 | + if (result && result.length) { | ||
739 | + return result; | ||
740 | + } else { | ||
741 | + return [query]; | ||
742 | + } | ||
743 | + } | ||
744 | + | ||
745 | + private createFilterForDashboardState(query: string): (stateId: string) => boolean { | ||
746 | + const lowercaseQuery = query.toLowerCase(); | ||
747 | + return stateId => stateId.toLowerCase().indexOf(lowercaseQuery) === 0; | ||
748 | + } | ||
749 | + | ||
599 | public validate(c: FormControl) { | 750 | public validate(c: FormControl) { |
600 | if (!this.dataSettings.valid) { | 751 | if (!this.dataSettings.valid) { |
601 | return { | 752 | return { |
@@ -603,6 +754,18 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | @@ -603,6 +754,18 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | ||
603 | valid: false | 754 | valid: false |
604 | } | 755 | } |
605 | }; | 756 | }; |
757 | + } else if (!this.widgetSettings.valid) { | ||
758 | + return { | ||
759 | + widgetSettings: { | ||
760 | + valid: false | ||
761 | + } | ||
762 | + }; | ||
763 | + } else if (!this.layoutSettings.valid) { | ||
764 | + return { | ||
765 | + widgetSettings: { | ||
766 | + valid: false | ||
767 | + } | ||
768 | + }; | ||
606 | } else if (!this.advancedSettings.valid) { | 769 | } else if (!this.advancedSettings.valid) { |
607 | return { | 770 | return { |
608 | advancedSettings: { | 771 | advancedSettings: { |
@@ -636,24 +799,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | @@ -636,24 +799,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont | ||
636 | }; | 799 | }; |
637 | } | 800 | } |
638 | } | 801 | } |
639 | - try { | ||
640 | - JSON.parse(this.widgetStyle); | ||
641 | - } catch (e) { | ||
642 | - return { | ||
643 | - widgetStyle: { | ||
644 | - valid: false | ||
645 | - } | ||
646 | - }; | ||
647 | - } | ||
648 | - try { | ||
649 | - JSON.parse(this.titleStyle); | ||
650 | - } catch (e) { | ||
651 | - return { | ||
652 | - titleStyle: { | ||
653 | - valid: false | ||
654 | - } | ||
655 | - }; | ||
656 | - } | ||
657 | } | 802 | } |
658 | return null; | 803 | return null; |
659 | } | 804 | } |
@@ -38,6 +38,7 @@ import { | @@ -38,6 +38,7 @@ import { | ||
38 | Datasource, | 38 | Datasource, |
39 | LegendConfig, | 39 | LegendConfig, |
40 | LegendData, | 40 | LegendData, |
41 | + LegendDirection, | ||
41 | LegendPosition, | 42 | LegendPosition, |
42 | Widget, | 43 | Widget, |
43 | WidgetActionDescriptor, | 44 | WidgetActionDescriptor, |
@@ -45,7 +46,8 @@ import { | @@ -45,7 +46,8 @@ import { | ||
45 | WidgetActionType, | 46 | WidgetActionType, |
46 | WidgetResource, | 47 | WidgetResource, |
47 | widgetType, | 48 | widgetType, |
48 | - WidgetTypeParameters | 49 | + WidgetTypeParameters, |
50 | + defaultLegendConfig | ||
49 | } from '@shared/models/widget.models'; | 51 | } from '@shared/models/widget.models'; |
50 | import { PageComponent } from '@shared/components/page.component'; | 52 | import { PageComponent } from '@shared/components/page.component'; |
51 | import { Store } from '@ngrx/store'; | 53 | import { Store } from '@ngrx/store'; |
@@ -165,6 +167,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -165,6 +167,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
165 | private ngZone: NgZone, | 167 | private ngZone: NgZone, |
166 | private cd: ChangeDetectorRef) { | 168 | private cd: ChangeDetectorRef) { |
167 | super(store); | 169 | super(store); |
170 | + this.cssParser.testMode = false; | ||
168 | } | 171 | } |
169 | 172 | ||
170 | ngOnInit(): void { | 173 | ngOnInit(): void { |
@@ -179,14 +182,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -179,14 +182,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
179 | this.legendContainerLayoutType = 'column'; | 182 | this.legendContainerLayoutType = 'column'; |
180 | 183 | ||
181 | if (this.displayLegend) { | 184 | if (this.displayLegend) { |
182 | - this.legendConfig = this.widget.config.legendConfig || | ||
183 | - { | ||
184 | - position: LegendPosition.bottom, | ||
185 | - showMin: false, | ||
186 | - showMax: false, | ||
187 | - showAvg: this.widget.type === widgetType.timeseries, | ||
188 | - showTotal: false | ||
189 | - }; | 185 | + this.legendConfig = this.widget.config.legendConfig || defaultLegendConfig(this.widget.type); |
190 | this.legendData = { | 186 | this.legendData = { |
191 | keys: [], | 187 | keys: [], |
192 | data: [] | 188 | data: [] |
@@ -194,8 +190,10 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -194,8 +190,10 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
194 | if (this.legendConfig.position === LegendPosition.top || | 190 | if (this.legendConfig.position === LegendPosition.top || |
195 | this.legendConfig.position === LegendPosition.bottom) { | 191 | this.legendConfig.position === LegendPosition.bottom) { |
196 | this.legendContainerLayoutType = 'column'; | 192 | this.legendContainerLayoutType = 'column'; |
193 | + this.isLegendFirst = this.legendConfig.position === LegendPosition.top; | ||
197 | } else { | 194 | } else { |
198 | this.legendContainerLayoutType = 'row'; | 195 | this.legendContainerLayoutType = 'row'; |
196 | + this.isLegendFirst = this.legendConfig.position === LegendPosition.left; | ||
199 | } | 197 | } |
200 | switch (this.legendConfig.position) { | 198 | switch (this.legendConfig.position) { |
201 | case LegendPosition.top: | 199 | case LegendPosition.top: |
@@ -352,7 +350,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -352,7 +350,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
352 | this.loadFromWidgetInfo(); | 350 | this.loadFromWidgetInfo(); |
353 | } | 351 | } |
354 | ); | 352 | ); |
355 | - | 353 | + setTimeout(() => { |
354 | + this.dashboardWidget.updateWidgetParams(); | ||
355 | + }, 0); | ||
356 | } | 356 | } |
357 | 357 | ||
358 | ngAfterViewInit(): void { | 358 | ngAfterViewInit(): void { |
@@ -764,6 +764,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -764,6 +764,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
764 | timeWindowUpdated: (subscription, timeWindowConfig) => { | 764 | timeWindowUpdated: (subscription, timeWindowConfig) => { |
765 | this.ngZone.run(() => { | 765 | this.ngZone.run(() => { |
766 | this.widget.config.timewindow = timeWindowConfig; | 766 | this.widget.config.timewindow = timeWindowConfig; |
767 | + this.cd.detectChanges(); | ||
767 | }); | 768 | }); |
768 | } | 769 | } |
769 | }; | 770 | }; |
@@ -924,24 +925,16 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -924,24 +925,16 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
924 | if (targetDashboardStateId) { | 925 | if (targetDashboardStateId) { |
925 | stateObject.id = targetDashboardStateId; | 926 | stateObject.id = targetDashboardStateId; |
926 | } | 927 | } |
927 | - const stateParams = { | ||
928 | - dashboardId: targetDashboardId, | ||
929 | - state: objToBase64([ stateObject ]) | ||
930 | - }; | ||
931 | const state = objToBase64([ stateObject ]); | 928 | const state = objToBase64([ stateObject ]); |
932 | - const currentUrl = this.route.snapshot.url; | 929 | + const isSinglePage = this.route.snapshot.data.singlePageMode; |
933 | let url; | 930 | let url; |
934 | - if (currentUrl.length > 1) { | ||
935 | - if (currentUrl[currentUrl.length - 2].path === 'dashboard') { | ||
936 | - url = `/dashboard/${targetDashboardId}?state=${state}`; | ||
937 | - } else { | ||
938 | - url = `/dashboards/${targetDashboardId}?state=${state}`; | ||
939 | - } | ||
940 | - } | ||
941 | - if (url) { | ||
942 | - const urlTree = this.router.parseUrl(url); | ||
943 | - this.router.navigateByUrl(url); | 931 | + if (isSinglePage) { |
932 | + url = `/dashboard/${targetDashboardId}?state=${state}`; | ||
933 | + } else { | ||
934 | + url = `/dashboards/${targetDashboardId}?state=${state}`; | ||
944 | } | 935 | } |
936 | + const urlTree = this.router.parseUrl(url); | ||
937 | + this.router.navigateByUrl(url); | ||
945 | break; | 938 | break; |
946 | case WidgetActionType.custom: | 939 | case WidgetActionType.custom: |
947 | const customFunction = descriptor.customFunction; | 940 | const customFunction = descriptor.customFunction; |
@@ -78,6 +78,7 @@ export interface IDashboardComponent { | @@ -78,6 +78,7 @@ export interface IDashboardComponent { | ||
78 | selectWidget(index: number, delay?: number); | 78 | selectWidget(index: number, delay?: number); |
79 | getSelectedWidget(): Widget; | 79 | getSelectedWidget(): Widget; |
80 | getEventGridPosition(event: Event): WidgetPosition; | 80 | getEventGridPosition(event: Event): WidgetPosition; |
81 | + notifyGridsterOptionsChanged(); | ||
81 | } | 82 | } |
82 | 83 | ||
83 | declare type DashboardWidgetUpdateOperation = 'add' | 'remove' | 'update'; | 84 | declare type DashboardWidgetUpdateOperation = 'add' | 'remove' | 'update'; |
@@ -185,7 +186,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { | @@ -185,7 +186,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { | ||
185 | 186 | ||
186 | highlightWidget(index: number): DashboardWidget { | 187 | highlightWidget(index: number): DashboardWidget { |
187 | const widget = this.findWidgetAtIndex(index); | 188 | const widget = this.findWidgetAtIndex(index); |
188 | - if (widget && (!this.highlightedMode || !widget.highlighted)) { | 189 | + if (widget && (!this.highlightedMode || !widget.highlighted || this.highlightedMode && widget.highlighted)) { |
189 | this.highlightedMode = true; | 190 | this.highlightedMode = true; |
190 | widget.highlighted = true; | 191 | widget.highlighted = true; |
191 | this.dashboardWidgets.forEach((dashboardWidget) => { | 192 | this.dashboardWidgets.forEach((dashboardWidget) => { |
@@ -248,6 +249,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { | @@ -248,6 +249,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { | ||
248 | }); | 249 | }); |
249 | this.sortWidgets(); | 250 | this.sortWidgets(); |
250 | this.dashboard.gridsterOpts.maxRows = maxRows; | 251 | this.dashboard.gridsterOpts.maxRows = maxRows; |
252 | + this.dashboard.notifyGridsterOptionsChanged(); | ||
251 | } | 253 | } |
252 | 254 | ||
253 | sortWidgets() { | 255 | sortWidgets() { |
@@ -68,7 +68,6 @@ export interface WidgetContext { | @@ -68,7 +68,6 @@ export interface WidgetContext { | ||
68 | width?: number; | 68 | width?: number; |
69 | height?: number; | 69 | height?: number; |
70 | $scope?: IDynamicWidgetComponent; | 70 | $scope?: IDynamicWidgetComponent; |
71 | - hideTitlePanel?: boolean; | ||
72 | isEdit?: boolean; | 71 | isEdit?: boolean; |
73 | isMobile?: boolean; | 72 | isMobile?: boolean; |
74 | dashboard?: IDashboardComponent; | 73 | dashboard?: IDashboardComponent; |
@@ -87,15 +86,18 @@ export interface WidgetContext { | @@ -87,15 +86,18 @@ export interface WidgetContext { | ||
87 | stateController?: IStateController; | 86 | stateController?: IStateController; |
88 | aliasController?: IAliasController; | 87 | aliasController?: IAliasController; |
89 | activeEntityInfo?: SubscriptionEntityInfo; | 88 | activeEntityInfo?: SubscriptionEntityInfo; |
90 | - widgetTitleTemplate?: string; | ||
91 | - widgetTitle?: string; | ||
92 | - customHeaderActions?: Array<WidgetHeaderAction>; | ||
93 | - widgetActions?: Array<WidgetAction>; | ||
94 | 89 | ||
95 | datasources?: Array<Datasource>; | 90 | datasources?: Array<Datasource>; |
96 | data?: Array<DatasourceData>; | 91 | data?: Array<DatasourceData>; |
97 | hiddenData?: Array<{data: DataSet}>; | 92 | hiddenData?: Array<{data: DataSet}>; |
98 | timeWindow?: WidgetTimewindow; | 93 | timeWindow?: WidgetTimewindow; |
94 | + | ||
95 | + hideTitlePanel?: boolean; | ||
96 | + widgetTitleTemplate?: string; | ||
97 | + widgetTitle?: string; | ||
98 | + customHeaderActions?: Array<WidgetHeaderAction>; | ||
99 | + widgetActions?: Array<WidgetAction>; | ||
100 | + | ||
99 | } | 101 | } |
100 | 102 | ||
101 | export interface IDynamicWidgetComponent { | 103 | export interface IDynamicWidgetComponent { |
@@ -122,7 +124,7 @@ export interface WidgetConfigComponentData { | @@ -122,7 +124,7 @@ export interface WidgetConfigComponentData { | ||
122 | layout: WidgetLayout; | 124 | layout: WidgetLayout; |
123 | widgetType: widgetType; | 125 | widgetType: widgetType; |
124 | typeParameters: WidgetTypeParameters; | 126 | typeParameters: WidgetTypeParameters; |
125 | - actionSources: {[key: string]: WidgetActionSource}; | 127 | + actionSources: {[actionSourceId: string]: WidgetActionSource}; |
126 | isDataEnabled: boolean; | 128 | isDataEnabled: boolean; |
127 | settingsSchema: any; | 129 | settingsSchema: any; |
128 | dataKeySettingsSchema: any; | 130 | dataKeySettingsSchema: any; |
@@ -178,7 +180,7 @@ export interface WidgetTypeInstance { | @@ -178,7 +180,7 @@ export interface WidgetTypeInstance { | ||
178 | getDataKeySettingsSchema?: () => string; | 180 | getDataKeySettingsSchema?: () => string; |
179 | typeParameters?: () => WidgetTypeParameters; | 181 | typeParameters?: () => WidgetTypeParameters; |
180 | useCustomDatasources?: () => boolean; | 182 | useCustomDatasources?: () => boolean; |
181 | - actionSources?: () => {[key: string]: WidgetActionSource}; | 183 | + actionSources?: () => {[actionSourceId: string]: WidgetActionSource}; |
182 | 184 | ||
183 | onInit?: () => void; | 185 | onInit?: () => void; |
184 | onDataUpdated?: () => void; | 186 | onDataUpdated?: () => void; |
@@ -14,7 +14,16 @@ | @@ -14,7 +14,16 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, Inject, OnDestroy, OnInit, ViewEncapsulation, ViewChild, NgZone } from '@angular/core'; | 17 | +import { |
18 | + Component, | ||
19 | + Inject, | ||
20 | + OnDestroy, | ||
21 | + OnInit, | ||
22 | + ViewEncapsulation, | ||
23 | + ViewChild, | ||
24 | + NgZone, | ||
25 | + ChangeDetectorRef, ChangeDetectionStrategy, ApplicationRef | ||
26 | +} from '@angular/core'; | ||
18 | import { PageComponent } from '@shared/components/page.component'; | 27 | import { PageComponent } from '@shared/components/page.component'; |
19 | import { Store } from '@ngrx/store'; | 28 | import { Store } from '@ngrx/store'; |
20 | import { AppState } from '@core/core.state'; | 29 | import { AppState } from '@core/core.state'; |
@@ -77,7 +86,8 @@ import { EditWidgetComponent } from '@home/pages/dashboard/edit-widget.component | @@ -77,7 +86,8 @@ import { EditWidgetComponent } from '@home/pages/dashboard/edit-widget.component | ||
77 | selector: 'tb-dashboard-page', | 86 | selector: 'tb-dashboard-page', |
78 | templateUrl: './dashboard-page.component.html', | 87 | templateUrl: './dashboard-page.component.html', |
79 | styleUrls: ['./dashboard-page.component.scss'], | 88 | styleUrls: ['./dashboard-page.component.scss'], |
80 | - encapsulation: ViewEncapsulation.None | 89 | + encapsulation: ViewEncapsulation.None, |
90 | + // changeDetection: ChangeDetectionStrategy.OnPush | ||
81 | }) | 91 | }) |
82 | export class DashboardPageComponent extends PageComponent implements IDashboardController, OnDestroy { | 92 | export class DashboardPageComponent extends PageComponent implements IDashboardController, OnDestroy { |
83 | 93 | ||
@@ -149,7 +159,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -149,7 +159,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
149 | dashboardTimewindow: null, | 159 | dashboardTimewindow: null, |
150 | state: null, | 160 | state: null, |
151 | stateController: null, | 161 | stateController: null, |
152 | - aliasController: null | 162 | + aliasController: null, |
163 | + runChangeDetection: this.runChangeDetection.bind(this) | ||
153 | }; | 164 | }; |
154 | 165 | ||
155 | addWidgetFabButtons: FooterFabButtons = { | 166 | addWidgetFabButtons: FooterFabButtons = { |
@@ -204,12 +215,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -204,12 +215,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
204 | private dashboardService: DashboardService, | 215 | private dashboardService: DashboardService, |
205 | private itembuffer: ItemBufferService, | 216 | private itembuffer: ItemBufferService, |
206 | private fb: FormBuilder, | 217 | private fb: FormBuilder, |
207 | - private dialog: MatDialog) { | 218 | + private dialog: MatDialog, |
219 | + private ngZone: NgZone, | ||
220 | + private cd: ChangeDetectorRef) { | ||
208 | super(store); | 221 | super(store); |
209 | 222 | ||
210 | this.rxSubscriptions.push(this.route.data.subscribe( | 223 | this.rxSubscriptions.push(this.route.data.subscribe( |
211 | (data) => { | 224 | (data) => { |
212 | this.init(data); | 225 | this.init(data); |
226 | + this.runChangeDetection(); | ||
213 | } | 227 | } |
214 | )); | 228 | )); |
215 | 229 | ||
@@ -294,6 +308,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -294,6 +308,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
294 | this.rxSubscriptions.length = 0; | 308 | this.rxSubscriptions.length = 0; |
295 | } | 309 | } |
296 | 310 | ||
311 | + public runChangeDetection() { | ||
312 | + /*setTimeout(() => { | ||
313 | + this.cd.detectChanges(); | ||
314 | + });*/ | ||
315 | + } | ||
316 | + | ||
297 | public openToolbar() { | 317 | public openToolbar() { |
298 | this.isToolbarOpenedAnimate = true; | 318 | this.isToolbarOpenedAnimate = true; |
299 | this.isToolbarOpened = true; | 319 | this.isToolbarOpened = true; |
@@ -646,7 +666,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -646,7 +666,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
646 | this.editingWidgetLayoutOriginal = widgetLayout; | 666 | this.editingWidgetLayoutOriginal = widgetLayout; |
647 | this.editingLayoutCtx.widgets[index] = widget; | 667 | this.editingLayoutCtx.widgets[index] = widget; |
648 | this.editingLayoutCtx.widgetLayouts[widget.id] = widgetLayout; | 668 | this.editingLayoutCtx.widgetLayouts[widget.id] = widgetLayout; |
649 | - this.editingLayoutCtx.ctrl.highlightWidget(index, 0); | 669 | + setTimeout(() => { |
670 | + this.editingLayoutCtx.ctrl.highlightWidget(index, 0); | ||
671 | + }, 0); | ||
650 | } | 672 | } |
651 | 673 | ||
652 | onEditWidgetClosed() { | 674 | onEditWidgetClosed() { |
@@ -25,6 +25,7 @@ import { | @@ -25,6 +25,7 @@ import { | ||
25 | WidgetPosition | 25 | WidgetPosition |
26 | } from '@home/models/dashboard-component.models'; | 26 | } from '@home/models/dashboard-component.models'; |
27 | import { Observable } from 'rxjs'; | 27 | import { Observable } from 'rxjs'; |
28 | +import { ChangeDetectorRef } from '@angular/core'; | ||
28 | 29 | ||
29 | export declare type DashboardPageScope = 'tenant' | 'customer'; | 30 | export declare type DashboardPageScope = 'tenant' | 'customer'; |
30 | 31 | ||
@@ -34,6 +35,7 @@ export interface DashboardContext { | @@ -34,6 +35,7 @@ export interface DashboardContext { | ||
34 | dashboardTimewindow: Timewindow; | 35 | dashboardTimewindow: Timewindow; |
35 | aliasController: IAliasController; | 36 | aliasController: IAliasController; |
36 | stateController: IStateController; | 37 | stateController: IStateController; |
38 | + runChangeDetection: () => void; | ||
37 | } | 39 | } |
38 | 40 | ||
39 | export interface IDashboardController { | 41 | export interface IDashboardController { |
@@ -21,6 +21,7 @@ | @@ -21,6 +21,7 @@ | ||
21 | [aliasController]="aliasController" | 21 | [aliasController]="aliasController" |
22 | [functionsOnly]="widgetEditMode" | 22 | [functionsOnly]="widgetEditMode" |
23 | [entityAliases]="dashboard.configuration.entityAliases" | 23 | [entityAliases]="dashboard.configuration.entityAliases" |
24 | + [dashboardStates]="dashboard.configuration.states" | ||
24 | formControlName="widgetConfig"> | 25 | formControlName="widgetConfig"> |
25 | </tb-widget-config> | 26 | </tb-widget-config> |
26 | </fieldset> | 27 | </fieldset> |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, OnInit, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; | 17 | +import { Component, OnInit, Input, OnChanges, SimpleChanges, ViewChild, ChangeDetectionStrategy } from '@angular/core'; |
18 | import { PageComponent } from '@shared/components/page.component'; | 18 | 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'; |
@@ -85,8 +85,8 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | @@ -85,8 +85,8 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo | ||
85 | this.rxSubscriptions.push(this.dashboard.dashboardTimewindowChanged.subscribe( | 85 | this.rxSubscriptions.push(this.dashboard.dashboardTimewindowChanged.subscribe( |
86 | (dashboardTimewindow) => { | 86 | (dashboardTimewindow) => { |
87 | this.dashboardCtx.dashboardTimewindow = dashboardTimewindow; | 87 | this.dashboardCtx.dashboardTimewindow = dashboardTimewindow; |
88 | - } | ||
89 | - ) | 88 | + this.dashboardCtx.runChangeDetection(); |
89 | + }) | ||
90 | ); | 90 | ); |
91 | this.initHotKeys(); | 91 | this.initHotKeys(); |
92 | } | 92 | } |
@@ -90,7 +90,7 @@ export class DefaultStateControllerComponent extends StateControllerComponent im | @@ -90,7 +90,7 @@ export class DefaultStateControllerComponent extends StateControllerComponent im | ||
90 | 90 | ||
91 | protected onStateChanged() { | 91 | protected onStateChanged() { |
92 | this.stateObject = this.parseState(this.currentState); | 92 | this.stateObject = this.parseState(this.currentState); |
93 | - this.gotoState(this.stateObject[0].id, true); | 93 | + this.gotoState(this.stateObject[0].id, false); |
94 | } | 94 | } |
95 | 95 | ||
96 | protected stateControllerId(): string { | 96 | protected stateControllerId(): string { |
@@ -95,7 +95,7 @@ export class EntityStateControllerComponent extends StateControllerComponent imp | @@ -95,7 +95,7 @@ export class EntityStateControllerComponent extends StateControllerComponent imp | ||
95 | protected onStateChanged() { | 95 | protected onStateChanged() { |
96 | this.stateObject = this.parseState(this.currentState); | 96 | this.stateObject = this.parseState(this.currentState); |
97 | this.selectedStateIndex = this.stateObject.length - 1; | 97 | this.selectedStateIndex = this.stateObject.length - 1; |
98 | - this.gotoState(this.stateObject[this.stateObject.length - 1].id, true); | 98 | + this.gotoState(this.stateObject[this.stateObject.length - 1].id, false); |
99 | } | 99 | } |
100 | 100 | ||
101 | protected stateControllerId(): string { | 101 | protected stateControllerId(): string { |
@@ -120,7 +120,7 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI | @@ -120,7 +120,7 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI | ||
120 | } | 120 | } |
121 | 121 | ||
122 | ngAfterViewInit(): void { | 122 | ngAfterViewInit(): void { |
123 | - this.selectFirstDashboardIfNeeded(); | 123 | + // this.selectFirstDashboardIfNeeded(); |
124 | } | 124 | } |
125 | 125 | ||
126 | selectFirstDashboardIfNeeded(): void { | 126 | selectFirstDashboardIfNeeded(): void { |
@@ -159,6 +159,7 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI | @@ -159,6 +159,7 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI | ||
159 | } else { | 159 | } else { |
160 | this.modelValue = null; | 160 | this.modelValue = null; |
161 | this.selectDashboardFormGroup.get('dashboard').patchValue(null, {emitEvent: true}); | 161 | this.selectDashboardFormGroup.get('dashboard').patchValue(null, {emitEvent: true}); |
162 | + this.selectFirstDashboardIfNeeded(); | ||
162 | } | 163 | } |
163 | } | 164 | } |
164 | 165 |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2019 The Thingsboard Authors | ||
4 | + | ||
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | + you may not use this file except in compliance with the License. | ||
7 | + You may obtain a copy of the License at | ||
8 | + | ||
9 | + http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | + | ||
11 | + Unless required by applicable law or agreed to in writing, software | ||
12 | + distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | + See the License for the specific language governing permissions and | ||
15 | + limitations under the License. | ||
16 | + | ||
17 | +--> | ||
18 | +<form class="tb-material-icons-dialog" style="min-width: 600px;"> | ||
19 | + <mat-toolbar fxLayout="row" color="primary"> | ||
20 | + <h2>{{ 'icon.select-icon' | translate }}</h2> | ||
21 | + <span fxFlex></span> | ||
22 | + <section fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> | ||
23 | + <mat-slide-toggle [formControl]="showAllControl"> | ||
24 | + </mat-slide-toggle> | ||
25 | + <label translate>icon.show-all</label> | ||
26 | + </section> | ||
27 | + <button mat-button mat-icon-button | ||
28 | + (click)="cancel()" | ||
29 | + type="button"> | ||
30 | + <mat-icon class="material-icons">close</mat-icon> | ||
31 | + </button> | ||
32 | + </mat-toolbar> | ||
33 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | ||
34 | + </mat-progress-bar> | ||
35 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | ||
36 | + <div class="tb-absolute-fill tb-icons-load" *ngIf="loadingIcons$ | async" fxLayout="column" fxLayoutAlign="center center"> | ||
37 | + <mat-spinner color="accent" mode="indeterminate" diameter="40"></mat-spinner> | ||
38 | + </div> | ||
39 | + <div mat-dialog-content> | ||
40 | + <div class="mat-content mat-padding" fxLayout="column"> | ||
41 | + <fieldset [disabled]="(isLoading$ | async)"> | ||
42 | + <ng-template ngFor let-icon [ngForOf]="icons$ | async" let-last="last"> | ||
43 | + <ng-container #iconButtons> | ||
44 | + <button *ngIf="icon === selectedIcon" | ||
45 | + class="tb-select-icon-button" | ||
46 | + mat-button mat-icon-button | ||
47 | + mat-raised-button | ||
48 | + color="primary" | ||
49 | + (click)="selectIcon(icon)" | ||
50 | + matTooltip="{{ icon }}" | ||
51 | + matTooltipPosition="above" | ||
52 | + type="button"> | ||
53 | + <mat-icon>{{icon}}</mat-icon> | ||
54 | + </button> | ||
55 | + <button *ngIf="icon !== selectedIcon" | ||
56 | + class="tb-select-icon-button" | ||
57 | + mat-button mat-icon-button | ||
58 | + (click)="selectIcon(icon)" | ||
59 | + matTooltip="{{ icon }}" | ||
60 | + matTooltipPosition="above" | ||
61 | + type="button"> | ||
62 | + <mat-icon>{{icon}}</mat-icon> | ||
63 | + </button> | ||
64 | + </ng-container> | ||
65 | + </ng-template> | ||
66 | + </fieldset> | ||
67 | + </div> | ||
68 | + </div> | ||
69 | + <div mat-dialog-actions fxLayout="row"> | ||
70 | + <span fxFlex></span> | ||
71 | + <button mat-button | ||
72 | + type="button" | ||
73 | + [disabled]="(isLoading$ | async)" | ||
74 | + (click)="cancel()"> | ||
75 | + {{ 'action.cancel' | translate }} | ||
76 | + </button> | ||
77 | + </div> | ||
78 | +</form> |
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 | +:host { | ||
17 | + .tb-material-icons-dialog { | ||
18 | + position: relative; | ||
19 | + } | ||
20 | + .tb-icons-load { | ||
21 | + top: 64px; | ||
22 | + z-index: 3; | ||
23 | + background: rgba(255, 255, 255, .75); | ||
24 | + } | ||
25 | +} | ||
26 | + | ||
27 | +:host ::ng-deep { | ||
28 | + .tb-material-icons-dialog { | ||
29 | + button.mat-icon-button.tb-select-icon-button { | ||
30 | + width: 56px; | ||
31 | + height: 56px; | ||
32 | + padding: 16px; | ||
33 | + margin: 10px; | ||
34 | + border: solid 1px #ffa500; | ||
35 | + border-radius: 0%; | ||
36 | + line-height: 0; | ||
37 | + } | ||
38 | + } | ||
39 | +} |
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, Inject, OnInit, QueryList, ViewChildren, TemplateRef, AfterViewInit } from '@angular/core'; | ||
18 | +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; | ||
19 | +import { Store } from '@ngrx/store'; | ||
20 | +import { AppState } from '@core/core.state'; | ||
21 | +import { Router } from '@angular/router'; | ||
22 | +import { DialogComponent } from '@app/shared/components/dialog.component'; | ||
23 | +import { UtilsService } from '@core/services/utils.service'; | ||
24 | +import { FormControl } from '@angular/forms'; | ||
25 | +import { Observable, of, merge, noop } from 'rxjs'; | ||
26 | +import { delay, map, mergeMap, share, startWith, tap, mapTo } from 'rxjs/operators'; | ||
27 | +import { DashboardInfo } from '@shared/models/dashboard.models'; | ||
28 | +import { MatTab } from '@angular/material/tabs'; | ||
29 | + | ||
30 | +export interface MaterialIconsDialogData { | ||
31 | + icon: string; | ||
32 | +} | ||
33 | + | ||
34 | +@Component({ | ||
35 | + selector: 'tb-material-icons-dialog', | ||
36 | + templateUrl: './material-icons-dialog.component.html', | ||
37 | + providers: [], | ||
38 | + styleUrls: ['./material-icons-dialog.component.scss'] | ||
39 | +}) | ||
40 | +export class MaterialIconsDialogComponent extends DialogComponent<MaterialIconsDialogComponent, string> | ||
41 | + implements OnInit, AfterViewInit { | ||
42 | + | ||
43 | + @ViewChildren('iconButtons') iconButtons: QueryList<HTMLElement>; | ||
44 | + | ||
45 | + selectedIcon: string; | ||
46 | + icons$: Observable<Array<string>>; | ||
47 | + loadingIcons$: Observable<boolean>; | ||
48 | + | ||
49 | + showAllControl: FormControl; | ||
50 | + | ||
51 | + constructor(protected store: Store<AppState>, | ||
52 | + protected router: Router, | ||
53 | + @Inject(MAT_DIALOG_DATA) public data: MaterialIconsDialogData, | ||
54 | + private utils: UtilsService, | ||
55 | + public dialogRef: MatDialogRef<MaterialIconsDialogComponent, string>) { | ||
56 | + super(store, router, dialogRef); | ||
57 | + this.selectedIcon = data.icon; | ||
58 | + this.showAllControl = new FormControl(false); | ||
59 | + } | ||
60 | + | ||
61 | + ngOnInit(): void { | ||
62 | + this.icons$ = this.showAllControl.valueChanges.pipe( | ||
63 | + map((showAll) => { | ||
64 | + return {firstTime: false, showAll}; | ||
65 | + }), | ||
66 | + startWith<{firstTime: boolean, showAll: boolean}>({firstTime: true, showAll: false}), | ||
67 | + mergeMap((data) => { | ||
68 | + if (data.showAll) { | ||
69 | + return this.utils.getMaterialIcons().pipe(delay(100)); | ||
70 | + } else { | ||
71 | + const res = of(this.utils.getCommonMaterialIcons()); | ||
72 | + return data.firstTime ? res : res.pipe(delay(50)); | ||
73 | + } | ||
74 | + }), | ||
75 | + share() | ||
76 | + ); | ||
77 | + } | ||
78 | + | ||
79 | + ngAfterViewInit(): void { | ||
80 | + this.loadingIcons$ = merge( | ||
81 | + this.showAllControl.valueChanges.pipe( | ||
82 | + mapTo(true), | ||
83 | + ), | ||
84 | + this.iconButtons.changes.pipe( | ||
85 | + delay(100), | ||
86 | + mapTo( false), | ||
87 | + ) | ||
88 | + ).pipe( | ||
89 | + tap((loadingIcons) => { | ||
90 | + if (loadingIcons) { | ||
91 | + this.showAllControl.disable({emitEvent: false}); | ||
92 | + } else { | ||
93 | + this.showAllControl.enable({emitEvent: false}); | ||
94 | + } | ||
95 | + }), | ||
96 | + share() | ||
97 | + ); | ||
98 | + } | ||
99 | + | ||
100 | + selectIcon(icon: string) { | ||
101 | + this.dialogRef.close(icon); | ||
102 | + } | ||
103 | + | ||
104 | + cancel(): void { | ||
105 | + this.dialogRef.close(null); | ||
106 | + } | ||
107 | + | ||
108 | +} |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, Input, HostListener } from '@angular/core'; | 17 | +import { Component, HostListener, Input } from '@angular/core'; |
18 | import { PageComponent } from '@shared/components/page.component'; | 18 | 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'; |
@@ -18,7 +18,7 @@ import { | @@ -18,7 +18,7 @@ import { | ||
18 | Directive, | 18 | Directive, |
19 | ElementRef, | 19 | ElementRef, |
20 | EventEmitter, | 20 | EventEmitter, |
21 | - Input, OnChanges, | 21 | + Input, OnChanges, OnDestroy, |
22 | Output, SimpleChanges, | 22 | Output, SimpleChanges, |
23 | ViewContainerRef | 23 | ViewContainerRef |
24 | } from '@angular/core'; | 24 | } from '@angular/core'; |
@@ -29,7 +29,7 @@ import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; | @@ -29,7 +29,7 @@ import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; | ||
29 | @Directive({ | 29 | @Directive({ |
30 | selector: '[tb-fullscreen]' | 30 | selector: '[tb-fullscreen]' |
31 | }) | 31 | }) |
32 | -export class FullscreenDirective implements OnChanges { | 32 | +export class FullscreenDirective implements OnChanges, OnDestroy { |
33 | 33 | ||
34 | fullscreenValue = false; | 34 | fullscreenValue = false; |
35 | 35 | ||
@@ -69,6 +69,12 @@ export class FullscreenDirective implements OnChanges { | @@ -69,6 +69,12 @@ export class FullscreenDirective implements OnChanges { | ||
69 | } | 69 | } |
70 | } | 70 | } |
71 | 71 | ||
72 | + ngOnDestroy(): void { | ||
73 | + if (this.fullscreen) { | ||
74 | + this.exitFullscreen(); | ||
75 | + } | ||
76 | + } | ||
77 | + | ||
72 | enterFullscreen() { | 78 | enterFullscreen() { |
73 | const targetElement: HTMLElement = this.fullscreenElement || this.elementRef.nativeElement; | 79 | const targetElement: HTMLElement = this.fullscreenElement || this.elementRef.nativeElement; |
74 | this.parentElement = targetElement.parentElement; | 80 | this.parentElement = targetElement.parentElement; |
@@ -17,7 +17,7 @@ | @@ -17,7 +17,7 @@ | ||
17 | --> | 17 | --> |
18 | <div class="tb-js-func" style="background: #fff;" [ngClass]="{'tb-disabled': disabled, 'fill-height': fillHeight}" | 18 | <div class="tb-js-func" style="background: #fff;" [ngClass]="{'tb-disabled': disabled, 'fill-height': fillHeight}" |
19 | tb-fullscreen | 19 | tb-fullscreen |
20 | - [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column"> | 20 | + [fullscreen]="fullscreen" fxLayout="column"> |
21 | <div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-js-func-toolbar"> | 21 | <div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-js-func-toolbar"> |
22 | <label class="tb-title no-padding">{{'function ' + (functionName ? functionName : '') + '(' + functionArgsString + ') {'}}</label> | 22 | <label class="tb-title no-padding">{{'function ' + (functionName ? functionName : '') + '(' + functionArgsString + ') {'}}</label> |
23 | <span fxFlex></span> | 23 | <span fxFlex></span> |
@@ -25,6 +25,7 @@ | @@ -25,6 +25,7 @@ | ||
25 | {{'js-func.tidy' | translate }} | 25 | {{'js-func.tidy' | translate }} |
26 | </button> | 26 | </button> |
27 | <button type='button' mat-button mat-icon-button (click)="fullscreen = !fullscreen" | 27 | <button type='button' mat-button mat-icon-button (click)="fullscreen = !fullscreen" |
28 | + class="tb-mat-32" | ||
28 | matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" | 29 | matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" |
29 | matTooltipPosition="above"> | 30 | matTooltipPosition="above"> |
30 | <mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> | 31 | <mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> |
@@ -16,11 +16,11 @@ | @@ -16,11 +16,11 @@ | ||
16 | .tb-js-func { | 16 | .tb-js-func { |
17 | position: relative; | 17 | position: relative; |
18 | 18 | ||
19 | - .tb-disabled { | 19 | + &.tb-disabled { |
20 | color: rgba(0, 0, 0, .38); | 20 | color: rgba(0, 0, 0, .38); |
21 | } | 21 | } |
22 | 22 | ||
23 | - .fill-height { | 23 | + &.fill-height { |
24 | height: 100%; | 24 | height: 100%; |
25 | } | 25 | } |
26 | 26 | ||
@@ -41,15 +41,20 @@ | @@ -41,15 +41,20 @@ | ||
41 | } | 41 | } |
42 | 42 | ||
43 | .tb-js-func-toolbar { | 43 | .tb-js-func-toolbar { |
44 | - .mat-button.tidy { | 44 | + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 { |
45 | + align-items: center; | ||
46 | + vertical-align: middle; | ||
45 | min-width: 32px; | 47 | min-width: 32px; |
46 | min-height: 15px; | 48 | min-height: 15px; |
47 | padding: 4px; | 49 | padding: 4px; |
48 | - margin: 0 5px 0 0; | 50 | + margin: 0; |
49 | font-size: .8rem; | 51 | font-size: .8rem; |
50 | line-height: 15px; | 52 | line-height: 15px; |
51 | color: #7b7b7b; | 53 | color: #7b7b7b; |
52 | background: rgba(220, 220, 220, .35); | 54 | background: rgba(220, 220, 220, .35); |
55 | + &:not(:last-child) { | ||
56 | + margin-right: 4px; | ||
57 | + } | ||
53 | } | 58 | } |
54 | } | 59 | } |
55 | } | 60 | } |
@@ -14,7 +14,16 @@ | @@ -14,7 +14,16 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; | 17 | +import { |
18 | + ChangeDetectionStrategy, | ||
19 | + Component, | ||
20 | + ElementRef, | ||
21 | + forwardRef, | ||
22 | + Input, OnDestroy, | ||
23 | + OnInit, | ||
24 | + ViewChild, | ||
25 | + ViewEncapsulation | ||
26 | +} from '@angular/core'; | ||
18 | import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; | 27 | import { ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'; |
19 | import * as ace from 'ace-builds'; | 28 | import * as ace from 'ace-builds'; |
20 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; | 29 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
@@ -24,6 +33,7 @@ import { AppState } from '@core/core.state'; | @@ -24,6 +33,7 @@ import { AppState } from '@core/core.state'; | ||
24 | import { UtilsService } from '@core/services/utils.service'; | 33 | import { UtilsService } from '@core/services/utils.service'; |
25 | import { isUndefined } from '@app/core/utils'; | 34 | import { isUndefined } from '@app/core/utils'; |
26 | import { TranslateService } from '@ngx-translate/core'; | 35 | import { TranslateService } from '@ngx-translate/core'; |
36 | +import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; | ||
27 | 37 | ||
28 | @Component({ | 38 | @Component({ |
29 | selector: 'tb-js-func', | 39 | selector: 'tb-js-func', |
@@ -43,12 +53,14 @@ import { TranslateService } from '@ngx-translate/core'; | @@ -43,12 +53,14 @@ import { TranslateService } from '@ngx-translate/core'; | ||
43 | ], | 53 | ], |
44 | encapsulation: ViewEncapsulation.None | 54 | encapsulation: ViewEncapsulation.None |
45 | }) | 55 | }) |
46 | -export class JsFuncComponent implements OnInit, ControlValueAccessor, Validator { | 56 | +export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator { |
47 | 57 | ||
48 | @ViewChild('javascriptEditor', {static: true}) | 58 | @ViewChild('javascriptEditor', {static: true}) |
49 | javascriptEditorElmRef: ElementRef; | 59 | javascriptEditorElmRef: ElementRef; |
50 | 60 | ||
51 | private jsEditor: ace.Ace.Editor; | 61 | private jsEditor: ace.Ace.Editor; |
62 | + private editorsResizeCaf: CancelAnimationFrame; | ||
63 | + private editorResizeListener: any; | ||
52 | 64 | ||
53 | @Input() functionName: string; | 65 | @Input() functionName: string; |
54 | 66 | ||
@@ -100,7 +112,8 @@ export class JsFuncComponent implements OnInit, ControlValueAccessor, Validator | @@ -100,7 +112,8 @@ export class JsFuncComponent implements OnInit, ControlValueAccessor, Validator | ||
100 | constructor(public elementRef: ElementRef, | 112 | constructor(public elementRef: ElementRef, |
101 | private utils: UtilsService, | 113 | private utils: UtilsService, |
102 | private translate: TranslateService, | 114 | private translate: TranslateService, |
103 | - protected store: Store<AppState>) { | 115 | + protected store: Store<AppState>, |
116 | + private raf: RafService) { | ||
104 | } | 117 | } |
105 | 118 | ||
106 | ngOnInit(): void { | 119 | ngOnInit(): void { |
@@ -137,6 +150,28 @@ export class JsFuncComponent implements OnInit, ControlValueAccessor, Validator | @@ -137,6 +150,28 @@ export class JsFuncComponent implements OnInit, ControlValueAccessor, Validator | ||
137 | this.cleanupJsErrors(); | 150 | this.cleanupJsErrors(); |
138 | this.updateView(); | 151 | this.updateView(); |
139 | }); | 152 | }); |
153 | + this.editorResizeListener = this.onAceEditorResize.bind(this); | ||
154 | + // @ts-ignore | ||
155 | + addResizeListener(editorElement, this.editorResizeListener); | ||
156 | + } | ||
157 | + | ||
158 | + ngOnDestroy(): void { | ||
159 | + if (this.editorResizeListener) { | ||
160 | + const editorElement = this.javascriptEditorElmRef.nativeElement; | ||
161 | + // @ts-ignore | ||
162 | + removeResizeListener(editorElement, this.editorResizeListener); | ||
163 | + } | ||
164 | + } | ||
165 | + | ||
166 | + private onAceEditorResize() { | ||
167 | + if (this.editorsResizeCaf) { | ||
168 | + this.editorsResizeCaf(); | ||
169 | + this.editorsResizeCaf = null; | ||
170 | + } | ||
171 | + this.editorsResizeCaf = this.raf.raf(() => { | ||
172 | + this.jsEditor.resize(); | ||
173 | + this.jsEditor.renderer.updateFull(); | ||
174 | + }); | ||
140 | } | 175 | } |
141 | 176 | ||
142 | registerOnChange(fn: any): void { | 177 | registerOnChange(fn: any): void { |
@@ -298,12 +333,4 @@ export class JsFuncComponent implements OnInit, ControlValueAccessor, Validator | @@ -298,12 +333,4 @@ export class JsFuncComponent implements OnInit, ControlValueAccessor, Validator | ||
298 | } | 333 | } |
299 | } | 334 | } |
300 | 335 | ||
301 | - onFullscreen() { | ||
302 | - if (this.jsEditor) { | ||
303 | - setTimeout(() => { | ||
304 | - this.jsEditor.resize(); | ||
305 | - }, 0); | ||
306 | - } | ||
307 | - } | ||
308 | - | ||
309 | } | 336 | } |
@@ -32,6 +32,6 @@ | @@ -32,6 +32,6 @@ | ||
32 | </div> | 32 | </div> |
33 | <div fxFlex="0%" id="tb-json-panel" tb-toast toastTarget="jsonObjectEditor" | 33 | <div fxFlex="0%" id="tb-json-panel" tb-toast toastTarget="jsonObjectEditor" |
34 | class="tb-json-object-panel" fxLayout="column"> | 34 | class="tb-json-object-panel" fxLayout="column"> |
35 | - <div fxFlex #jsonEditor id="tb-json-input" [ngClass]="{'fill-height': fillHeight}"></div> | 35 | + <div #jsonEditor id="tb-json-input" [ngStyle]="editorStyle" [ngClass]="{'fill-height': fillHeight}"></div> |
36 | </div> | 36 | </div> |
37 | </div> | 37 | </div> |