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 | 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 | 567 | @PreAuthorize("hasAuthority('TENANT_ADMIN')") |
537 | 568 | @RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.GET) |
538 | 569 | @ResponseBody |
... | ... | @@ -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 | 632 | private HomeDashboard extractHomeDashboardFromAdditionalInfo(JsonNode additionalInfo) { |
586 | 633 | try { |
587 | 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 | 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 | 19 | fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen"> |
20 | 20 | <tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet> |
21 | 21 | <section class="tb-dashboard-toolbar" |
... | ... | @@ -25,7 +25,7 @@ |
25 | 25 | [toolbarOpened]="toolbarOpened" (triggerClick)="openToolbar()"> |
26 | 26 | <div class="tb-dashboard-action-panels" fxLayout="column" fxLayout.gt-sm="row" |
27 | 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 | 29 | fxLayoutAlign.gt-sm="end center" fxLayoutAlign="space-between center" fxLayoutGap="12px"> |
30 | 30 | <tb-user-menu *ngIf="!isPublicUser() && forceFullscreen" fxHide.gt-sm displayUserInfo="true"> |
31 | 31 | </tb-user-menu> |
... | ... | @@ -49,7 +49,7 @@ |
49 | 49 | <tb-states-component fxFlex.lt-md |
50 | 50 | [statesControllerId]="isEdit ? 'default' : dashboardConfiguration.settings.stateControllerId" |
51 | 51 | [dashboardCtrl]="this" |
52 | - [dashboardId]="(!embedded && dashboard.id) ? dashboard.id.id : ''" | |
52 | + [dashboardId]="setStateDashboardId ? dashboard.id.id : ''" | |
53 | 53 | [isMobile]="isMobile" |
54 | 54 | [state]="dashboardCtx.state" |
55 | 55 | [currentState]="currentState" | ... | ... |
... | ... | @@ -18,6 +18,7 @@ |
18 | 18 | $toolbar-height: 50px !default; |
19 | 19 | $fullscreen-toolbar-height: 64px !default; |
20 | 20 | $mobile-toolbar-height: 84px !default; |
21 | +$mobile-app-toolbar-height: 40px !default; | |
21 | 22 | |
22 | 23 | tb-dashboard-page { |
23 | 24 | display: flex; |
... | ... | @@ -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 | 113 | mat-drawer-container.tb-dashboard-drawer-container { |
105 | 114 | mat-drawer-container.tb-dashboard-layouts { |
106 | 115 | width: 100%; | ... | ... |
... | ... | @@ -120,7 +120,8 @@ import { |
120 | 120 | DisplayWidgetTypesPanelData |
121 | 121 | } from '@home/components/dashboard-page/widget-types-panel.component'; |
122 | 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 | 126 | // @dynamic |
126 | 127 | @Component({ |
... | ... | @@ -159,6 +160,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
159 | 160 | singlePageMode: boolean; |
160 | 161 | forceFullscreen = this.authState.forceFullscreen; |
161 | 162 | |
163 | + isMobileApp = this.mobileService.isMobileApp(); | |
162 | 164 | isFullscreen = false; |
163 | 165 | isEdit = false; |
164 | 166 | isEditingWidget = false; |
... | ... | @@ -189,6 +191,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
189 | 191 | currentCustomerId: string; |
190 | 192 | currentDashboardScope: DashboardPageScope; |
191 | 193 | |
194 | + setStateDashboardId = false; | |
195 | + | |
192 | 196 | addingLayoutCtx: DashboardPageLayoutContext; |
193 | 197 | |
194 | 198 | logo = 'assets/logo_title_white.svg'; |
... | ... | @@ -284,6 +288,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
284 | 288 | private dashboardService: DashboardService, |
285 | 289 | private itembuffer: ItemBufferService, |
286 | 290 | private importExport: ImportExportService, |
291 | + private mobileService: MobileService, | |
287 | 292 | private fb: FormBuilder, |
288 | 293 | private dialog: MatDialog, |
289 | 294 | private translate: TranslateService, |
... | ... | @@ -322,6 +327,19 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
322 | 327 | |
323 | 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 | 343 | this.currentDashboardId = data.currentDashboardId; |
326 | 344 | |
327 | 345 | if (this.route.snapshot.params.customerId) { |
... | ... | @@ -332,7 +350,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
332 | 350 | this.currentCustomerId = this.authUser.customerId; |
333 | 351 | } |
334 | 352 | |
335 | - this.dashboard = data.dashboard; | |
336 | 353 | this.dashboardConfiguration = this.dashboard.configuration; |
337 | 354 | this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow; |
338 | 355 | this.layouts.main.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboardCtx); |
... | ... | @@ -388,6 +405,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
388 | 405 | this.currentCustomerId = null; |
389 | 406 | this.currentDashboardScope = null; |
390 | 407 | |
408 | + this.setStateDashboardId = false; | |
409 | + | |
391 | 410 | this.dashboardCtx.state = null; |
392 | 411 | } |
393 | 412 | ... | ... |
... | ... | @@ -21,6 +21,42 @@ $mobile-toolbar-height: 80px !default; |
21 | 21 | $half-mobile-toolbar-height: 40px !default; |
22 | 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 | 60 | tb-dashboard-toolbar { |
25 | 61 | mat-fab-toolbar { |
26 | 62 | mat-fab-trigger { | ... | ... |
... | ... | @@ -26,6 +26,7 @@ import { UtilsService } from '@core/services/utils.service'; |
26 | 26 | import { base64toObj, objToBase64URI } from '@app/core/utils'; |
27 | 27 | import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; |
28 | 28 | import { EntityService } from '@core/http/entity.service'; |
29 | +import { MobileService } from '@core/services/mobile.service'; | |
29 | 30 | |
30 | 31 | @Component({ |
31 | 32 | selector: 'tb-default-state-controller', |
... | ... | @@ -40,6 +41,7 @@ export class DefaultStateControllerComponent extends StateControllerComponent im |
40 | 41 | protected statesControllerService: StatesControllerService, |
41 | 42 | private utils: UtilsService, |
42 | 43 | private entityService: EntityService, |
44 | + private mobileService: MobileService, | |
43 | 45 | private dashboardUtils: DashboardUtilsService) { |
44 | 46 | super(router, route, ngZone, statesControllerService); |
45 | 47 | } |
... | ... | @@ -229,6 +231,9 @@ export class DefaultStateControllerComponent extends StateControllerComponent im |
229 | 231 | private gotoState(stateId: string, update: boolean, openRightLayout?: boolean) { |
230 | 232 | if (this.dashboardCtrl.dashboardCtx.state !== stateId) { |
231 | 233 | this.dashboardCtrl.openDashboardState(stateId, openRightLayout); |
234 | + if (stateId && this.statesValue[stateId]) { | |
235 | + this.mobileService.handleDashboardStateName(this.getStateName(stateId, this.statesValue[stateId])); | |
236 | + } | |
232 | 237 | if (update) { |
233 | 238 | this.updateLocation(); |
234 | 239 | } | ... | ... |
... | ... | @@ -28,6 +28,7 @@ import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; |
28 | 28 | import { EntityService } from '@core/http/entity.service'; |
29 | 29 | import { EntityType } from '@shared/models/entity-type.models'; |
30 | 30 | import { map, tap } from 'rxjs/operators'; |
31 | +import { MobileService } from '@core/services/mobile.service'; | |
31 | 32 | |
32 | 33 | @Component({ |
33 | 34 | selector: 'tb-entity-state-controller', |
... | ... | @@ -44,6 +45,7 @@ export class EntityStateControllerComponent extends StateControllerComponent imp |
44 | 45 | protected statesControllerService: StatesControllerService, |
45 | 46 | private utils: UtilsService, |
46 | 47 | private entityService: EntityService, |
48 | + private mobileService: MobileService, | |
47 | 49 | private dashboardUtils: DashboardUtilsService) { |
48 | 50 | super(router, route, ngZone, statesControllerService); |
49 | 51 | } |
... | ... | @@ -270,6 +272,7 @@ export class EntityStateControllerComponent extends StateControllerComponent imp |
270 | 272 | |
271 | 273 | private gotoState(stateId: string, update: boolean, openRightLayout?: boolean) { |
272 | 274 | this.dashboardCtrl.openDashboardState(stateId, openRightLayout); |
275 | + this.mobileService.handleDashboardStateName(this.getStateName(this.stateObject.length - 1)); | |
273 | 276 | if (update) { |
274 | 277 | this.updateLocation(); |
275 | 278 | } | ... | ... |
... | ... | @@ -55,6 +55,7 @@ import { ManageWidgetActionsComponent } from '@home/components/widget/action/man |
55 | 55 | import { WidgetActionDialogComponent } from '@home/components/widget/action/widget-action-dialog.component'; |
56 | 56 | import { CustomActionPrettyResourcesTabsComponent } from '@home/components/widget/action/custom-action-pretty-resources-tabs.component'; |
57 | 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 | 59 | import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service'; |
59 | 60 | import { CustomDialogContainerComponent } from '@home/components/widget/dialog/custom-dialog-container.component'; |
60 | 61 | import { ImportExportService } from '@home/components/import-export/import-export.service'; |
... | ... | @@ -183,6 +184,7 @@ import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-pag |
183 | 184 | WidgetActionDialogComponent, |
184 | 185 | CustomActionPrettyResourcesTabsComponent, |
185 | 186 | CustomActionPrettyEditorComponent, |
187 | + MobileActionEditorComponent, | |
186 | 188 | CustomDialogContainerComponent, |
187 | 189 | ImportDialogComponent, |
188 | 190 | ImportDialogCsvComponent, |
... | ... | @@ -296,6 +298,7 @@ import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-pag |
296 | 298 | WidgetActionDialogComponent, |
297 | 299 | CustomActionPrettyResourcesTabsComponent, |
298 | 300 | CustomActionPrettyEditorComponent, |
301 | + MobileActionEditorComponent, | |
299 | 302 | CustomDialogContainerComponent, |
300 | 303 | ImportDialogComponent, |
301 | 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 | 180 | formControlName="customAction"> |
181 | 181 | </tb-custom-action-pretty-editor> |
182 | 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 | 188 | </section> |
184 | 189 | </fieldset> |
185 | 190 | </div> | ... | ... |
... | ... | @@ -45,6 +45,7 @@ import { Dashboard } from '@shared/models/dashboard.models'; |
45 | 45 | import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; |
46 | 46 | import { CustomActionEditorCompleter } from '@home/components/widget/action/custom-action.models'; |
47 | 47 | import { isDefinedAndNotNull } from '@core/utils'; |
48 | +import { MobileActionEditorComponent } from '@home/components/widget/action/mobile-action-editor.component'; | |
48 | 49 | |
49 | 50 | export interface WidgetActionDialogData { |
50 | 51 | isAdd: boolean; |
... | ... | @@ -64,6 +65,8 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia |
64 | 65 | |
65 | 66 | @ViewChild('dashboardStateInput') dashboardStateInput: ElementRef; |
66 | 67 | |
68 | + @ViewChild('mobileActionEditor', {static: false}) mobileActionEditor: MobileActionEditorComponent; | |
69 | + | |
67 | 70 | widgetActionFormGroup: FormGroup; |
68 | 71 | actionTypeFormGroup: FormGroup; |
69 | 72 | |
... | ... | @@ -197,6 +200,12 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia |
197 | 200 | this.fb.control(toCustomAction(action), [Validators.required]) |
198 | 201 | ); |
199 | 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 | 322 | |
314 | 323 | save(): void { |
315 | 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 | 39 | defaultLegendConfig, |
40 | 40 | LegendConfig, |
41 | 41 | LegendData, |
42 | - LegendPosition, | |
42 | + LegendPosition, MobileActionResult, | |
43 | 43 | Widget, |
44 | 44 | WidgetActionDescriptor, |
45 | 45 | widgetActionSources, |
46 | 46 | WidgetActionType, |
47 | - WidgetComparisonSettings, | |
47 | + WidgetComparisonSettings, WidgetMobileActionDescriptor, WidgetMobileActionType, | |
48 | 48 | WidgetResource, |
49 | 49 | widgetType, |
50 | 50 | WidgetTypeParameters |
... | ... | @@ -77,7 +77,7 @@ import { EntityId } from '@shared/models/id/entity-id'; |
77 | 77 | import { ActivatedRoute, Router } from '@angular/router'; |
78 | 78 | import cssjs from '@core/css/css'; |
79 | 79 | import { ResourcesService } from '@core/services/resources.service'; |
80 | -import { catchError, switchMap } from 'rxjs/operators'; | |
80 | +import { catchError, map, switchMap } from 'rxjs/operators'; | |
81 | 81 | import { ActionNotificationShow } from '@core/notification/notification.actions'; |
82 | 82 | import { TimeService } from '@core/services/time.service'; |
83 | 83 | import { DeviceService } from '@app/core/http/device.service'; |
... | ... | @@ -97,6 +97,8 @@ import { AlarmDataService } from '@core/api/alarm-data.service'; |
97 | 97 | import { MatDialog } from '@angular/material/dialog'; |
98 | 98 | import { ComponentType } from '@angular/cdk/portal'; |
99 | 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 | 103 | @Component({ |
102 | 104 | selector: 'tb-widget', |
... | ... | @@ -177,6 +179,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
177 | 179 | private alarmDataService: AlarmDataService, |
178 | 180 | private translate: TranslateService, |
179 | 181 | private utils: UtilsService, |
182 | + private mobileService: MobileService, | |
183 | + private dialogs: DialogService, | |
180 | 184 | private raf: RafService, |
181 | 185 | private ngZone: NgZone, |
182 | 186 | private cd: ChangeDetectorRef) { |
... | ... | @@ -1094,6 +1098,178 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
1094 | 1098 | } |
1095 | 1099 | ); |
1096 | 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 | 77 | import { SortOrder } from '@shared/models/page/sort-order'; |
78 | 78 | import { DomSanitizer } from '@angular/platform-browser'; |
79 | 79 | import { Router } from '@angular/router'; |
80 | +import { map, mergeMap } from 'rxjs/operators'; | |
80 | 81 | |
81 | 82 | export interface IWidgetAction { |
82 | 83 | name: string; |
... | ... | @@ -238,7 +239,9 @@ export class WidgetContext { |
238 | 239 | |
239 | 240 | rxjs = { |
240 | 241 | forkJoin, |
241 | - of | |
242 | + of, | |
243 | + map, | |
244 | + mergeMap | |
242 | 245 | }; |
243 | 246 | |
244 | 247 | showSuccessToast(message: string, duration: number = 1000, | ... | ... |
... | ... | @@ -332,7 +332,19 @@ export enum WidgetActionType { |
332 | 332 | updateDashboardState = 'updateDashboardState', |
333 | 333 | openDashboard = 'openDashboard', |
334 | 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 | 350 | export const widgetActionTypeTranslationMap = new Map<WidgetActionType, string>( |
... | ... | @@ -341,10 +353,90 @@ export const widgetActionTypeTranslationMap = new Map<WidgetActionType, string>( |
341 | 353 | [ WidgetActionType.updateDashboardState, 'widget-action.update-dashboard-state' ], |
342 | 354 | [ WidgetActionType.openDashboard, 'widget-action.open-dashboard' ], |
343 | 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 | 440 | export interface CustomActionDescriptor { |
349 | 441 | customFunction?: string; |
350 | 442 | customResources?: Array<WidgetResource>; |
... | ... | @@ -369,6 +461,7 @@ export interface WidgetActionDescriptor extends CustomActionDescriptor { |
369 | 461 | dialogHeight?: number; |
370 | 462 | setEntityId?: boolean; |
371 | 463 | stateEntityParamName?: string; |
464 | + mobileAction?: WidgetMobileActionDescriptor; | |
372 | 465 | } |
373 | 466 | |
374 | 467 | export interface WidgetComparisonSettings { | ... | ... |
... | ... | @@ -2704,6 +2704,7 @@ |
2704 | 2704 | "open-dashboard": "Navigate to other dashboard", |
2705 | 2705 | "custom": "Custom action", |
2706 | 2706 | "custom-pretty": "Custom action (with HTML template)", |
2707 | + "mobile-action": "Mobile action", | |
2707 | 2708 | "target-dashboard-state": "Target dashboard state", |
2708 | 2709 | "target-dashboard-state-required": "Target dashboard state is required", |
2709 | 2710 | "set-entity-from-widget": "Set entity from widget", |
... | ... | @@ -2715,7 +2716,19 @@ |
2715 | 2716 | "dialog-width": "Dialog width in percents relative to viewport width", |
2716 | 2717 | "dialog-height": "Dialog height in percents relative to viewport height", |
2717 | 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 | 2733 | "widgets-bundle": { |
2721 | 2734 | "current": "Current bundle", | ... | ... |