Commit 59606f8330ce2c056b88c8eac94cefab03a764c8
1 parent
fbf2d3ef
Add support for dashboboards in mobile app. Introduce widget mobile actions.
Showing
18 changed files
with
1186 additions
and
20 deletions
@@ -533,6 +533,37 @@ public class DashboardController extends BaseController { | @@ -533,6 +533,37 @@ public class DashboardController extends BaseController { | ||
533 | } | 533 | } |
534 | } | 534 | } |
535 | 535 | ||
536 | + @PreAuthorize("isAuthenticated()") | ||
537 | + @RequestMapping(value = "/dashboard/home/info", method = RequestMethod.GET) | ||
538 | + @ResponseBody | ||
539 | + public HomeDashboardInfo getHomeDashboardInfo() throws ThingsboardException { | ||
540 | + try { | ||
541 | + SecurityUser securityUser = getCurrentUser(); | ||
542 | + if (securityUser.isSystemAdmin()) { | ||
543 | + return null; | ||
544 | + } | ||
545 | + User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId()); | ||
546 | + JsonNode additionalInfo = user.getAdditionalInfo(); | ||
547 | + HomeDashboardInfo homeDashboardInfo; | ||
548 | + homeDashboardInfo = extractHomeDashboardInfoFromAdditionalInfo(additionalInfo); | ||
549 | + if (homeDashboardInfo == null) { | ||
550 | + if (securityUser.isCustomerUser()) { | ||
551 | + Customer customer = customerService.findCustomerById(securityUser.getTenantId(), securityUser.getCustomerId()); | ||
552 | + additionalInfo = customer.getAdditionalInfo(); | ||
553 | + homeDashboardInfo = extractHomeDashboardInfoFromAdditionalInfo(additionalInfo); | ||
554 | + } | ||
555 | + if (homeDashboardInfo == null) { | ||
556 | + Tenant tenant = tenantService.findTenantById(securityUser.getTenantId()); | ||
557 | + additionalInfo = tenant.getAdditionalInfo(); | ||
558 | + homeDashboardInfo = extractHomeDashboardInfoFromAdditionalInfo(additionalInfo); | ||
559 | + } | ||
560 | + } | ||
561 | + return homeDashboardInfo; | ||
562 | + } catch (Exception e) { | ||
563 | + throw handleException(e); | ||
564 | + } | ||
565 | + } | ||
566 | + | ||
536 | @PreAuthorize("hasAuthority('TENANT_ADMIN')") | 567 | @PreAuthorize("hasAuthority('TENANT_ADMIN')") |
537 | @RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.GET) | 568 | @RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.GET) |
538 | @ResponseBody | 569 | @ResponseBody |
@@ -582,6 +613,22 @@ public class DashboardController extends BaseController { | @@ -582,6 +613,22 @@ public class DashboardController extends BaseController { | ||
582 | } | 613 | } |
583 | } | 614 | } |
584 | 615 | ||
616 | + private HomeDashboardInfo extractHomeDashboardInfoFromAdditionalInfo(JsonNode additionalInfo) { | ||
617 | + try { | ||
618 | + if (additionalInfo != null && additionalInfo.has(HOME_DASHBOARD_ID) && !additionalInfo.get(HOME_DASHBOARD_ID).isNull()) { | ||
619 | + String strDashboardId = additionalInfo.get(HOME_DASHBOARD_ID).asText(); | ||
620 | + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); | ||
621 | + checkDashboardId(dashboardId, Operation.READ); | ||
622 | + boolean hideDashboardToolbar = true; | ||
623 | + if (additionalInfo.has(HOME_DASHBOARD_HIDE_TOOLBAR)) { | ||
624 | + hideDashboardToolbar = additionalInfo.get(HOME_DASHBOARD_HIDE_TOOLBAR).asBoolean(); | ||
625 | + } | ||
626 | + return new HomeDashboardInfo(dashboardId, hideDashboardToolbar); | ||
627 | + } | ||
628 | + } catch (Exception e) {} | ||
629 | + return null; | ||
630 | + } | ||
631 | + | ||
585 | private HomeDashboard extractHomeDashboardFromAdditionalInfo(JsonNode additionalInfo) { | 632 | private HomeDashboard extractHomeDashboardFromAdditionalInfo(JsonNode additionalInfo) { |
586 | try { | 633 | try { |
587 | if (additionalInfo != null && additionalInfo.has(HOME_DASHBOARD_ID) && !additionalInfo.get(HOME_DASHBOARD_ID).isNull()) { | 634 | if (additionalInfo != null && additionalInfo.has(HOME_DASHBOARD_ID) && !additionalInfo.get(HOME_DASHBOARD_ID).isNull()) { |
1 | +/// | ||
2 | +/// Copyright © 2016-2021 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 { Inject, Injectable } from '@angular/core'; | ||
18 | +import { WINDOW } from '@core/services/window.service'; | ||
19 | +import { isDefined } from '@core/utils'; | ||
20 | +import { MobileActionResult, WidgetMobileActionResult, WidgetMobileActionType } from '@shared/models/widget.models'; | ||
21 | +import { from, of } from 'rxjs'; | ||
22 | +import { Observable } from 'rxjs/internal/Observable'; | ||
23 | +import { catchError } from 'rxjs/operators'; | ||
24 | + | ||
25 | +const dashboardStateNameHandler = 'tbMobileDashboardStateNameHandler'; | ||
26 | +const mobileHandler = 'tbMobileHandler'; | ||
27 | + | ||
28 | +// @dynamic | ||
29 | +@Injectable({ | ||
30 | + providedIn: 'root' | ||
31 | +}) | ||
32 | +export class MobileService { | ||
33 | + | ||
34 | + private readonly mobileApp; | ||
35 | + private readonly mobileChannel; | ||
36 | + | ||
37 | + constructor(@Inject(WINDOW) private window: Window) { | ||
38 | + const w = (this.window as any); | ||
39 | + this.mobileChannel = w.flutter_inappwebview; | ||
40 | + this.mobileApp = isDefined(this.mobileChannel); | ||
41 | + } | ||
42 | + | ||
43 | + public isMobileApp(): boolean { | ||
44 | + return this.mobileApp; | ||
45 | + } | ||
46 | + | ||
47 | + public handleDashboardStateName(name: string) { | ||
48 | + if (this.mobileApp) { | ||
49 | + this.mobileChannel.callHandler(dashboardStateNameHandler, name); | ||
50 | + } | ||
51 | + } | ||
52 | + | ||
53 | + public handleWidgetMobileAction<T extends MobileActionResult>(type: WidgetMobileActionType, ...args: any[]): | ||
54 | + Observable<WidgetMobileActionResult<T>> { | ||
55 | + if (this.mobileApp) { | ||
56 | + return from( | ||
57 | + this.mobileChannel.callHandler(mobileHandler, type, ...args) as Promise<WidgetMobileActionResult<T>>).pipe( | ||
58 | + catchError((err: Error) => { | ||
59 | + return of({ | ||
60 | + hasError: true, | ||
61 | + error: err?.message ? err.message : `Failed to execute mobile action ${type}` | ||
62 | + } as WidgetMobileActionResult<any>); | ||
63 | + }) | ||
64 | + ); | ||
65 | + } else { | ||
66 | + return of(null); | ||
67 | + } | ||
68 | + } | ||
69 | + | ||
70 | +} |
@@ -15,7 +15,7 @@ | @@ -15,7 +15,7 @@ | ||
15 | limitations under the License. | 15 | limitations under the License. |
16 | 16 | ||
17 | --> | 17 | --> |
18 | -<div class="tb-dashboard-page mat-content" style="padding-top: 150px;" | 18 | +<div class="tb-dashboard-page mat-content" [ngClass]="{'mobile-app': isMobileApp && !isEdit}" style="padding-top: 150px;" |
19 | fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen"> | 19 | fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen"> |
20 | <tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet> | 20 | <tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet> |
21 | <section class="tb-dashboard-toolbar" | 21 | <section class="tb-dashboard-toolbar" |
@@ -25,7 +25,7 @@ | @@ -25,7 +25,7 @@ | ||
25 | [toolbarOpened]="toolbarOpened" (triggerClick)="openToolbar()"> | 25 | [toolbarOpened]="toolbarOpened" (triggerClick)="openToolbar()"> |
26 | <div class="tb-dashboard-action-panels" fxLayout="column" fxLayout.gt-sm="row" | 26 | <div class="tb-dashboard-action-panels" fxLayout="column" fxLayout.gt-sm="row" |
27 | fxLayoutAlign="center stretch" fxLayoutAlign.gt-sm="space-between center"> | 27 | fxLayoutAlign="center stretch" fxLayoutAlign.gt-sm="space-between center"> |
28 | - <div class="tb-dashboard-action-panel" fxFlex="auto" fxLayout="row-reverse" | 28 | + <div class="tb-dashboard-action-panel" fxFlex="auto" fxLayout="row-reverse" [fxHide]="isMobileApp && !isEdit" |
29 | fxLayoutAlign.gt-sm="end center" fxLayoutAlign="space-between center" fxLayoutGap="12px"> | 29 | fxLayoutAlign.gt-sm="end center" fxLayoutAlign="space-between center" fxLayoutGap="12px"> |
30 | <tb-user-menu *ngIf="!isPublicUser() && forceFullscreen" fxHide.gt-sm displayUserInfo="true"> | 30 | <tb-user-menu *ngIf="!isPublicUser() && forceFullscreen" fxHide.gt-sm displayUserInfo="true"> |
31 | </tb-user-menu> | 31 | </tb-user-menu> |
@@ -49,7 +49,7 @@ | @@ -49,7 +49,7 @@ | ||
49 | <tb-states-component fxFlex.lt-md | 49 | <tb-states-component fxFlex.lt-md |
50 | [statesControllerId]="isEdit ? 'default' : dashboardConfiguration.settings.stateControllerId" | 50 | [statesControllerId]="isEdit ? 'default' : dashboardConfiguration.settings.stateControllerId" |
51 | [dashboardCtrl]="this" | 51 | [dashboardCtrl]="this" |
52 | - [dashboardId]="(!embedded && dashboard.id) ? dashboard.id.id : ''" | 52 | + [dashboardId]="setStateDashboardId ? dashboard.id.id : ''" |
53 | [isMobile]="isMobile" | 53 | [isMobile]="isMobile" |
54 | [state]="dashboardCtx.state" | 54 | [state]="dashboardCtx.state" |
55 | [currentState]="currentState" | 55 | [currentState]="currentState" |
@@ -18,6 +18,7 @@ | @@ -18,6 +18,7 @@ | ||
18 | $toolbar-height: 50px !default; | 18 | $toolbar-height: 50px !default; |
19 | $fullscreen-toolbar-height: 64px !default; | 19 | $fullscreen-toolbar-height: 64px !default; |
20 | $mobile-toolbar-height: 84px !default; | 20 | $mobile-toolbar-height: 84px !default; |
21 | +$mobile-app-toolbar-height: 40px !default; | ||
21 | 22 | ||
22 | tb-dashboard-page { | 23 | tb-dashboard-page { |
23 | display: flex; | 24 | display: flex; |
@@ -101,6 +102,14 @@ div.tb-dashboard-page { | @@ -101,6 +102,14 @@ div.tb-dashboard-page { | ||
101 | } | 102 | } |
102 | } | 103 | } |
103 | 104 | ||
105 | + &.mobile-app { | ||
106 | + .tb-dashboard-container { | ||
107 | + &.tb-dashboard-toolbar-opened { | ||
108 | + margin-top: $mobile-app-toolbar-height; | ||
109 | + } | ||
110 | + } | ||
111 | + } | ||
112 | + | ||
104 | mat-drawer-container.tb-dashboard-drawer-container { | 113 | mat-drawer-container.tb-dashboard-drawer-container { |
105 | mat-drawer-container.tb-dashboard-layouts { | 114 | mat-drawer-container.tb-dashboard-layouts { |
106 | width: 100%; | 115 | width: 100%; |
@@ -120,7 +120,8 @@ import { | @@ -120,7 +120,8 @@ import { | ||
120 | DisplayWidgetTypesPanelData | 120 | DisplayWidgetTypesPanelData |
121 | } from '@home/components/dashboard-page/widget-types-panel.component'; | 121 | } from '@home/components/dashboard-page/widget-types-panel.component'; |
122 | import { DashboardWidgetSelectComponent } from '@home/components/dashboard-page/dashboard-widget-select.component'; | 122 | import { DashboardWidgetSelectComponent } from '@home/components/dashboard-page/dashboard-widget-select.component'; |
123 | -import {AliasEntityType, EntityType} from "@shared/models/entity-type.models"; | 123 | +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; |
124 | +import { MobileService } from '@core/services/mobile.service'; | ||
124 | 125 | ||
125 | // @dynamic | 126 | // @dynamic |
126 | @Component({ | 127 | @Component({ |
@@ -159,6 +160,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -159,6 +160,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
159 | singlePageMode: boolean; | 160 | singlePageMode: boolean; |
160 | forceFullscreen = this.authState.forceFullscreen; | 161 | forceFullscreen = this.authState.forceFullscreen; |
161 | 162 | ||
163 | + isMobileApp = this.mobileService.isMobileApp(); | ||
162 | isFullscreen = false; | 164 | isFullscreen = false; |
163 | isEdit = false; | 165 | isEdit = false; |
164 | isEditingWidget = false; | 166 | isEditingWidget = false; |
@@ -189,6 +191,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -189,6 +191,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
189 | currentCustomerId: string; | 191 | currentCustomerId: string; |
190 | currentDashboardScope: DashboardPageScope; | 192 | currentDashboardScope: DashboardPageScope; |
191 | 193 | ||
194 | + setStateDashboardId = false; | ||
195 | + | ||
192 | addingLayoutCtx: DashboardPageLayoutContext; | 196 | addingLayoutCtx: DashboardPageLayoutContext; |
193 | 197 | ||
194 | logo = 'assets/logo_title_white.svg'; | 198 | logo = 'assets/logo_title_white.svg'; |
@@ -284,6 +288,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -284,6 +288,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
284 | private dashboardService: DashboardService, | 288 | private dashboardService: DashboardService, |
285 | private itembuffer: ItemBufferService, | 289 | private itembuffer: ItemBufferService, |
286 | private importExport: ImportExportService, | 290 | private importExport: ImportExportService, |
291 | + private mobileService: MobileService, | ||
287 | private fb: FormBuilder, | 292 | private fb: FormBuilder, |
288 | private dialog: MatDialog, | 293 | private dialog: MatDialog, |
289 | private translate: TranslateService, | 294 | private translate: TranslateService, |
@@ -322,6 +327,19 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -322,6 +327,19 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
322 | 327 | ||
323 | this.reset(); | 328 | this.reset(); |
324 | 329 | ||
330 | + this.dashboard = data.dashboard; | ||
331 | + if (!this.embedded && this.dashboard.id) { | ||
332 | + this.setStateDashboardId = true; | ||
333 | + } | ||
334 | + | ||
335 | + if (this.route.snapshot.queryParamMap.has('hideToolbar')) { | ||
336 | + this.hideToolbar = this.route.snapshot.queryParamMap.get('hideToolbar') === 'true'; | ||
337 | + } | ||
338 | + | ||
339 | + if (this.route.snapshot.queryParamMap.has('embedded')) { | ||
340 | + this.embedded = this.route.snapshot.queryParamMap.get('embedded') === 'true'; | ||
341 | + } | ||
342 | + | ||
325 | this.currentDashboardId = data.currentDashboardId; | 343 | this.currentDashboardId = data.currentDashboardId; |
326 | 344 | ||
327 | if (this.route.snapshot.params.customerId) { | 345 | if (this.route.snapshot.params.customerId) { |
@@ -332,7 +350,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -332,7 +350,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
332 | this.currentCustomerId = this.authUser.customerId; | 350 | this.currentCustomerId = this.authUser.customerId; |
333 | } | 351 | } |
334 | 352 | ||
335 | - this.dashboard = data.dashboard; | ||
336 | this.dashboardConfiguration = this.dashboard.configuration; | 353 | this.dashboardConfiguration = this.dashboard.configuration; |
337 | this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow; | 354 | this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow; |
338 | this.layouts.main.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboardCtx); | 355 | this.layouts.main.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboardCtx); |
@@ -388,6 +405,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | @@ -388,6 +405,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC | ||
388 | this.currentCustomerId = null; | 405 | this.currentCustomerId = null; |
389 | this.currentDashboardScope = null; | 406 | this.currentDashboardScope = null; |
390 | 407 | ||
408 | + this.setStateDashboardId = false; | ||
409 | + | ||
391 | this.dashboardCtx.state = null; | 410 | this.dashboardCtx.state = null; |
392 | } | 411 | } |
393 | 412 |
@@ -21,6 +21,42 @@ $mobile-toolbar-height: 80px !default; | @@ -21,6 +21,42 @@ $mobile-toolbar-height: 80px !default; | ||
21 | $half-mobile-toolbar-height: 40px !default; | 21 | $half-mobile-toolbar-height: 40px !default; |
22 | $mobile-toolbar-height-total: 84px !default; | 22 | $mobile-toolbar-height-total: 84px !default; |
23 | 23 | ||
24 | +$mobile-app-toolbar-height: 40px !default; | ||
25 | + | ||
26 | +div.tb-dashboard-page.mobile-app { | ||
27 | + tb-dashboard-toolbar { | ||
28 | + mat-fab-toolbar { | ||
29 | + .mat-fab-toolbar-wrapper { | ||
30 | + height: $mobile-app-toolbar-height; | ||
31 | + mat-toolbar { | ||
32 | + height: $mobile-app-toolbar-height; | ||
33 | + min-height: $mobile-app-toolbar-height; | ||
34 | + .mat-toolbar-tools { | ||
35 | + height: $mobile-app-toolbar-height; | ||
36 | + min-height: $mobile-app-toolbar-height; | ||
37 | + } | ||
38 | + mat-fab-actions { | ||
39 | + height: $mobile-app-toolbar-height; | ||
40 | + max-height: $mobile-app-toolbar-height; | ||
41 | + .mat-fab-action-item { | ||
42 | + height: $mobile-app-toolbar-height; | ||
43 | + .tb-dashboard-action-panels { | ||
44 | + height: $mobile-app-toolbar-height; | ||
45 | + .tb-dashboard-action-panel { | ||
46 | + height: $mobile-app-toolbar-height; | ||
47 | + > div { | ||
48 | + height: $mobile-app-toolbar-height; | ||
49 | + } | ||
50 | + } | ||
51 | + } | ||
52 | + } | ||
53 | + } | ||
54 | + } | ||
55 | + } | ||
56 | + } | ||
57 | + } | ||
58 | +} | ||
59 | + | ||
24 | tb-dashboard-toolbar { | 60 | tb-dashboard-toolbar { |
25 | mat-fab-toolbar { | 61 | mat-fab-toolbar { |
26 | mat-fab-trigger { | 62 | mat-fab-trigger { |
@@ -26,6 +26,7 @@ import { UtilsService } from '@core/services/utils.service'; | @@ -26,6 +26,7 @@ import { UtilsService } from '@core/services/utils.service'; | ||
26 | import { base64toObj, objToBase64URI } from '@app/core/utils'; | 26 | import { base64toObj, objToBase64URI } from '@app/core/utils'; |
27 | import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; | 27 | import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; |
28 | import { EntityService } from '@core/http/entity.service'; | 28 | import { EntityService } from '@core/http/entity.service'; |
29 | +import { MobileService } from '@core/services/mobile.service'; | ||
29 | 30 | ||
30 | @Component({ | 31 | @Component({ |
31 | selector: 'tb-default-state-controller', | 32 | selector: 'tb-default-state-controller', |
@@ -40,6 +41,7 @@ export class DefaultStateControllerComponent extends StateControllerComponent im | @@ -40,6 +41,7 @@ export class DefaultStateControllerComponent extends StateControllerComponent im | ||
40 | protected statesControllerService: StatesControllerService, | 41 | protected statesControllerService: StatesControllerService, |
41 | private utils: UtilsService, | 42 | private utils: UtilsService, |
42 | private entityService: EntityService, | 43 | private entityService: EntityService, |
44 | + private mobileService: MobileService, | ||
43 | private dashboardUtils: DashboardUtilsService) { | 45 | private dashboardUtils: DashboardUtilsService) { |
44 | super(router, route, ngZone, statesControllerService); | 46 | super(router, route, ngZone, statesControllerService); |
45 | } | 47 | } |
@@ -229,6 +231,9 @@ export class DefaultStateControllerComponent extends StateControllerComponent im | @@ -229,6 +231,9 @@ export class DefaultStateControllerComponent extends StateControllerComponent im | ||
229 | private gotoState(stateId: string, update: boolean, openRightLayout?: boolean) { | 231 | private gotoState(stateId: string, update: boolean, openRightLayout?: boolean) { |
230 | if (this.dashboardCtrl.dashboardCtx.state !== stateId) { | 232 | if (this.dashboardCtrl.dashboardCtx.state !== stateId) { |
231 | this.dashboardCtrl.openDashboardState(stateId, openRightLayout); | 233 | this.dashboardCtrl.openDashboardState(stateId, openRightLayout); |
234 | + if (stateId && this.statesValue[stateId]) { | ||
235 | + this.mobileService.handleDashboardStateName(this.getStateName(stateId, this.statesValue[stateId])); | ||
236 | + } | ||
232 | if (update) { | 237 | if (update) { |
233 | this.updateLocation(); | 238 | this.updateLocation(); |
234 | } | 239 | } |
@@ -28,6 +28,7 @@ import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; | @@ -28,6 +28,7 @@ import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; | ||
28 | import { EntityService } from '@core/http/entity.service'; | 28 | import { EntityService } from '@core/http/entity.service'; |
29 | import { EntityType } from '@shared/models/entity-type.models'; | 29 | import { EntityType } from '@shared/models/entity-type.models'; |
30 | import { map, tap } from 'rxjs/operators'; | 30 | import { map, tap } from 'rxjs/operators'; |
31 | +import { MobileService } from '@core/services/mobile.service'; | ||
31 | 32 | ||
32 | @Component({ | 33 | @Component({ |
33 | selector: 'tb-entity-state-controller', | 34 | selector: 'tb-entity-state-controller', |
@@ -44,6 +45,7 @@ export class EntityStateControllerComponent extends StateControllerComponent imp | @@ -44,6 +45,7 @@ export class EntityStateControllerComponent extends StateControllerComponent imp | ||
44 | protected statesControllerService: StatesControllerService, | 45 | protected statesControllerService: StatesControllerService, |
45 | private utils: UtilsService, | 46 | private utils: UtilsService, |
46 | private entityService: EntityService, | 47 | private entityService: EntityService, |
48 | + private mobileService: MobileService, | ||
47 | private dashboardUtils: DashboardUtilsService) { | 49 | private dashboardUtils: DashboardUtilsService) { |
48 | super(router, route, ngZone, statesControllerService); | 50 | super(router, route, ngZone, statesControllerService); |
49 | } | 51 | } |
@@ -270,6 +272,7 @@ export class EntityStateControllerComponent extends StateControllerComponent imp | @@ -270,6 +272,7 @@ export class EntityStateControllerComponent extends StateControllerComponent imp | ||
270 | 272 | ||
271 | private gotoState(stateId: string, update: boolean, openRightLayout?: boolean) { | 273 | private gotoState(stateId: string, update: boolean, openRightLayout?: boolean) { |
272 | this.dashboardCtrl.openDashboardState(stateId, openRightLayout); | 274 | this.dashboardCtrl.openDashboardState(stateId, openRightLayout); |
275 | + this.mobileService.handleDashboardStateName(this.getStateName(this.stateObject.length - 1)); | ||
273 | if (update) { | 276 | if (update) { |
274 | this.updateLocation(); | 277 | this.updateLocation(); |
275 | } | 278 | } |
@@ -55,6 +55,7 @@ import { ManageWidgetActionsComponent } from '@home/components/widget/action/man | @@ -55,6 +55,7 @@ import { ManageWidgetActionsComponent } from '@home/components/widget/action/man | ||
55 | import { WidgetActionDialogComponent } from '@home/components/widget/action/widget-action-dialog.component'; | 55 | import { WidgetActionDialogComponent } from '@home/components/widget/action/widget-action-dialog.component'; |
56 | import { CustomActionPrettyResourcesTabsComponent } from '@home/components/widget/action/custom-action-pretty-resources-tabs.component'; | 56 | import { CustomActionPrettyResourcesTabsComponent } from '@home/components/widget/action/custom-action-pretty-resources-tabs.component'; |
57 | import { CustomActionPrettyEditorComponent } from '@home/components/widget/action/custom-action-pretty-editor.component'; | 57 | import { CustomActionPrettyEditorComponent } from '@home/components/widget/action/custom-action-pretty-editor.component'; |
58 | +import { MobileActionEditorComponent } from '@home/components/widget/action/mobile-action-editor.component'; | ||
58 | import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service'; | 59 | import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service'; |
59 | import { CustomDialogContainerComponent } from '@home/components/widget/dialog/custom-dialog-container.component'; | 60 | import { CustomDialogContainerComponent } from '@home/components/widget/dialog/custom-dialog-container.component'; |
60 | import { ImportExportService } from '@home/components/import-export/import-export.service'; | 61 | import { ImportExportService } from '@home/components/import-export/import-export.service'; |
@@ -183,6 +184,7 @@ import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-pag | @@ -183,6 +184,7 @@ import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-pag | ||
183 | WidgetActionDialogComponent, | 184 | WidgetActionDialogComponent, |
184 | CustomActionPrettyResourcesTabsComponent, | 185 | CustomActionPrettyResourcesTabsComponent, |
185 | CustomActionPrettyEditorComponent, | 186 | CustomActionPrettyEditorComponent, |
187 | + MobileActionEditorComponent, | ||
186 | CustomDialogContainerComponent, | 188 | CustomDialogContainerComponent, |
187 | ImportDialogComponent, | 189 | ImportDialogComponent, |
188 | ImportDialogCsvComponent, | 190 | ImportDialogCsvComponent, |
@@ -296,6 +298,7 @@ import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-pag | @@ -296,6 +298,7 @@ import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-pag | ||
296 | WidgetActionDialogComponent, | 298 | WidgetActionDialogComponent, |
297 | CustomActionPrettyResourcesTabsComponent, | 299 | CustomActionPrettyResourcesTabsComponent, |
298 | CustomActionPrettyEditorComponent, | 300 | CustomActionPrettyEditorComponent, |
301 | + MobileActionEditorComponent, | ||
299 | CustomDialogContainerComponent, | 302 | CustomDialogContainerComponent, |
300 | ImportDialogComponent, | 303 | ImportDialogComponent, |
301 | ImportDialogCsvComponent, | 304 | ImportDialogCsvComponent, |
1 | +<!-- | ||
2 | + | ||
3 | + Copyright © 2016-2021 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 [formGroup]="mobileActionFormGroup"> | ||
19 | + <mat-form-field class="mat-block"> | ||
20 | + <mat-label translate>widget-action.mobile.action-type</mat-label> | ||
21 | + <mat-select required formControlName="type"> | ||
22 | + <mat-option *ngFor="let actionType of mobileActionTypes" [value]="actionType"> | ||
23 | + {{ mobileActionTypeTranslations.get(mobileActionType[actionType]) | translate }} | ||
24 | + </mat-option> | ||
25 | + </mat-select> | ||
26 | + <mat-error *ngIf="mobileActionFormGroup.get('type').hasError('required')"> | ||
27 | + {{ 'widget-action.mobile.action-type-required' | translate }} | ||
28 | + </mat-error> | ||
29 | + </mat-form-field> | ||
30 | + <div [formGroup]="mobileActionTypeFormGroup" [ngSwitch]="mobileActionFormGroup.get('type').value"> | ||
31 | + <ng-template [ngSwitchCase]="mobileActionFormGroup.get('type').value === mobileActionType.mapDirection || | ||
32 | + mobileActionFormGroup.get('type').value === mobileActionType.mapLocation ? | ||
33 | + mobileActionFormGroup.get('type').value : ''"> | ||
34 | + <tb-js-func | ||
35 | + formControlName="getLocationFunction" | ||
36 | + functionName="getLocation" | ||
37 | + [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" | ||
38 | + [editorCompleter]="customActionEditorCompleter" | ||
39 | + ></tb-js-func> | ||
40 | + </ng-template> | ||
41 | + <ng-template [ngSwitchCase]="mobileActionType.makePhoneCall"> | ||
42 | + <tb-js-func | ||
43 | + formControlName="getPhoneNumberFunction" | ||
44 | + functionName="getPhoneNumber" | ||
45 | + [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" | ||
46 | + [editorCompleter]="customActionEditorCompleter" | ||
47 | + ></tb-js-func> | ||
48 | + </ng-template> | ||
49 | + <ng-template [ngSwitchCase]="mobileActionFormGroup.get('type').value === mobileActionType.takePhoto || | ||
50 | + mobileActionFormGroup.get('type').value === mobileActionType.takePictureFromGallery || | ||
51 | + mobileActionFormGroup.get('type').value === mobileActionType.takeScreenshot ? | ||
52 | + mobileActionFormGroup.get('type').value : ''"> | ||
53 | + <tb-js-func | ||
54 | + formControlName="processImageFunction" | ||
55 | + functionName="processImage" | ||
56 | + [functionArgs]="['imageUrl', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" | ||
57 | + [editorCompleter]="customActionEditorCompleter" | ||
58 | + ></tb-js-func> | ||
59 | + </ng-template> | ||
60 | + <ng-template [ngSwitchCase]="mobileActionType.scanQrCode"> | ||
61 | + <tb-js-func | ||
62 | + formControlName="processQrCodeFunction" | ||
63 | + functionName="processQrCode" | ||
64 | + [functionArgs]="['code', 'format', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" | ||
65 | + [editorCompleter]="customActionEditorCompleter" | ||
66 | + ></tb-js-func> | ||
67 | + </ng-template> | ||
68 | + <ng-template [ngSwitchCase]="mobileActionType.getLocation"> | ||
69 | + <tb-js-func | ||
70 | + formControlName="processLocationFunction" | ||
71 | + functionName="processLocation" | ||
72 | + [functionArgs]="['latitude', 'longitude', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" | ||
73 | + [editorCompleter]="customActionEditorCompleter" | ||
74 | + ></tb-js-func> | ||
75 | + </ng-template> | ||
76 | + <ng-template [ngSwitchCase]="mobileActionFormGroup.get('type').value === mobileActionType.mapDirection || | ||
77 | + mobileActionFormGroup.get('type').value === mobileActionType.mapLocation || | ||
78 | + mobileActionFormGroup.get('type').value === mobileActionType.makePhoneCall ? | ||
79 | + mobileActionFormGroup.get('type').value : ''"> | ||
80 | + <tb-js-func | ||
81 | + formControlName="processLaunchResultFunction" | ||
82 | + functionName="processLaunchResult" | ||
83 | + [functionArgs]="['launched', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" | ||
84 | + [editorCompleter]="customActionEditorCompleter" | ||
85 | + ></tb-js-func> | ||
86 | + </ng-template> | ||
87 | + </div> | ||
88 | + <tb-js-func *ngIf="mobileActionFormGroup.get('type').value" | ||
89 | + formControlName="handleEmptyResultFunction" | ||
90 | + functionName="handleEmptyResult" | ||
91 | + [functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" | ||
92 | + [editorCompleter]="customActionEditorCompleter" | ||
93 | + ></tb-js-func> | ||
94 | + <tb-js-func *ngIf="mobileActionFormGroup.get('type').value" | ||
95 | + formControlName="handleErrorFunction" | ||
96 | + functionName="handleError" | ||
97 | + [functionArgs]="['error', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" | ||
98 | + [editorCompleter]="customActionEditorCompleter" | ||
99 | + ></tb-js-func> | ||
100 | +</div> |
1 | +/// | ||
2 | +/// Copyright © 2016-2021 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, forwardRef, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; | ||
18 | +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; | ||
19 | +import { Store } from '@ngrx/store'; | ||
20 | +import { AppState } from '@app/core/core.state'; | ||
21 | +import { coerceBooleanProperty } from '@angular/cdk/coercion'; | ||
22 | +import { deepClone } from '@core/utils'; | ||
23 | +import { | ||
24 | + WidgetMobileActionDescriptor, | ||
25 | + WidgetMobileActionType, widgetMobileActionTypeTranslationMap | ||
26 | +} from '@shared/models/widget.models'; | ||
27 | +import { CustomActionEditorCompleter } from '@home/components/widget/action/custom-action.models'; | ||
28 | +import { JsFuncComponent } from '@shared/components/js-func.component'; | ||
29 | +import { | ||
30 | + getDefaultGetLocationFunction, getDefaultGetPhoneNumberFunction, | ||
31 | + getDefaultHandleEmptyResultFunction, | ||
32 | + getDefaultHandleErrorFunction, | ||
33 | + getDefaultProcessImageFunction, | ||
34 | + getDefaultProcessLaunchResultFunction, getDefaultProcessLocationFunction, | ||
35 | + getDefaultProcessQrCodeFunction | ||
36 | +} from '@home/components/widget/action/mobile-action-editor.models'; | ||
37 | + | ||
38 | +@Component({ | ||
39 | + selector: 'tb-mobile-action-editor', | ||
40 | + templateUrl: './mobile-action-editor.component.html', | ||
41 | + styleUrls: [], | ||
42 | + providers: [{ | ||
43 | + provide: NG_VALUE_ACCESSOR, | ||
44 | + useExisting: forwardRef(() => MobileActionEditorComponent), | ||
45 | + multi: true | ||
46 | + }] | ||
47 | +}) | ||
48 | +export class MobileActionEditorComponent implements ControlValueAccessor, OnInit { | ||
49 | + | ||
50 | + @ViewChildren(JsFuncComponent) jsFuncComponents: QueryList<JsFuncComponent>; | ||
51 | + | ||
52 | + mobileActionTypes = Object.keys(WidgetMobileActionType); | ||
53 | + mobileActionTypeTranslations = widgetMobileActionTypeTranslationMap; | ||
54 | + mobileActionType = WidgetMobileActionType; | ||
55 | + | ||
56 | + customActionEditorCompleter = CustomActionEditorCompleter; | ||
57 | + | ||
58 | + mobileActionFormGroup: FormGroup; | ||
59 | + mobileActionTypeFormGroup: FormGroup; | ||
60 | + | ||
61 | + private requiredValue: boolean; | ||
62 | + get required(): boolean { | ||
63 | + return this.requiredValue; | ||
64 | + } | ||
65 | + @Input() | ||
66 | + set required(value: boolean) { | ||
67 | + this.requiredValue = coerceBooleanProperty(value); | ||
68 | + } | ||
69 | + | ||
70 | + @Input() | ||
71 | + disabled: boolean; | ||
72 | + | ||
73 | + private propagateChange = (v: any) => { }; | ||
74 | + | ||
75 | + constructor(private store: Store<AppState>, | ||
76 | + private fb: FormBuilder) { | ||
77 | + } | ||
78 | + | ||
79 | + registerOnChange(fn: any): void { | ||
80 | + this.propagateChange = fn; | ||
81 | + } | ||
82 | + | ||
83 | + registerOnTouched(fn: any): void { | ||
84 | + } | ||
85 | + | ||
86 | + ngOnInit() { | ||
87 | + this.mobileActionFormGroup = this.fb.group({ | ||
88 | + type: [null, Validators.required], | ||
89 | + handleEmptyResultFunction: [null], | ||
90 | + handleErrorFunction: [null] | ||
91 | + }); | ||
92 | + this.mobileActionFormGroup.get('type').valueChanges.subscribe((type: WidgetMobileActionType) => { | ||
93 | + let action: WidgetMobileActionDescriptor = this.mobileActionFormGroup.value; | ||
94 | + if (this.mobileActionTypeFormGroup) { | ||
95 | + action = {...action, ...this.mobileActionTypeFormGroup.value}; | ||
96 | + } | ||
97 | + this.updateMobileActionType(type, action); | ||
98 | + }); | ||
99 | + this.mobileActionFormGroup.valueChanges.subscribe(() => { | ||
100 | + this.updateModel(); | ||
101 | + }); | ||
102 | + } | ||
103 | + | ||
104 | + setDisabledState(isDisabled: boolean): void { | ||
105 | + this.disabled = isDisabled; | ||
106 | + if (this.disabled) { | ||
107 | + this.mobileActionFormGroup.disable({emitEvent: false}); | ||
108 | + if (this.mobileActionTypeFormGroup) { | ||
109 | + this.mobileActionTypeFormGroup.disable({emitEvent: false}); | ||
110 | + } | ||
111 | + } else { | ||
112 | + this.mobileActionFormGroup.enable({emitEvent: false}); | ||
113 | + if (this.mobileActionTypeFormGroup) { | ||
114 | + this.mobileActionTypeFormGroup.enable({emitEvent: false}); | ||
115 | + } | ||
116 | + } | ||
117 | + } | ||
118 | + | ||
119 | + writeValue(value: WidgetMobileActionDescriptor | null): void { | ||
120 | + this.mobileActionFormGroup.patchValue({type: value?.type, | ||
121 | + handleEmptyResultFunction: value?.handleEmptyResultFunction, | ||
122 | + handleErrorFunction: value?.handleErrorFunction}, {emitEvent: false}); | ||
123 | + this.updateMobileActionType(value?.type, value); | ||
124 | + } | ||
125 | + | ||
126 | + private updateModel() { | ||
127 | + let descriptor: WidgetMobileActionDescriptor = null; | ||
128 | + if (this.mobileActionFormGroup.valid && this.mobileActionTypeFormGroup.valid) { | ||
129 | + descriptor = { ...this.mobileActionFormGroup.getRawValue(), ...this.mobileActionTypeFormGroup.getRawValue() }; | ||
130 | + } | ||
131 | + this.propagateChange(descriptor); | ||
132 | + } | ||
133 | + | ||
134 | + private updateMobileActionType(type?: WidgetMobileActionType, action?: WidgetMobileActionDescriptor) { | ||
135 | + const prevType = action?.type; | ||
136 | + const targetType = type || prevType; | ||
137 | + const changed = prevType !== type; | ||
138 | + if (changed && targetType) { | ||
139 | + let handleEmptyResultFunction = action?.handleEmptyResultFunction; | ||
140 | + const defaultHandleEmptyResultFunction = getDefaultHandleEmptyResultFunction(targetType); | ||
141 | + if (defaultHandleEmptyResultFunction !== handleEmptyResultFunction) { | ||
142 | + handleEmptyResultFunction = getDefaultHandleEmptyResultFunction(type); | ||
143 | + this.mobileActionFormGroup.patchValue({handleEmptyResultFunction}, {emitEvent: false}); | ||
144 | + } | ||
145 | + let handleErrorFunction = action?.handleErrorFunction; | ||
146 | + const defaultHandleErrorFunction = getDefaultHandleErrorFunction(targetType); | ||
147 | + if (defaultHandleErrorFunction !== handleErrorFunction) { | ||
148 | + handleErrorFunction = getDefaultHandleErrorFunction(type); | ||
149 | + this.mobileActionFormGroup.patchValue({handleErrorFunction}, {emitEvent: false}); | ||
150 | + } | ||
151 | + } | ||
152 | + this.mobileActionTypeFormGroup = this.fb.group({}); | ||
153 | + if (type) { | ||
154 | + switch (type) { | ||
155 | + case WidgetMobileActionType.takePictureFromGallery: | ||
156 | + case WidgetMobileActionType.takePhoto: | ||
157 | + case WidgetMobileActionType.takeScreenshot: | ||
158 | + let processImageFunction = action?.processImageFunction; | ||
159 | + if (changed) { | ||
160 | + const defaultProcessImageFunction = getDefaultProcessImageFunction(targetType); | ||
161 | + if (defaultProcessImageFunction !== processImageFunction) { | ||
162 | + processImageFunction = getDefaultProcessImageFunction(type); | ||
163 | + } | ||
164 | + } | ||
165 | + this.mobileActionTypeFormGroup.addControl( | ||
166 | + 'processImageFunction', | ||
167 | + this.fb.control(processImageFunction, []) | ||
168 | + ); | ||
169 | + break; | ||
170 | + case WidgetMobileActionType.mapDirection: | ||
171 | + case WidgetMobileActionType.mapLocation: | ||
172 | + let getLocationFunction = action?.getLocationFunction; | ||
173 | + let processLaunchResultFunction = action?.processLaunchResultFunction; | ||
174 | + if (changed) { | ||
175 | + const defaultGetLocationFunction = getDefaultGetLocationFunction(); | ||
176 | + if (defaultGetLocationFunction !== getLocationFunction) { | ||
177 | + getLocationFunction = defaultGetLocationFunction; | ||
178 | + } | ||
179 | + const defaultProcessLaunchResultFunction = getDefaultProcessLaunchResultFunction(targetType); | ||
180 | + if (defaultProcessLaunchResultFunction !== processLaunchResultFunction) { | ||
181 | + processLaunchResultFunction = getDefaultProcessLaunchResultFunction(type); | ||
182 | + } | ||
183 | + } | ||
184 | + this.mobileActionTypeFormGroup.addControl( | ||
185 | + 'getLocationFunction', | ||
186 | + this.fb.control(getLocationFunction, [Validators.required]) | ||
187 | + ); | ||
188 | + this.mobileActionTypeFormGroup.addControl( | ||
189 | + 'processLaunchResultFunction', | ||
190 | + this.fb.control(processLaunchResultFunction, []) | ||
191 | + ); | ||
192 | + break; | ||
193 | + case WidgetMobileActionType.scanQrCode: | ||
194 | + let processQrCodeFunction = action?.processQrCodeFunction; | ||
195 | + if (changed) { | ||
196 | + const defaultProcessQrCodeFunction = getDefaultProcessQrCodeFunction(); | ||
197 | + if (defaultProcessQrCodeFunction !== processQrCodeFunction) { | ||
198 | + processQrCodeFunction = defaultProcessQrCodeFunction; | ||
199 | + } | ||
200 | + } | ||
201 | + this.mobileActionTypeFormGroup.addControl( | ||
202 | + 'processQrCodeFunction', | ||
203 | + this.fb.control(processQrCodeFunction, []) | ||
204 | + ); | ||
205 | + break; | ||
206 | + case WidgetMobileActionType.makePhoneCall: | ||
207 | + let getPhoneNumberFunction = action?.getPhoneNumberFunction; | ||
208 | + processLaunchResultFunction = action?.processLaunchResultFunction; | ||
209 | + if (changed) { | ||
210 | + const defaultGetPhoneNumberFunction = getDefaultGetPhoneNumberFunction(); | ||
211 | + if (defaultGetPhoneNumberFunction !== getPhoneNumberFunction) { | ||
212 | + getPhoneNumberFunction = defaultGetPhoneNumberFunction; | ||
213 | + } | ||
214 | + const defaultProcessLaunchResultFunction = getDefaultProcessLaunchResultFunction(targetType); | ||
215 | + if (defaultProcessLaunchResultFunction !== processLaunchResultFunction) { | ||
216 | + processLaunchResultFunction = getDefaultProcessLaunchResultFunction(type); | ||
217 | + } | ||
218 | + } | ||
219 | + this.mobileActionTypeFormGroup.addControl( | ||
220 | + 'getPhoneNumberFunction', | ||
221 | + this.fb.control(getPhoneNumberFunction, [Validators.required]) | ||
222 | + ); | ||
223 | + this.mobileActionTypeFormGroup.addControl( | ||
224 | + 'processLaunchResultFunction', | ||
225 | + this.fb.control(processLaunchResultFunction, []) | ||
226 | + ); | ||
227 | + break; | ||
228 | + case WidgetMobileActionType.getLocation: | ||
229 | + let processLocationFunction = action?.processLocationFunction; | ||
230 | + if (changed) { | ||
231 | + const defaultProcessLocationFunction = getDefaultProcessLocationFunction(); | ||
232 | + if (defaultProcessLocationFunction !== processLocationFunction) { | ||
233 | + processLocationFunction = defaultProcessLocationFunction; | ||
234 | + } | ||
235 | + } | ||
236 | + this.mobileActionTypeFormGroup.addControl( | ||
237 | + 'processLocationFunction', | ||
238 | + this.fb.control(processLocationFunction, [Validators.required]) | ||
239 | + ); | ||
240 | + break; | ||
241 | + } | ||
242 | + } | ||
243 | + this.mobileActionTypeFormGroup.valueChanges.subscribe(() => { | ||
244 | + this.updateModel(); | ||
245 | + }); | ||
246 | + } | ||
247 | + | ||
248 | + public validateOnSubmit() { | ||
249 | + for (const jsFuncComponent of this.jsFuncComponents.toArray()) { | ||
250 | + jsFuncComponent.validateOnSubmit(); | ||
251 | + } | ||
252 | + } | ||
253 | +} |
1 | +/// | ||
2 | +/// Copyright © 2016-2021 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 { WidgetMobileActionType } from '@shared/models/widget.models'; | ||
18 | + | ||
19 | +const processImageFunctionTemplate = | ||
20 | + '// Function body to process image obtained as a result of mobile action (take photo, take image from gallery, etc.). \n' + | ||
21 | + '// - imageUrl - image URL in base64 data format\n\n' + | ||
22 | + 'showImageDialog(\'--TITLE--\', imageUrl);\n' + | ||
23 | + '//saveEntityImageAttribute(\'image\', imageUrl);\n' + | ||
24 | + '\n' + | ||
25 | + 'function showImageDialog(title, imageUrl) {\n' + | ||
26 | + ' setTimeout(function() {\n' + | ||
27 | + ' widgetContext.customDialog.customDialog(imageDialogTemplate, ImageDialogController, {imageUrl: imageUrl, title: title}).subscribe();\n' + | ||
28 | + ' }, 100);\n' + | ||
29 | + '}\n' + | ||
30 | + '\n' + | ||
31 | + 'function saveEntityImageAttribute(attributeName, imageUrl) {\n' + | ||
32 | + ' if (entityId) {\n' + | ||
33 | + ' let attributes = [{\n' + | ||
34 | + ' key: attributeName, value: imageUrl\n' + | ||
35 | + ' }];\n' + | ||
36 | + ' widgetContext.attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributes).subscribe(\n' + | ||
37 | + ' function() {\n' + | ||
38 | + ' widgetContext.showSuccessToast(\'Image attribute saved!\');\n' + | ||
39 | + ' },\n' + | ||
40 | + ' function(error) {\n' + | ||
41 | + ' widgetContext.dialogs.alert(\'Image attribute save failed\', JSON.stringify(error));\n' + | ||
42 | + ' }\n' + | ||
43 | + ' );\n' + | ||
44 | + ' }\n' + | ||
45 | + '}\n' + | ||
46 | + '\n' + | ||
47 | + 'var\n' + | ||
48 | + ' imageDialogTemplate =\n' + | ||
49 | + ' \'<div aria-label="Image">\' +\n' + | ||
50 | + ' \'<form #theForm="ngForm">\' +\n' + | ||
51 | + ' \'<mat-toolbar fxLayout="row" color="primary">\' +\n' + | ||
52 | + ' \'<h2>{{title}}</h2>\' +\n' + | ||
53 | + ' \'<span fxFlex></span>\' +\n' + | ||
54 | + ' \'<button mat-icon-button (click)="close()">\' +\n' + | ||
55 | + ' \'<mat-icon>close</mat-icon>\' +\n' + | ||
56 | + ' \'</button>\' +\n' + | ||
57 | + ' \'</mat-toolbar>\' +\n' + | ||
58 | + ' \'<div mat-dialog-content>\' +\n' + | ||
59 | + ' \'<div class="mat-content mat-padding">\' +\n' + | ||
60 | + ' \'<div fxLayout="column" fxFlex>\' +\n' + | ||
61 | + ' \'<div style="padding-top: 20px;">\' +\n' + | ||
62 | + ' \'<img [src]="imageUrl" style="height: 300px;"/>\' +\n' + | ||
63 | + ' \'</div>\' +\n' + | ||
64 | + ' \'</div>\' +\n' + | ||
65 | + ' \'</div>\' +\n' + | ||
66 | + ' \'</div>\' +\n' + | ||
67 | + ' \'<div mat-dialog-actions fxLayout="row">\' +\n' + | ||
68 | + ' \'<span fxFlex></span>\' +\n' + | ||
69 | + ' \'<button mat-button (click)="close()" style="margin-right:20px;">Close</button>\' +\n' + | ||
70 | + ' \'</div>\' +\n' + | ||
71 | + ' \'</form>\' +\n' + | ||
72 | + ' \'</div>\';\n' + | ||
73 | + '\n' + | ||
74 | + 'function ImageDialogController(instance) {\n' + | ||
75 | + ' let vm = instance;\n' + | ||
76 | + ' vm.title = vm.data.title;\n' + | ||
77 | + ' vm.imageUrl = vm.data.imageUrl;\n' + | ||
78 | + ' vm.close = function ()\n' + | ||
79 | + ' {\n' + | ||
80 | + ' vm.dialogRef.close(null);\n' + | ||
81 | + ' }\n' + | ||
82 | + '}\n'; | ||
83 | + | ||
84 | +const processLaunchResultFunctionTemplate = | ||
85 | + '// Optional function body to process result of attempt to launch external mobile application (for ex. map application or phone call application). \n' + | ||
86 | + '// - launched - boolean value indicating if the external application was successfully launched.\n\n' + | ||
87 | + 'showLaunchStatusDialog(\'--TITLE--\', launched);\n' + | ||
88 | + '\n' + | ||
89 | + 'function showLaunchStatusDialog(title, status) {\n' + | ||
90 | + ' setTimeout(function() {\n' + | ||
91 | + ' widgetContext.dialogs.alert(title, status ? \'Successfully launched\' : \'Failed to launch\').subscribe();\n' + | ||
92 | + ' }, 100);\n' + | ||
93 | + '}\n'; | ||
94 | + | ||
95 | +const processQrCodeFunction = | ||
96 | + '// Function body to process result of QR code scanning. \n' + | ||
97 | + '// - code - scanned QR code\n' + | ||
98 | + '// - format - scanned QR code format\n\n' + | ||
99 | + 'showQrCodeDialog(\'QR Code\', code, format);\n' + | ||
100 | + '\n' + | ||
101 | + 'function showQrCodeDialog(title, code, format) {\n' + | ||
102 | + ' setTimeout(function() {\n' + | ||
103 | + ' widgetContext.dialogs.alert(title, \'Code: [\'+code+\']<br>Format: \' + format).subscribe();\n' + | ||
104 | + ' }, 100);\n' + | ||
105 | + '}\n'; | ||
106 | + | ||
107 | +const processLocationFunction = | ||
108 | + '// Function body to process current location of the phone. \n' + | ||
109 | + '// - latitude - phone location latitude\n' + | ||
110 | + '// - longitude - phone location longitude\n\n' + | ||
111 | + 'showLocationDialog(\'Location\', latitude, longitude);\n' + | ||
112 | + '// saveEntityLocationAttributes(\'latitude\', \'longitude\', latitude, longitude);\n' + | ||
113 | + '\n' + | ||
114 | + 'function showImageDialog(title, imageUrl) {\n' + | ||
115 | + ' setTimeout(function() {\n' + | ||
116 | + ' widgetContext.customDialog.customDialog(imageDialogTemplate, ImageDialogController, {imageUrl: imageUrl, title: title}).subscribe();\n' + | ||
117 | + ' }, 100);\n' + | ||
118 | + '}\n' + | ||
119 | + '\n' + | ||
120 | + 'function saveEntityLocationAttributes(latitudeAttributeName, longitudeAttributeName, latitude, longitude) {\n' + | ||
121 | + ' if (entityId) {\n' + | ||
122 | + ' let attributes = [\n' + | ||
123 | + ' { key: latitudeAttributeName, value: latitude },\n' + | ||
124 | + ' { key: longitudeAttributeName, value: longitude }\n' + | ||
125 | + ' ];\n' + | ||
126 | + ' widgetContext.attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributes).subscribe(\n' + | ||
127 | + ' function() {\n' + | ||
128 | + ' widgetContext.showSuccessToast(\'Location attributes saved!\');\n' + | ||
129 | + ' },\n' + | ||
130 | + ' function(error) {\n' + | ||
131 | + ' widgetContext.dialogs.alert(\'Location attributes save failed\', JSON.stringify(error));\n' + | ||
132 | + ' }\n' + | ||
133 | + ' );\n' + | ||
134 | + ' }\n' + | ||
135 | + '}\n' + | ||
136 | + '\n' + | ||
137 | + '\n' + | ||
138 | + 'function showLocationDialog(title, latitude, longitude) {\n' + | ||
139 | + ' setTimeout(function() {\n' + | ||
140 | + ' widgetContext.dialogs.alert(title, \'Latitude: \'+latitude+\'<br>Longitude: \' + longitude).subscribe();\n' + | ||
141 | + ' }, 100);\n' + | ||
142 | + '}'; | ||
143 | + | ||
144 | +const handleEmptyResultFunctionTemplate = | ||
145 | + '// Optional function body to handle empty result. \n' + | ||
146 | + '// Usually this happens when user cancels the action (for ex. by pressing phone back button). \n\n' + | ||
147 | + 'showEmptyResultDialog(\'--MESSAGE--\');\n' + | ||
148 | + '\n' + | ||
149 | + 'function showEmptyResultDialog(message) {\n' + | ||
150 | + ' setTimeout(function() {\n' + | ||
151 | + ' widgetContext.dialogs.alert(\'Empty result\', message).subscribe();\n' + | ||
152 | + ' }, 100);\n' + | ||
153 | + '}\n'; | ||
154 | + | ||
155 | +const handleErrorFunctionTemplate = | ||
156 | + '// Optional function body to handle error occurred while mobile action execution \n' + | ||
157 | + '// - error - Error message\n\n' + | ||
158 | + 'showErrorDialog(\'--TITLE--\', error);\n' + | ||
159 | + '\n' + | ||
160 | + 'function showErrorDialog(title, error) {\n' + | ||
161 | + ' setTimeout(function() {\n' + | ||
162 | + ' widgetContext.dialogs.alert(title, error).subscribe();\n' + | ||
163 | + ' }, 100);\n' + | ||
164 | + '}\n'; | ||
165 | + | ||
166 | +const getLocationFunctionTemplate = | ||
167 | + '// Function body that should return location as array of two numbers (latitude, longitude) for further processing by mobile action.\n' + | ||
168 | + '// Usually location can be obtained from entity attributes/telemetry. \n\n' + | ||
169 | + 'return getLocationFromEntityAttributes();\n' + | ||
170 | + '//return [30, 30]; // TEST LOCATION\n' + | ||
171 | + '\n' + | ||
172 | + '\n' + | ||
173 | + 'function getLocationFromEntityAttributes() {\n' + | ||
174 | + ' if (entityId) {\n' + | ||
175 | + ' return widgetContext.attributeService.getEntityAttributes(entityId, \'SERVER_SCOPE\', [\'latitude\', \'longitude\']).pipe(widgetContext.rxjs.map(function(attributeData) {\n' + | ||
176 | + ' var res = [0,0];\n' + | ||
177 | + ' if (attributeData && attributeData.length === 2) {\n' + | ||
178 | + ' res[0] = attributeData.filter(function (data) { return data.key === \'latitude\'})[0].value;\n' + | ||
179 | + ' res[1] = attributeData.filter(function (data) { return data.key === \'longitude\'})[0].value;\n' + | ||
180 | + ' }\n' + | ||
181 | + ' return res;\n' + | ||
182 | + ' }));\n' + | ||
183 | + ' } else {\n' + | ||
184 | + ' return [0,0];\n' + | ||
185 | + ' }\n' + | ||
186 | + '}\n'; | ||
187 | + | ||
188 | +const getPhoneNumberFunctionTemplate = | ||
189 | + '// Function body that should return phone number for further processing by mobile action.\n' + | ||
190 | + '// Usually phone number can be obtained from entity attributes/telemetry. \n\n' + | ||
191 | + 'return getPhoneNumberFromEntityAttributes();\n' + | ||
192 | + '//return 123456789; // TEST PHONE NUMBER\n' + | ||
193 | + '\n' + | ||
194 | + '\n' + | ||
195 | + 'function getPhoneNumberFromEntityAttributes() {\n' + | ||
196 | + ' if (entityId) {\n' + | ||
197 | + ' return widgetContext.attributeService.getEntityAttributes(entityId, \'SERVER_SCOPE\', [\'phone\']).pipe(widgetContext.rxjs.map(function(attributeData) {\n' + | ||
198 | + ' var res = 0;\n' + | ||
199 | + ' if (attributeData && attributeData.length === 1) {\n' + | ||
200 | + ' res = attributeData[0].value;\n' + | ||
201 | + ' }\n' + | ||
202 | + ' return res;\n' + | ||
203 | + ' }));\n' + | ||
204 | + ' } else {\n' + | ||
205 | + ' return 0;\n' + | ||
206 | + ' }\n' + | ||
207 | + '}\n'; | ||
208 | + | ||
209 | +export function getDefaultProcessImageFunction(type: WidgetMobileActionType): string { | ||
210 | + let title; | ||
211 | + switch (type) { | ||
212 | + case WidgetMobileActionType.takePictureFromGallery: | ||
213 | + title = 'Gallery picture'; | ||
214 | + break; | ||
215 | + case WidgetMobileActionType.takePhoto: | ||
216 | + title = 'Photo'; | ||
217 | + break; | ||
218 | + case WidgetMobileActionType.takeScreenshot: | ||
219 | + title = 'Screenshot'; | ||
220 | + break; | ||
221 | + } | ||
222 | + return processImageFunctionTemplate.replace('--TITLE--', title); | ||
223 | +} | ||
224 | + | ||
225 | +export function getDefaultProcessLaunchResultFunction(type: WidgetMobileActionType): string { | ||
226 | + let title; | ||
227 | + switch (type) { | ||
228 | + case WidgetMobileActionType.mapLocation: | ||
229 | + title = 'Map location'; | ||
230 | + break; | ||
231 | + case WidgetMobileActionType.mapDirection: | ||
232 | + title = 'Map direction'; | ||
233 | + break; | ||
234 | + case WidgetMobileActionType.makePhoneCall: | ||
235 | + title = 'Phone call'; | ||
236 | + break; | ||
237 | + } | ||
238 | + return processLaunchResultFunctionTemplate.replace('--TITLE--', title); | ||
239 | +} | ||
240 | + | ||
241 | +export function getDefaultProcessQrCodeFunction() { | ||
242 | + return processQrCodeFunction; | ||
243 | +} | ||
244 | + | ||
245 | +export function getDefaultProcessLocationFunction() { | ||
246 | + return processLocationFunction; | ||
247 | +} | ||
248 | + | ||
249 | +export function getDefaultGetLocationFunction() { | ||
250 | + return getLocationFunctionTemplate; | ||
251 | +} | ||
252 | + | ||
253 | +export function getDefaultGetPhoneNumberFunction() { | ||
254 | + return getPhoneNumberFunctionTemplate; | ||
255 | +} | ||
256 | + | ||
257 | +export function getDefaultHandleEmptyResultFunction(type: WidgetMobileActionType): string { | ||
258 | + let message = 'Mobile action was cancelled!'; | ||
259 | + switch (type) { | ||
260 | + case WidgetMobileActionType.takePictureFromGallery: | ||
261 | + message = 'Take picture from gallery action was cancelled!'; | ||
262 | + break; | ||
263 | + case WidgetMobileActionType.takePhoto: | ||
264 | + message = 'Take photo action was cancelled!'; | ||
265 | + break; | ||
266 | + case WidgetMobileActionType.mapDirection: | ||
267 | + message = 'Open map directions was not invoked!'; | ||
268 | + break; | ||
269 | + case WidgetMobileActionType.mapLocation: | ||
270 | + message = 'Open location on map was not invoked!'; | ||
271 | + break; | ||
272 | + case WidgetMobileActionType.scanQrCode: | ||
273 | + message = 'Scan QR code action was canceled!'; | ||
274 | + break; | ||
275 | + case WidgetMobileActionType.makePhoneCall: | ||
276 | + message = 'Phone call was not invoked!'; | ||
277 | + break; | ||
278 | + case WidgetMobileActionType.getLocation: | ||
279 | + message = 'Get location action was canceled!'; | ||
280 | + break; | ||
281 | + case WidgetMobileActionType.takeScreenshot: | ||
282 | + message = 'Take screenshot action was cancelled!'; | ||
283 | + break; | ||
284 | + } | ||
285 | + return handleEmptyResultFunctionTemplate.replace('--MESSAGE--', message); | ||
286 | +} | ||
287 | + | ||
288 | +export function getDefaultHandleErrorFunction(type: WidgetMobileActionType): string { | ||
289 | + let title = 'Mobile action failed'; | ||
290 | + switch (type) { | ||
291 | + case WidgetMobileActionType.takePictureFromGallery: | ||
292 | + title = 'Failed to take picture from gallery'; | ||
293 | + break; | ||
294 | + case WidgetMobileActionType.takePhoto: | ||
295 | + title = 'Failed to take photo'; | ||
296 | + break; | ||
297 | + case WidgetMobileActionType.mapDirection: | ||
298 | + title = 'Failed to open map directions'; | ||
299 | + break; | ||
300 | + case WidgetMobileActionType.mapLocation: | ||
301 | + title = 'Failed to open map location'; | ||
302 | + break; | ||
303 | + case WidgetMobileActionType.scanQrCode: | ||
304 | + title = 'Failed to scan QR code'; | ||
305 | + break; | ||
306 | + case WidgetMobileActionType.makePhoneCall: | ||
307 | + title = 'Failed to make phone call'; | ||
308 | + break; | ||
309 | + case WidgetMobileActionType.getLocation: | ||
310 | + title = 'Failed to get phone location'; | ||
311 | + break; | ||
312 | + case WidgetMobileActionType.takeScreenshot: | ||
313 | + title = 'Failed to take screenshot'; | ||
314 | + break; | ||
315 | + } | ||
316 | + return handleErrorFunctionTemplate.replace('--TITLE--', title); | ||
317 | +} |
@@ -180,6 +180,11 @@ | @@ -180,6 +180,11 @@ | ||
180 | formControlName="customAction"> | 180 | formControlName="customAction"> |
181 | </tb-custom-action-pretty-editor> | 181 | </tb-custom-action-pretty-editor> |
182 | </ng-template> | 182 | </ng-template> |
183 | + <ng-template [ngSwitchCase]="widgetActionType.mobileAction"> | ||
184 | + <tb-mobile-action-editor #mobileActionEditor | ||
185 | + formControlName="mobileAction"> | ||
186 | + </tb-mobile-action-editor> | ||
187 | + </ng-template> | ||
183 | </section> | 188 | </section> |
184 | </fieldset> | 189 | </fieldset> |
185 | </div> | 190 | </div> |
@@ -45,6 +45,7 @@ import { Dashboard } from '@shared/models/dashboard.models'; | @@ -45,6 +45,7 @@ import { Dashboard } from '@shared/models/dashboard.models'; | ||
45 | import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; | 45 | import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; |
46 | import { CustomActionEditorCompleter } from '@home/components/widget/action/custom-action.models'; | 46 | import { CustomActionEditorCompleter } from '@home/components/widget/action/custom-action.models'; |
47 | import { isDefinedAndNotNull } from '@core/utils'; | 47 | import { isDefinedAndNotNull } from '@core/utils'; |
48 | +import { MobileActionEditorComponent } from '@home/components/widget/action/mobile-action-editor.component'; | ||
48 | 49 | ||
49 | export interface WidgetActionDialogData { | 50 | export interface WidgetActionDialogData { |
50 | isAdd: boolean; | 51 | isAdd: boolean; |
@@ -64,6 +65,8 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia | @@ -64,6 +65,8 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia | ||
64 | 65 | ||
65 | @ViewChild('dashboardStateInput') dashboardStateInput: ElementRef; | 66 | @ViewChild('dashboardStateInput') dashboardStateInput: ElementRef; |
66 | 67 | ||
68 | + @ViewChild('mobileActionEditor', {static: false}) mobileActionEditor: MobileActionEditorComponent; | ||
69 | + | ||
67 | widgetActionFormGroup: FormGroup; | 70 | widgetActionFormGroup: FormGroup; |
68 | actionTypeFormGroup: FormGroup; | 71 | actionTypeFormGroup: FormGroup; |
69 | 72 | ||
@@ -197,6 +200,12 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia | @@ -197,6 +200,12 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia | ||
197 | this.fb.control(toCustomAction(action), [Validators.required]) | 200 | this.fb.control(toCustomAction(action), [Validators.required]) |
198 | ); | 201 | ); |
199 | break; | 202 | break; |
203 | + case WidgetActionType.mobileAction: | ||
204 | + this.actionTypeFormGroup.addControl( | ||
205 | + 'mobileAction', | ||
206 | + this.fb.control(action ? action.mobileAction : null, [Validators.required]) | ||
207 | + ); | ||
208 | + break; | ||
200 | } | 209 | } |
201 | } | 210 | } |
202 | } | 211 | } |
@@ -313,14 +322,19 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia | @@ -313,14 +322,19 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia | ||
313 | 322 | ||
314 | save(): void { | 323 | save(): void { |
315 | this.submitted = true; | 324 | this.submitted = true; |
316 | - const type: WidgetActionType = this.widgetActionFormGroup.get('type').value; | ||
317 | - let result: WidgetActionDescriptorInfo; | ||
318 | - if (type === WidgetActionType.customPretty) { | ||
319 | - result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.get('customAction').value}; | ||
320 | - } else { | ||
321 | - result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.value}; | 325 | + if (this.mobileActionEditor != null) { |
326 | + this.mobileActionEditor.validateOnSubmit(); | ||
327 | + } | ||
328 | + if (this.widgetActionFormGroup.valid && this.actionTypeFormGroup.valid) { | ||
329 | + const type: WidgetActionType = this.widgetActionFormGroup.get('type').value; | ||
330 | + let result: WidgetActionDescriptorInfo; | ||
331 | + if (type === WidgetActionType.customPretty) { | ||
332 | + result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.get('customAction').value}; | ||
333 | + } else { | ||
334 | + result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.value}; | ||
335 | + } | ||
336 | + result.id = this.action.id; | ||
337 | + this.dialogRef.close(result); | ||
322 | } | 338 | } |
323 | - result.id = this.action.id; | ||
324 | - this.dialogRef.close(result); | ||
325 | } | 339 | } |
326 | } | 340 | } |
@@ -39,12 +39,12 @@ import { | @@ -39,12 +39,12 @@ import { | ||
39 | defaultLegendConfig, | 39 | defaultLegendConfig, |
40 | LegendConfig, | 40 | LegendConfig, |
41 | LegendData, | 41 | LegendData, |
42 | - LegendPosition, | 42 | + LegendPosition, MobileActionResult, |
43 | Widget, | 43 | Widget, |
44 | WidgetActionDescriptor, | 44 | WidgetActionDescriptor, |
45 | widgetActionSources, | 45 | widgetActionSources, |
46 | WidgetActionType, | 46 | WidgetActionType, |
47 | - WidgetComparisonSettings, | 47 | + WidgetComparisonSettings, WidgetMobileActionDescriptor, WidgetMobileActionType, |
48 | WidgetResource, | 48 | WidgetResource, |
49 | widgetType, | 49 | widgetType, |
50 | WidgetTypeParameters | 50 | WidgetTypeParameters |
@@ -77,7 +77,7 @@ import { EntityId } from '@shared/models/id/entity-id'; | @@ -77,7 +77,7 @@ import { EntityId } from '@shared/models/id/entity-id'; | ||
77 | import { ActivatedRoute, Router } from '@angular/router'; | 77 | import { ActivatedRoute, Router } from '@angular/router'; |
78 | import cssjs from '@core/css/css'; | 78 | import cssjs from '@core/css/css'; |
79 | import { ResourcesService } from '@core/services/resources.service'; | 79 | import { ResourcesService } from '@core/services/resources.service'; |
80 | -import { catchError, switchMap } from 'rxjs/operators'; | 80 | +import { catchError, map, switchMap } from 'rxjs/operators'; |
81 | import { ActionNotificationShow } from '@core/notification/notification.actions'; | 81 | import { ActionNotificationShow } from '@core/notification/notification.actions'; |
82 | import { TimeService } from '@core/services/time.service'; | 82 | import { TimeService } from '@core/services/time.service'; |
83 | import { DeviceService } from '@app/core/http/device.service'; | 83 | import { DeviceService } from '@app/core/http/device.service'; |
@@ -97,6 +97,8 @@ import { AlarmDataService } from '@core/api/alarm-data.service'; | @@ -97,6 +97,8 @@ import { AlarmDataService } from '@core/api/alarm-data.service'; | ||
97 | import { MatDialog } from '@angular/material/dialog'; | 97 | import { MatDialog } from '@angular/material/dialog'; |
98 | import { ComponentType } from '@angular/cdk/portal'; | 98 | import { ComponentType } from '@angular/cdk/portal'; |
99 | import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token'; | 99 | import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token'; |
100 | +import { MobileService } from '@core/services/mobile.service'; | ||
101 | +import { DialogService } from '@core/services/dialog.service'; | ||
100 | 102 | ||
101 | @Component({ | 103 | @Component({ |
102 | selector: 'tb-widget', | 104 | selector: 'tb-widget', |
@@ -177,6 +179,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -177,6 +179,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
177 | private alarmDataService: AlarmDataService, | 179 | private alarmDataService: AlarmDataService, |
178 | private translate: TranslateService, | 180 | private translate: TranslateService, |
179 | private utils: UtilsService, | 181 | private utils: UtilsService, |
182 | + private mobileService: MobileService, | ||
183 | + private dialogs: DialogService, | ||
180 | private raf: RafService, | 184 | private raf: RafService, |
181 | private ngZone: NgZone, | 185 | private ngZone: NgZone, |
182 | private cd: ChangeDetectorRef) { | 186 | private cd: ChangeDetectorRef) { |
@@ -1094,6 +1098,178 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -1094,6 +1098,178 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
1094 | } | 1098 | } |
1095 | ); | 1099 | ); |
1096 | break; | 1100 | break; |
1101 | + case WidgetActionType.mobileAction: | ||
1102 | + const mobileAction = descriptor.mobileAction; | ||
1103 | + this.handleMobileAction($event, mobileAction, entityId, entityName, additionalParams, entityLabel); | ||
1104 | + break; | ||
1105 | + } | ||
1106 | + } | ||
1107 | + | ||
1108 | + private handleMobileAction($event: Event, mobileAction: WidgetMobileActionDescriptor, | ||
1109 | + entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) { | ||
1110 | + const type = mobileAction.type; | ||
1111 | + let argsObservable: Observable<any[]>; | ||
1112 | + switch (type) { | ||
1113 | + case WidgetMobileActionType.takePictureFromGallery: | ||
1114 | + case WidgetMobileActionType.takePhoto: | ||
1115 | + case WidgetMobileActionType.scanQrCode: | ||
1116 | + case WidgetMobileActionType.getLocation: | ||
1117 | + case WidgetMobileActionType.takeScreenshot: | ||
1118 | + argsObservable = of([]); | ||
1119 | + break; | ||
1120 | + case WidgetMobileActionType.mapDirection: | ||
1121 | + case WidgetMobileActionType.mapLocation: | ||
1122 | + const getLocationFunctionString = mobileAction.getLocationFunction; | ||
1123 | + const getLocationFunction = new Function('$event', 'widgetContext', 'entityId', | ||
1124 | + 'entityName', 'additionalParams', 'entityLabel', getLocationFunctionString); | ||
1125 | + const locationArgs = getLocationFunction($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); | ||
1126 | + if (locationArgs && locationArgs instanceof Observable) { | ||
1127 | + argsObservable = locationArgs; | ||
1128 | + } else { | ||
1129 | + argsObservable = of(locationArgs); | ||
1130 | + } | ||
1131 | + argsObservable = argsObservable.pipe(map(latLng => { | ||
1132 | + let valid = false; | ||
1133 | + if (Array.isArray(latLng) && latLng.length === 2) { | ||
1134 | + if (typeof latLng[0] === 'number' && typeof latLng[1] === 'number') { | ||
1135 | + valid = true; | ||
1136 | + } | ||
1137 | + } | ||
1138 | + if (valid) { | ||
1139 | + return latLng; | ||
1140 | + } else { | ||
1141 | + throw new Error('Location function did not return valid array of latitude/longitude!'); | ||
1142 | + } | ||
1143 | + })); | ||
1144 | + break; | ||
1145 | + case WidgetMobileActionType.makePhoneCall: | ||
1146 | + const getPhoneNumberFunctionString = mobileAction.getPhoneNumberFunction; | ||
1147 | + const getPhoneNumberFunction = new Function('$event', 'widgetContext', 'entityId', | ||
1148 | + 'entityName', 'additionalParams', 'entityLabel', getPhoneNumberFunctionString); | ||
1149 | + const phoneNumberArg = getPhoneNumberFunction($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); | ||
1150 | + if (phoneNumberArg && phoneNumberArg instanceof Observable) { | ||
1151 | + argsObservable = phoneNumberArg.pipe(map(phoneNumber => [phoneNumber])); | ||
1152 | + } else { | ||
1153 | + argsObservable = of([phoneNumberArg]); | ||
1154 | + } | ||
1155 | + argsObservable = argsObservable.pipe(map(phoneNumberArr => { | ||
1156 | + let valid = false; | ||
1157 | + if (Array.isArray(phoneNumberArr) && phoneNumberArr.length === 1) { | ||
1158 | + if (phoneNumberArr[0] !== null) { | ||
1159 | + valid = true; | ||
1160 | + } | ||
1161 | + } | ||
1162 | + if (valid) { | ||
1163 | + return phoneNumberArr; | ||
1164 | + } else { | ||
1165 | + throw new Error('Phone number function did not return valid number!'); | ||
1166 | + } | ||
1167 | + })); | ||
1168 | + break; | ||
1169 | + } | ||
1170 | + argsObservable.subscribe((args) => { | ||
1171 | + this.mobileService.handleWidgetMobileAction(type, ...args).subscribe( | ||
1172 | + (result) => { | ||
1173 | + if (result) { | ||
1174 | + if (result.hasError) { | ||
1175 | + this.handleWidgetMobileActionError(result.error, $event, mobileAction, entityId, entityName, additionalParams, entityLabel); | ||
1176 | + } else if (result.hasResult) { | ||
1177 | + const actionResult = result.result; | ||
1178 | + switch (type) { | ||
1179 | + case WidgetMobileActionType.takePictureFromGallery: | ||
1180 | + case WidgetMobileActionType.takePhoto: | ||
1181 | + case WidgetMobileActionType.takeScreenshot: | ||
1182 | + const imageUrl = actionResult.imageUrl; | ||
1183 | + if (mobileAction.processImageFunction && mobileAction.processImageFunction.length) { | ||
1184 | + try { | ||
1185 | + const processImageFunction = new Function('imageUrl', '$event', 'widgetContext', 'entityId', | ||
1186 | + 'entityName', 'additionalParams', 'entityLabel', mobileAction.processImageFunction); | ||
1187 | + processImageFunction(imageUrl, $event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); | ||
1188 | + } catch (e) { | ||
1189 | + console.error(e); | ||
1190 | + } | ||
1191 | + } | ||
1192 | + break; | ||
1193 | + case WidgetMobileActionType.scanQrCode: | ||
1194 | + const code = actionResult.code; | ||
1195 | + const format = actionResult.format; | ||
1196 | + if (mobileAction.processQrCodeFunction && mobileAction.processQrCodeFunction.length) { | ||
1197 | + try { | ||
1198 | + const processQrCodeFunction = new Function('code', 'format', '$event', 'widgetContext', 'entityId', | ||
1199 | + 'entityName', 'additionalParams', 'entityLabel', mobileAction.processQrCodeFunction); | ||
1200 | + processQrCodeFunction(code, format, $event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); | ||
1201 | + } catch (e) { | ||
1202 | + console.error(e); | ||
1203 | + } | ||
1204 | + } | ||
1205 | + break; | ||
1206 | + case WidgetMobileActionType.getLocation: | ||
1207 | + const latitude = actionResult.latitude; | ||
1208 | + const longitude = actionResult.longitude; | ||
1209 | + if (mobileAction.processLocationFunction && mobileAction.processLocationFunction.length) { | ||
1210 | + try { | ||
1211 | + const processLocationFunction = new Function('latitude', 'longitude', '$event', 'widgetContext', 'entityId', | ||
1212 | + 'entityName', 'additionalParams', 'entityLabel', mobileAction.processLocationFunction); | ||
1213 | + processLocationFunction(latitude, longitude, $event, this.widgetContext, | ||
1214 | + entityId, entityName, additionalParams, entityLabel); | ||
1215 | + } catch (e) { | ||
1216 | + console.error(e); | ||
1217 | + } | ||
1218 | + } | ||
1219 | + break; | ||
1220 | + case WidgetMobileActionType.mapDirection: | ||
1221 | + case WidgetMobileActionType.mapLocation: | ||
1222 | + case WidgetMobileActionType.makePhoneCall: | ||
1223 | + const launched = actionResult.launched; | ||
1224 | + if (mobileAction.processLaunchResultFunction && mobileAction.processLaunchResultFunction.length) { | ||
1225 | + try { | ||
1226 | + const processLaunchResultFunction = new Function('launched', '$event', 'widgetContext', 'entityId', | ||
1227 | + 'entityName', 'additionalParams', 'entityLabel', mobileAction.processLaunchResultFunction); | ||
1228 | + processLaunchResultFunction(launched, $event, this.widgetContext, | ||
1229 | + entityId, entityName, additionalParams, entityLabel); | ||
1230 | + } catch (e) { | ||
1231 | + console.error(e); | ||
1232 | + } | ||
1233 | + } | ||
1234 | + break; | ||
1235 | + } | ||
1236 | + } else { | ||
1237 | + if (mobileAction.handleEmptyResultFunction && mobileAction.handleEmptyResultFunction.length) { | ||
1238 | + try { | ||
1239 | + const handleEmptyResultFunction = new Function('$event', 'widgetContext', 'entityId', | ||
1240 | + 'entityName', 'additionalParams', 'entityLabel', mobileAction.handleEmptyResultFunction); | ||
1241 | + handleEmptyResultFunction($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); | ||
1242 | + } catch (e) { | ||
1243 | + console.error(e); | ||
1244 | + } | ||
1245 | + } | ||
1246 | + } | ||
1247 | + } | ||
1248 | + } | ||
1249 | + ); | ||
1250 | + }, | ||
1251 | + (err) => { | ||
1252 | + let errorMessage; | ||
1253 | + if (err && typeof err === 'string') { | ||
1254 | + errorMessage = err; | ||
1255 | + } else if (err && err.message) { | ||
1256 | + errorMessage = err.message; | ||
1257 | + } | ||
1258 | + errorMessage = `Failed to get mobile action arguments${errorMessage ? `: ${errorMessage}` : '!'}`; | ||
1259 | + this.handleWidgetMobileActionError(errorMessage, $event, mobileAction, entityId, entityName, additionalParams, entityLabel); | ||
1260 | + }); | ||
1261 | + } | ||
1262 | + | ||
1263 | + private handleWidgetMobileActionError(error: string, $event: Event, mobileAction: WidgetMobileActionDescriptor, | ||
1264 | + entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) { | ||
1265 | + if (mobileAction.handleErrorFunction && mobileAction.handleErrorFunction.length) { | ||
1266 | + try { | ||
1267 | + const handleErrorFunction = new Function('error', '$event', 'widgetContext', 'entityId', | ||
1268 | + 'entityName', 'additionalParams', 'entityLabel', mobileAction.handleErrorFunction); | ||
1269 | + handleErrorFunction(error, $event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); | ||
1270 | + } catch (e) { | ||
1271 | + console.error(e); | ||
1272 | + } | ||
1097 | } | 1273 | } |
1098 | } | 1274 | } |
1099 | 1275 |
@@ -77,6 +77,7 @@ import { PageLink } from '@shared/models/page/page-link'; | @@ -77,6 +77,7 @@ import { PageLink } from '@shared/models/page/page-link'; | ||
77 | import { SortOrder } from '@shared/models/page/sort-order'; | 77 | import { SortOrder } from '@shared/models/page/sort-order'; |
78 | import { DomSanitizer } from '@angular/platform-browser'; | 78 | import { DomSanitizer } from '@angular/platform-browser'; |
79 | import { Router } from '@angular/router'; | 79 | import { Router } from '@angular/router'; |
80 | +import { map, mergeMap } from 'rxjs/operators'; | ||
80 | 81 | ||
81 | export interface IWidgetAction { | 82 | export interface IWidgetAction { |
82 | name: string; | 83 | name: string; |
@@ -238,7 +239,9 @@ export class WidgetContext { | @@ -238,7 +239,9 @@ export class WidgetContext { | ||
238 | 239 | ||
239 | rxjs = { | 240 | rxjs = { |
240 | forkJoin, | 241 | forkJoin, |
241 | - of | 242 | + of, |
243 | + map, | ||
244 | + mergeMap | ||
242 | }; | 245 | }; |
243 | 246 | ||
244 | showSuccessToast(message: string, duration: number = 1000, | 247 | showSuccessToast(message: string, duration: number = 1000, |
@@ -332,7 +332,19 @@ export enum WidgetActionType { | @@ -332,7 +332,19 @@ export enum WidgetActionType { | ||
332 | updateDashboardState = 'updateDashboardState', | 332 | updateDashboardState = 'updateDashboardState', |
333 | openDashboard = 'openDashboard', | 333 | openDashboard = 'openDashboard', |
334 | custom = 'custom', | 334 | custom = 'custom', |
335 | - customPretty = 'customPretty' | 335 | + customPretty = 'customPretty', |
336 | + mobileAction = 'mobileAction' | ||
337 | +} | ||
338 | + | ||
339 | +export enum WidgetMobileActionType { | ||
340 | + takePictureFromGallery = 'takePictureFromGallery', | ||
341 | + takePhoto = 'takePhoto', | ||
342 | + mapDirection = 'mapDirection', | ||
343 | + mapLocation = 'mapLocation', | ||
344 | + scanQrCode = 'scanQrCode', | ||
345 | + makePhoneCall = 'makePhoneCall', | ||
346 | + getLocation = 'getLocation', | ||
347 | + takeScreenshot = 'takeScreenshot' | ||
336 | } | 348 | } |
337 | 349 | ||
338 | export const widgetActionTypeTranslationMap = new Map<WidgetActionType, string>( | 350 | export const widgetActionTypeTranslationMap = new Map<WidgetActionType, string>( |
@@ -341,10 +353,90 @@ export const widgetActionTypeTranslationMap = new Map<WidgetActionType, string>( | @@ -341,10 +353,90 @@ export const widgetActionTypeTranslationMap = new Map<WidgetActionType, string>( | ||
341 | [ WidgetActionType.updateDashboardState, 'widget-action.update-dashboard-state' ], | 353 | [ WidgetActionType.updateDashboardState, 'widget-action.update-dashboard-state' ], |
342 | [ WidgetActionType.openDashboard, 'widget-action.open-dashboard' ], | 354 | [ WidgetActionType.openDashboard, 'widget-action.open-dashboard' ], |
343 | [ WidgetActionType.custom, 'widget-action.custom' ], | 355 | [ WidgetActionType.custom, 'widget-action.custom' ], |
344 | - [ WidgetActionType.customPretty, 'widget-action.custom-pretty' ] | 356 | + [ WidgetActionType.customPretty, 'widget-action.custom-pretty' ], |
357 | + [ WidgetActionType.mobileAction, 'widget-action.mobile-action' ] | ||
358 | + ] | ||
359 | +); | ||
360 | + | ||
361 | +export const widgetMobileActionTypeTranslationMap = new Map<WidgetMobileActionType, string>( | ||
362 | + [ | ||
363 | + [ WidgetMobileActionType.takePictureFromGallery, 'widget-action.mobile.take-picture-from-gallery' ], | ||
364 | + [ WidgetMobileActionType.takePhoto, 'widget-action.mobile.take-photo' ], | ||
365 | + [ WidgetMobileActionType.mapDirection, 'widget-action.mobile.map-direction' ], | ||
366 | + [ WidgetMobileActionType.mapLocation, 'widget-action.mobile.map-location' ], | ||
367 | + [ WidgetMobileActionType.scanQrCode, 'widget-action.mobile.scan-qr-code' ], | ||
368 | + [ WidgetMobileActionType.makePhoneCall, 'widget-action.mobile.make-phone-call' ], | ||
369 | + [ WidgetMobileActionType.getLocation, 'widget-action.mobile.get-location' ], | ||
370 | + [ WidgetMobileActionType.takeScreenshot, 'widget-action.mobile.take-screenshot' ] | ||
345 | ] | 371 | ] |
346 | ); | 372 | ); |
347 | 373 | ||
374 | +export interface MobileLaunchResult { | ||
375 | + launched: boolean; | ||
376 | +} | ||
377 | + | ||
378 | +export interface MobileImageResult { | ||
379 | + imageUrl: string; | ||
380 | +} | ||
381 | + | ||
382 | +export interface MobileQrCodeResult { | ||
383 | + code: string; | ||
384 | + format: string; | ||
385 | +} | ||
386 | + | ||
387 | +export interface MobileLocationResult { | ||
388 | + latitude: number; | ||
389 | + longitude: number; | ||
390 | +} | ||
391 | + | ||
392 | +export type MobileActionResult = MobileLaunchResult & | ||
393 | + MobileImageResult & | ||
394 | + MobileQrCodeResult & | ||
395 | + MobileLocationResult; | ||
396 | + | ||
397 | +export interface WidgetMobileActionResult<T extends MobileActionResult> { | ||
398 | + result?: T; | ||
399 | + hasResult: boolean; | ||
400 | + error?: string; | ||
401 | + hasError: boolean; | ||
402 | +} | ||
403 | + | ||
404 | +export interface ProcessImageDescriptor { | ||
405 | + processImageFunction: string; | ||
406 | +} | ||
407 | + | ||
408 | +export interface ProcessLaunchResultDescriptor { | ||
409 | + processLaunchResultFunction?: string; | ||
410 | +} | ||
411 | + | ||
412 | +export interface LaunchMapDescriptor extends ProcessLaunchResultDescriptor { | ||
413 | + getLocationFunction: string; | ||
414 | +} | ||
415 | + | ||
416 | +export interface ScanQrCodeDescriptor { | ||
417 | + processQrCodeFunction: string; | ||
418 | +} | ||
419 | + | ||
420 | +export interface MakePhoneCallDescriptor extends ProcessLaunchResultDescriptor { | ||
421 | + getPhoneNumberFunction: string; | ||
422 | +} | ||
423 | + | ||
424 | +export interface GetLocationDescriptor { | ||
425 | + processLocationFunction: string; | ||
426 | +} | ||
427 | + | ||
428 | +export type WidgetMobileActionDescriptors = ProcessImageDescriptor & | ||
429 | + LaunchMapDescriptor & | ||
430 | + ScanQrCodeDescriptor & | ||
431 | + MakePhoneCallDescriptor & | ||
432 | + GetLocationDescriptor; | ||
433 | + | ||
434 | +export interface WidgetMobileActionDescriptor extends WidgetMobileActionDescriptors { | ||
435 | + type: WidgetMobileActionType; | ||
436 | + handleErrorFunction?: string; | ||
437 | + handleEmptyResultFunction?: string; | ||
438 | +} | ||
439 | + | ||
348 | export interface CustomActionDescriptor { | 440 | export interface CustomActionDescriptor { |
349 | customFunction?: string; | 441 | customFunction?: string; |
350 | customResources?: Array<WidgetResource>; | 442 | customResources?: Array<WidgetResource>; |
@@ -369,6 +461,7 @@ export interface WidgetActionDescriptor extends CustomActionDescriptor { | @@ -369,6 +461,7 @@ export interface WidgetActionDescriptor extends CustomActionDescriptor { | ||
369 | dialogHeight?: number; | 461 | dialogHeight?: number; |
370 | setEntityId?: boolean; | 462 | setEntityId?: boolean; |
371 | stateEntityParamName?: string; | 463 | stateEntityParamName?: string; |
464 | + mobileAction?: WidgetMobileActionDescriptor; | ||
372 | } | 465 | } |
373 | 466 | ||
374 | export interface WidgetComparisonSettings { | 467 | export interface WidgetComparisonSettings { |
@@ -2704,6 +2704,7 @@ | @@ -2704,6 +2704,7 @@ | ||
2704 | "open-dashboard": "Navigate to other dashboard", | 2704 | "open-dashboard": "Navigate to other dashboard", |
2705 | "custom": "Custom action", | 2705 | "custom": "Custom action", |
2706 | "custom-pretty": "Custom action (with HTML template)", | 2706 | "custom-pretty": "Custom action (with HTML template)", |
2707 | + "mobile-action": "Mobile action", | ||
2707 | "target-dashboard-state": "Target dashboard state", | 2708 | "target-dashboard-state": "Target dashboard state", |
2708 | "target-dashboard-state-required": "Target dashboard state is required", | 2709 | "target-dashboard-state-required": "Target dashboard state is required", |
2709 | "set-entity-from-widget": "Set entity from widget", | 2710 | "set-entity-from-widget": "Set entity from widget", |
@@ -2715,7 +2716,19 @@ | @@ -2715,7 +2716,19 @@ | ||
2715 | "dialog-width": "Dialog width in percents relative to viewport width", | 2716 | "dialog-width": "Dialog width in percents relative to viewport width", |
2716 | "dialog-height": "Dialog height in percents relative to viewport height", | 2717 | "dialog-height": "Dialog height in percents relative to viewport height", |
2717 | "dialog-size-range-error": "Dialog size percent value should be in a range from 1 to 100.", | 2718 | "dialog-size-range-error": "Dialog size percent value should be in a range from 1 to 100.", |
2718 | - "open-new-browser-tab": "Open in a new browser tab" | 2719 | + "open-new-browser-tab": "Open in a new browser tab", |
2720 | + "mobile": { | ||
2721 | + "action-type": "Mobile action type", | ||
2722 | + "action-type-required": "Mobile action type is required", | ||
2723 | + "take-picture-from-gallery": "Take picture from gallery", | ||
2724 | + "take-photo": "Take photo", | ||
2725 | + "map-direction": "Open map directions", | ||
2726 | + "map-location": "Open map location", | ||
2727 | + "scan-qr-code": "Scan QR Code", | ||
2728 | + "make-phone-call": "Make phone call", | ||
2729 | + "get-location": "Get phone location", | ||
2730 | + "take-screenshot": "Take screenshot" | ||
2731 | + } | ||
2719 | }, | 2732 | }, |
2720 | "widgets-bundle": { | 2733 | "widgets-bundle": { |
2721 | "current": "Current bundle", | 2734 | "current": "Current bundle", |