-
@@ -49,7 +49,7 @@
div {
+ height: $mobile-app-toolbar-height;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
tb-dashboard-toolbar {
mat-fab-toolbar {
mat-fab-trigger {
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.ts
index 6d2e756489..27e613a64e 100644
--- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.ts
+++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.ts
@@ -26,6 +26,7 @@ import { UtilsService } from '@core/services/utils.service';
import { base64toObj, objToBase64URI } from '@app/core/utils';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { EntityService } from '@core/http/entity.service';
+import { MobileService } from '@core/services/mobile.service';
@Component({
selector: 'tb-default-state-controller',
@@ -40,6 +41,7 @@ export class DefaultStateControllerComponent extends StateControllerComponent im
protected statesControllerService: StatesControllerService,
private utils: UtilsService,
private entityService: EntityService,
+ private mobileService: MobileService,
private dashboardUtils: DashboardUtilsService) {
super(router, route, ngZone, statesControllerService);
}
@@ -229,6 +231,9 @@ export class DefaultStateControllerComponent extends StateControllerComponent im
private gotoState(stateId: string, update: boolean, openRightLayout?: boolean) {
if (this.dashboardCtrl.dashboardCtx.state !== stateId) {
this.dashboardCtrl.openDashboardState(stateId, openRightLayout);
+ if (stateId && this.statesValue[stateId]) {
+ this.mobileService.handleDashboardStateName(this.getStateName(stateId, this.statesValue[stateId]));
+ }
if (update) {
this.updateLocation();
}
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.ts
index 39b340901d..cd5628dc06 100644
--- a/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.ts
+++ b/ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.ts
@@ -28,6 +28,7 @@ import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { EntityService } from '@core/http/entity.service';
import { EntityType } from '@shared/models/entity-type.models';
import { map, tap } from 'rxjs/operators';
+import { MobileService } from '@core/services/mobile.service';
@Component({
selector: 'tb-entity-state-controller',
@@ -44,6 +45,7 @@ export class EntityStateControllerComponent extends StateControllerComponent imp
protected statesControllerService: StatesControllerService,
private utils: UtilsService,
private entityService: EntityService,
+ private mobileService: MobileService,
private dashboardUtils: DashboardUtilsService) {
super(router, route, ngZone, statesControllerService);
}
@@ -270,6 +272,7 @@ export class EntityStateControllerComponent extends StateControllerComponent imp
private gotoState(stateId: string, update: boolean, openRightLayout?: boolean) {
this.dashboardCtrl.openDashboardState(stateId, openRightLayout);
+ this.mobileService.handleDashboardStateName(this.getStateName(this.stateObject.length - 1));
if (update) {
this.updateLocation();
}
diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts
index a5a3ed460b..53b95c7979 100644
--- a/ui-ngx/src/app/modules/home/components/home-components.module.ts
+++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts
@@ -55,6 +55,7 @@ import { ManageWidgetActionsComponent } from '@home/components/widget/action/man
import { WidgetActionDialogComponent } from '@home/components/widget/action/widget-action-dialog.component';
import { CustomActionPrettyResourcesTabsComponent } from '@home/components/widget/action/custom-action-pretty-resources-tabs.component';
import { CustomActionPrettyEditorComponent } from '@home/components/widget/action/custom-action-pretty-editor.component';
+import { MobileActionEditorComponent } from '@home/components/widget/action/mobile-action-editor.component';
import { CustomDialogService } from '@home/components/widget/dialog/custom-dialog.service';
import { CustomDialogContainerComponent } from '@home/components/widget/dialog/custom-dialog-container.component';
import { ImportExportService } from '@home/components/import-export/import-export.service';
@@ -185,6 +186,7 @@ import { SecurityConfigLwm2mServerComponent } from '@home/components/device/secu
WidgetActionDialogComponent,
CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent,
+ MobileActionEditorComponent,
CustomDialogContainerComponent,
ImportDialogComponent,
ImportDialogCsvComponent,
@@ -300,6 +302,7 @@ import { SecurityConfigLwm2mServerComponent } from '@home/components/device/secu
WidgetActionDialogComponent,
CustomActionPrettyResourcesTabsComponent,
CustomActionPrettyEditorComponent,
+ MobileActionEditorComponent,
CustomDialogContainerComponent,
ImportDialogComponent,
ImportDialogCsvComponent,
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.html b/ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.html
new file mode 100644
index 0000000000..cdb4615068
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.html
@@ -0,0 +1,100 @@
+
+
+
+ widget-action.mobile.action-type
+
+
+ {{ mobileActionTypeTranslations.get(mobileActionType[actionType]) | translate }}
+
+
+
+ {{ 'widget-action.mobile.action-type-required' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.ts
new file mode 100644
index 0000000000..966268faa3
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.ts
@@ -0,0 +1,253 @@
+///
+/// Copyright © 2016-2021 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { Component, forwardRef, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
+import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
+import { Store } from '@ngrx/store';
+import { AppState } from '@app/core/core.state';
+import { coerceBooleanProperty } from '@angular/cdk/coercion';
+import { deepClone } from '@core/utils';
+import {
+ WidgetMobileActionDescriptor,
+ WidgetMobileActionType, widgetMobileActionTypeTranslationMap
+} from '@shared/models/widget.models';
+import { CustomActionEditorCompleter } from '@home/components/widget/action/custom-action.models';
+import { JsFuncComponent } from '@shared/components/js-func.component';
+import {
+ getDefaultGetLocationFunction, getDefaultGetPhoneNumberFunction,
+ getDefaultHandleEmptyResultFunction,
+ getDefaultHandleErrorFunction,
+ getDefaultProcessImageFunction,
+ getDefaultProcessLaunchResultFunction, getDefaultProcessLocationFunction,
+ getDefaultProcessQrCodeFunction
+} from '@home/components/widget/action/mobile-action-editor.models';
+
+@Component({
+ selector: 'tb-mobile-action-editor',
+ templateUrl: './mobile-action-editor.component.html',
+ styleUrls: [],
+ providers: [{
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => MobileActionEditorComponent),
+ multi: true
+ }]
+})
+export class MobileActionEditorComponent implements ControlValueAccessor, OnInit {
+
+ @ViewChildren(JsFuncComponent) jsFuncComponents: QueryList;
+
+ mobileActionTypes = Object.keys(WidgetMobileActionType);
+ mobileActionTypeTranslations = widgetMobileActionTypeTranslationMap;
+ mobileActionType = WidgetMobileActionType;
+
+ customActionEditorCompleter = CustomActionEditorCompleter;
+
+ mobileActionFormGroup: FormGroup;
+ mobileActionTypeFormGroup: FormGroup;
+
+ private requiredValue: boolean;
+ get required(): boolean {
+ return this.requiredValue;
+ }
+ @Input()
+ set required(value: boolean) {
+ this.requiredValue = coerceBooleanProperty(value);
+ }
+
+ @Input()
+ disabled: boolean;
+
+ private propagateChange = (v: any) => { };
+
+ constructor(private store: Store,
+ private fb: FormBuilder) {
+ }
+
+ registerOnChange(fn: any): void {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ }
+
+ ngOnInit() {
+ this.mobileActionFormGroup = this.fb.group({
+ type: [null, Validators.required],
+ handleEmptyResultFunction: [null],
+ handleErrorFunction: [null]
+ });
+ this.mobileActionFormGroup.get('type').valueChanges.subscribe((type: WidgetMobileActionType) => {
+ let action: WidgetMobileActionDescriptor = this.mobileActionFormGroup.value;
+ if (this.mobileActionTypeFormGroup) {
+ action = {...action, ...this.mobileActionTypeFormGroup.value};
+ }
+ this.updateMobileActionType(type, action);
+ });
+ this.mobileActionFormGroup.valueChanges.subscribe(() => {
+ this.updateModel();
+ });
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ if (this.disabled) {
+ this.mobileActionFormGroup.disable({emitEvent: false});
+ if (this.mobileActionTypeFormGroup) {
+ this.mobileActionTypeFormGroup.disable({emitEvent: false});
+ }
+ } else {
+ this.mobileActionFormGroup.enable({emitEvent: false});
+ if (this.mobileActionTypeFormGroup) {
+ this.mobileActionTypeFormGroup.enable({emitEvent: false});
+ }
+ }
+ }
+
+ writeValue(value: WidgetMobileActionDescriptor | null): void {
+ this.mobileActionFormGroup.patchValue({type: value?.type,
+ handleEmptyResultFunction: value?.handleEmptyResultFunction,
+ handleErrorFunction: value?.handleErrorFunction}, {emitEvent: false});
+ this.updateMobileActionType(value?.type, value);
+ }
+
+ private updateModel() {
+ let descriptor: WidgetMobileActionDescriptor = null;
+ if (this.mobileActionFormGroup.valid && this.mobileActionTypeFormGroup.valid) {
+ descriptor = { ...this.mobileActionFormGroup.getRawValue(), ...this.mobileActionTypeFormGroup.getRawValue() };
+ }
+ this.propagateChange(descriptor);
+ }
+
+ private updateMobileActionType(type?: WidgetMobileActionType, action?: WidgetMobileActionDescriptor) {
+ const prevType = action?.type;
+ const targetType = type || prevType;
+ const changed = prevType !== type;
+ if (changed && targetType) {
+ let handleEmptyResultFunction = action?.handleEmptyResultFunction;
+ const defaultHandleEmptyResultFunction = getDefaultHandleEmptyResultFunction(targetType);
+ if (defaultHandleEmptyResultFunction !== handleEmptyResultFunction) {
+ handleEmptyResultFunction = getDefaultHandleEmptyResultFunction(type);
+ this.mobileActionFormGroup.patchValue({handleEmptyResultFunction}, {emitEvent: false});
+ }
+ let handleErrorFunction = action?.handleErrorFunction;
+ const defaultHandleErrorFunction = getDefaultHandleErrorFunction(targetType);
+ if (defaultHandleErrorFunction !== handleErrorFunction) {
+ handleErrorFunction = getDefaultHandleErrorFunction(type);
+ this.mobileActionFormGroup.patchValue({handleErrorFunction}, {emitEvent: false});
+ }
+ }
+ this.mobileActionTypeFormGroup = this.fb.group({});
+ if (type) {
+ switch (type) {
+ case WidgetMobileActionType.takePictureFromGallery:
+ case WidgetMobileActionType.takePhoto:
+ case WidgetMobileActionType.takeScreenshot:
+ let processImageFunction = action?.processImageFunction;
+ if (changed) {
+ const defaultProcessImageFunction = getDefaultProcessImageFunction(targetType);
+ if (defaultProcessImageFunction !== processImageFunction) {
+ processImageFunction = getDefaultProcessImageFunction(type);
+ }
+ }
+ this.mobileActionTypeFormGroup.addControl(
+ 'processImageFunction',
+ this.fb.control(processImageFunction, [])
+ );
+ break;
+ case WidgetMobileActionType.mapDirection:
+ case WidgetMobileActionType.mapLocation:
+ let getLocationFunction = action?.getLocationFunction;
+ let processLaunchResultFunction = action?.processLaunchResultFunction;
+ if (changed) {
+ const defaultGetLocationFunction = getDefaultGetLocationFunction();
+ if (defaultGetLocationFunction !== getLocationFunction) {
+ getLocationFunction = defaultGetLocationFunction;
+ }
+ const defaultProcessLaunchResultFunction = getDefaultProcessLaunchResultFunction(targetType);
+ if (defaultProcessLaunchResultFunction !== processLaunchResultFunction) {
+ processLaunchResultFunction = getDefaultProcessLaunchResultFunction(type);
+ }
+ }
+ this.mobileActionTypeFormGroup.addControl(
+ 'getLocationFunction',
+ this.fb.control(getLocationFunction, [Validators.required])
+ );
+ this.mobileActionTypeFormGroup.addControl(
+ 'processLaunchResultFunction',
+ this.fb.control(processLaunchResultFunction, [])
+ );
+ break;
+ case WidgetMobileActionType.scanQrCode:
+ let processQrCodeFunction = action?.processQrCodeFunction;
+ if (changed) {
+ const defaultProcessQrCodeFunction = getDefaultProcessQrCodeFunction();
+ if (defaultProcessQrCodeFunction !== processQrCodeFunction) {
+ processQrCodeFunction = defaultProcessQrCodeFunction;
+ }
+ }
+ this.mobileActionTypeFormGroup.addControl(
+ 'processQrCodeFunction',
+ this.fb.control(processQrCodeFunction, [])
+ );
+ break;
+ case WidgetMobileActionType.makePhoneCall:
+ let getPhoneNumberFunction = action?.getPhoneNumberFunction;
+ processLaunchResultFunction = action?.processLaunchResultFunction;
+ if (changed) {
+ const defaultGetPhoneNumberFunction = getDefaultGetPhoneNumberFunction();
+ if (defaultGetPhoneNumberFunction !== getPhoneNumberFunction) {
+ getPhoneNumberFunction = defaultGetPhoneNumberFunction;
+ }
+ const defaultProcessLaunchResultFunction = getDefaultProcessLaunchResultFunction(targetType);
+ if (defaultProcessLaunchResultFunction !== processLaunchResultFunction) {
+ processLaunchResultFunction = getDefaultProcessLaunchResultFunction(type);
+ }
+ }
+ this.mobileActionTypeFormGroup.addControl(
+ 'getPhoneNumberFunction',
+ this.fb.control(getPhoneNumberFunction, [Validators.required])
+ );
+ this.mobileActionTypeFormGroup.addControl(
+ 'processLaunchResultFunction',
+ this.fb.control(processLaunchResultFunction, [])
+ );
+ break;
+ case WidgetMobileActionType.getLocation:
+ let processLocationFunction = action?.processLocationFunction;
+ if (changed) {
+ const defaultProcessLocationFunction = getDefaultProcessLocationFunction();
+ if (defaultProcessLocationFunction !== processLocationFunction) {
+ processLocationFunction = defaultProcessLocationFunction;
+ }
+ }
+ this.mobileActionTypeFormGroup.addControl(
+ 'processLocationFunction',
+ this.fb.control(processLocationFunction, [Validators.required])
+ );
+ break;
+ }
+ }
+ this.mobileActionTypeFormGroup.valueChanges.subscribe(() => {
+ this.updateModel();
+ });
+ }
+
+ public validateOnSubmit() {
+ for (const jsFuncComponent of this.jsFuncComponents.toArray()) {
+ jsFuncComponent.validateOnSubmit();
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.models.ts b/ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.models.ts
new file mode 100644
index 0000000000..ea32b812be
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.models.ts
@@ -0,0 +1,317 @@
+///
+/// Copyright © 2016-2021 The Thingsboard Authors
+///
+/// Licensed under the Apache License, Version 2.0 (the "License");
+/// you may not use this file except in compliance with the License.
+/// You may obtain a copy of the License at
+///
+/// http://www.apache.org/licenses/LICENSE-2.0
+///
+/// Unless required by applicable law or agreed to in writing, software
+/// distributed under the License is distributed on an "AS IS" BASIS,
+/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+/// See the License for the specific language governing permissions and
+/// limitations under the License.
+///
+
+import { WidgetMobileActionType } from '@shared/models/widget.models';
+
+const processImageFunctionTemplate =
+ '// Function body to process image obtained as a result of mobile action (take photo, take image from gallery, etc.). \n' +
+ '// - imageUrl - image URL in base64 data format\n\n' +
+ 'showImageDialog(\'--TITLE--\', imageUrl);\n' +
+ '//saveEntityImageAttribute(\'image\', imageUrl);\n' +
+ '\n' +
+ 'function showImageDialog(title, imageUrl) {\n' +
+ ' setTimeout(function() {\n' +
+ ' widgetContext.customDialog.customDialog(imageDialogTemplate, ImageDialogController, {imageUrl: imageUrl, title: title}).subscribe();\n' +
+ ' }, 100);\n' +
+ '}\n' +
+ '\n' +
+ 'function saveEntityImageAttribute(attributeName, imageUrl) {\n' +
+ ' if (entityId) {\n' +
+ ' let attributes = [{\n' +
+ ' key: attributeName, value: imageUrl\n' +
+ ' }];\n' +
+ ' widgetContext.attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributes).subscribe(\n' +
+ ' function() {\n' +
+ ' widgetContext.showSuccessToast(\'Image attribute saved!\');\n' +
+ ' },\n' +
+ ' function(error) {\n' +
+ ' widgetContext.dialogs.alert(\'Image attribute save failed\', JSON.stringify(error));\n' +
+ ' }\n' +
+ ' );\n' +
+ ' }\n' +
+ '}\n' +
+ '\n' +
+ 'var\n' +
+ ' imageDialogTemplate =\n' +
+ ' \'\' +\n' +
+ ' \'
\' +\n' +
+ ' \'
\';\n' +
+ '\n' +
+ 'function ImageDialogController(instance) {\n' +
+ ' let vm = instance;\n' +
+ ' vm.title = vm.data.title;\n' +
+ ' vm.imageUrl = vm.data.imageUrl;\n' +
+ ' vm.close = function ()\n' +
+ ' {\n' +
+ ' vm.dialogRef.close(null);\n' +
+ ' }\n' +
+ '}\n';
+
+const processLaunchResultFunctionTemplate =
+ '// Optional function body to process result of attempt to launch external mobile application (for ex. map application or phone call application). \n' +
+ '// - launched - boolean value indicating if the external application was successfully launched.\n\n' +
+ 'showLaunchStatusDialog(\'--TITLE--\', launched);\n' +
+ '\n' +
+ 'function showLaunchStatusDialog(title, status) {\n' +
+ ' setTimeout(function() {\n' +
+ ' widgetContext.dialogs.alert(title, status ? \'Successfully launched\' : \'Failed to launch\').subscribe();\n' +
+ ' }, 100);\n' +
+ '}\n';
+
+const processQrCodeFunction =
+ '// Function body to process result of QR code scanning. \n' +
+ '// - code - scanned QR code\n' +
+ '// - format - scanned QR code format\n\n' +
+ 'showQrCodeDialog(\'QR Code\', code, format);\n' +
+ '\n' +
+ 'function showQrCodeDialog(title, code, format) {\n' +
+ ' setTimeout(function() {\n' +
+ ' widgetContext.dialogs.alert(title, \'Code: [\'+code+\']
Format: \' + format).subscribe();\n' +
+ ' }, 100);\n' +
+ '}\n';
+
+const processLocationFunction =
+ '// Function body to process current location of the phone. \n' +
+ '// - latitude - phone location latitude\n' +
+ '// - longitude - phone location longitude\n\n' +
+ 'showLocationDialog(\'Location\', latitude, longitude);\n' +
+ '// saveEntityLocationAttributes(\'latitude\', \'longitude\', latitude, longitude);\n' +
+ '\n' +
+ 'function showImageDialog(title, imageUrl) {\n' +
+ ' setTimeout(function() {\n' +
+ ' widgetContext.customDialog.customDialog(imageDialogTemplate, ImageDialogController, {imageUrl: imageUrl, title: title}).subscribe();\n' +
+ ' }, 100);\n' +
+ '}\n' +
+ '\n' +
+ 'function saveEntityLocationAttributes(latitudeAttributeName, longitudeAttributeName, latitude, longitude) {\n' +
+ ' if (entityId) {\n' +
+ ' let attributes = [\n' +
+ ' { key: latitudeAttributeName, value: latitude },\n' +
+ ' { key: longitudeAttributeName, value: longitude }\n' +
+ ' ];\n' +
+ ' widgetContext.attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributes).subscribe(\n' +
+ ' function() {\n' +
+ ' widgetContext.showSuccessToast(\'Location attributes saved!\');\n' +
+ ' },\n' +
+ ' function(error) {\n' +
+ ' widgetContext.dialogs.alert(\'Location attributes save failed\', JSON.stringify(error));\n' +
+ ' }\n' +
+ ' );\n' +
+ ' }\n' +
+ '}\n' +
+ '\n' +
+ '\n' +
+ 'function showLocationDialog(title, latitude, longitude) {\n' +
+ ' setTimeout(function() {\n' +
+ ' widgetContext.dialogs.alert(title, \'Latitude: \'+latitude+\'
Longitude: \' + longitude).subscribe();\n' +
+ ' }, 100);\n' +
+ '}';
+
+const handleEmptyResultFunctionTemplate =
+ '// Optional function body to handle empty result. \n' +
+ '// Usually this happens when user cancels the action (for ex. by pressing phone back button). \n\n' +
+ 'showEmptyResultDialog(\'--MESSAGE--\');\n' +
+ '\n' +
+ 'function showEmptyResultDialog(message) {\n' +
+ ' setTimeout(function() {\n' +
+ ' widgetContext.dialogs.alert(\'Empty result\', message).subscribe();\n' +
+ ' }, 100);\n' +
+ '}\n';
+
+const handleErrorFunctionTemplate =
+ '// Optional function body to handle error occurred while mobile action execution \n' +
+ '// - error - Error message\n\n' +
+ 'showErrorDialog(\'--TITLE--\', error);\n' +
+ '\n' +
+ 'function showErrorDialog(title, error) {\n' +
+ ' setTimeout(function() {\n' +
+ ' widgetContext.dialogs.alert(title, error).subscribe();\n' +
+ ' }, 100);\n' +
+ '}\n';
+
+const getLocationFunctionTemplate =
+ '// Function body that should return location as array of two numbers (latitude, longitude) for further processing by mobile action.\n' +
+ '// Usually location can be obtained from entity attributes/telemetry. \n\n' +
+ 'return getLocationFromEntityAttributes();\n' +
+ '//return [30, 30]; // TEST LOCATION\n' +
+ '\n' +
+ '\n' +
+ 'function getLocationFromEntityAttributes() {\n' +
+ ' if (entityId) {\n' +
+ ' return widgetContext.attributeService.getEntityAttributes(entityId, \'SERVER_SCOPE\', [\'latitude\', \'longitude\']).pipe(widgetContext.rxjs.map(function(attributeData) {\n' +
+ ' var res = [0,0];\n' +
+ ' if (attributeData && attributeData.length === 2) {\n' +
+ ' res[0] = attributeData.filter(function (data) { return data.key === \'latitude\'})[0].value;\n' +
+ ' res[1] = attributeData.filter(function (data) { return data.key === \'longitude\'})[0].value;\n' +
+ ' }\n' +
+ ' return res;\n' +
+ ' }));\n' +
+ ' } else {\n' +
+ ' return [0,0];\n' +
+ ' }\n' +
+ '}\n';
+
+const getPhoneNumberFunctionTemplate =
+ '// Function body that should return phone number for further processing by mobile action.\n' +
+ '// Usually phone number can be obtained from entity attributes/telemetry. \n\n' +
+ 'return getPhoneNumberFromEntityAttributes();\n' +
+ '//return 123456789; // TEST PHONE NUMBER\n' +
+ '\n' +
+ '\n' +
+ 'function getPhoneNumberFromEntityAttributes() {\n' +
+ ' if (entityId) {\n' +
+ ' return widgetContext.attributeService.getEntityAttributes(entityId, \'SERVER_SCOPE\', [\'phone\']).pipe(widgetContext.rxjs.map(function(attributeData) {\n' +
+ ' var res = 0;\n' +
+ ' if (attributeData && attributeData.length === 1) {\n' +
+ ' res = attributeData[0].value;\n' +
+ ' }\n' +
+ ' return res;\n' +
+ ' }));\n' +
+ ' } else {\n' +
+ ' return 0;\n' +
+ ' }\n' +
+ '}\n';
+
+export function getDefaultProcessImageFunction(type: WidgetMobileActionType): string {
+ let title;
+ switch (type) {
+ case WidgetMobileActionType.takePictureFromGallery:
+ title = 'Gallery picture';
+ break;
+ case WidgetMobileActionType.takePhoto:
+ title = 'Photo';
+ break;
+ case WidgetMobileActionType.takeScreenshot:
+ title = 'Screenshot';
+ break;
+ }
+ return processImageFunctionTemplate.replace('--TITLE--', title);
+}
+
+export function getDefaultProcessLaunchResultFunction(type: WidgetMobileActionType): string {
+ let title;
+ switch (type) {
+ case WidgetMobileActionType.mapLocation:
+ title = 'Map location';
+ break;
+ case WidgetMobileActionType.mapDirection:
+ title = 'Map direction';
+ break;
+ case WidgetMobileActionType.makePhoneCall:
+ title = 'Phone call';
+ break;
+ }
+ return processLaunchResultFunctionTemplate.replace('--TITLE--', title);
+}
+
+export function getDefaultProcessQrCodeFunction() {
+ return processQrCodeFunction;
+}
+
+export function getDefaultProcessLocationFunction() {
+ return processLocationFunction;
+}
+
+export function getDefaultGetLocationFunction() {
+ return getLocationFunctionTemplate;
+}
+
+export function getDefaultGetPhoneNumberFunction() {
+ return getPhoneNumberFunctionTemplate;
+}
+
+export function getDefaultHandleEmptyResultFunction(type: WidgetMobileActionType): string {
+ let message = 'Mobile action was cancelled!';
+ switch (type) {
+ case WidgetMobileActionType.takePictureFromGallery:
+ message = 'Take picture from gallery action was cancelled!';
+ break;
+ case WidgetMobileActionType.takePhoto:
+ message = 'Take photo action was cancelled!';
+ break;
+ case WidgetMobileActionType.mapDirection:
+ message = 'Open map directions was not invoked!';
+ break;
+ case WidgetMobileActionType.mapLocation:
+ message = 'Open location on map was not invoked!';
+ break;
+ case WidgetMobileActionType.scanQrCode:
+ message = 'Scan QR code action was canceled!';
+ break;
+ case WidgetMobileActionType.makePhoneCall:
+ message = 'Phone call was not invoked!';
+ break;
+ case WidgetMobileActionType.getLocation:
+ message = 'Get location action was canceled!';
+ break;
+ case WidgetMobileActionType.takeScreenshot:
+ message = 'Take screenshot action was cancelled!';
+ break;
+ }
+ return handleEmptyResultFunctionTemplate.replace('--MESSAGE--', message);
+}
+
+export function getDefaultHandleErrorFunction(type: WidgetMobileActionType): string {
+ let title = 'Mobile action failed';
+ switch (type) {
+ case WidgetMobileActionType.takePictureFromGallery:
+ title = 'Failed to take picture from gallery';
+ break;
+ case WidgetMobileActionType.takePhoto:
+ title = 'Failed to take photo';
+ break;
+ case WidgetMobileActionType.mapDirection:
+ title = 'Failed to open map directions';
+ break;
+ case WidgetMobileActionType.mapLocation:
+ title = 'Failed to open map location';
+ break;
+ case WidgetMobileActionType.scanQrCode:
+ title = 'Failed to scan QR code';
+ break;
+ case WidgetMobileActionType.makePhoneCall:
+ title = 'Failed to make phone call';
+ break;
+ case WidgetMobileActionType.getLocation:
+ title = 'Failed to get phone location';
+ break;
+ case WidgetMobileActionType.takeScreenshot:
+ title = 'Failed to take screenshot';
+ break;
+ }
+ return handleErrorFunctionTemplate.replace('--TITLE--', title);
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html
index 33678c88b3..44bfe44e1e 100644
--- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html
+++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html
@@ -180,6 +180,11 @@
formControlName="customAction">
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts
index c2fc1e3cfe..0227ded562 100644
--- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts
@@ -45,6 +45,7 @@ import { Dashboard } from '@shared/models/dashboard.models';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { CustomActionEditorCompleter } from '@home/components/widget/action/custom-action.models';
import { isDefinedAndNotNull } from '@core/utils';
+import { MobileActionEditorComponent } from '@home/components/widget/action/mobile-action-editor.component';
export interface WidgetActionDialogData {
isAdd: boolean;
@@ -64,6 +65,8 @@ export class WidgetActionDialogComponent extends DialogComponent
;
+ switch (type) {
+ case WidgetMobileActionType.takePictureFromGallery:
+ case WidgetMobileActionType.takePhoto:
+ case WidgetMobileActionType.scanQrCode:
+ case WidgetMobileActionType.getLocation:
+ case WidgetMobileActionType.takeScreenshot:
+ argsObservable = of([]);
+ break;
+ case WidgetMobileActionType.mapDirection:
+ case WidgetMobileActionType.mapLocation:
+ const getLocationFunctionString = mobileAction.getLocationFunction;
+ const getLocationFunction = new Function('$event', 'widgetContext', 'entityId',
+ 'entityName', 'additionalParams', 'entityLabel', getLocationFunctionString);
+ const locationArgs = getLocationFunction($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel);
+ if (locationArgs && locationArgs instanceof Observable) {
+ argsObservable = locationArgs;
+ } else {
+ argsObservable = of(locationArgs);
+ }
+ argsObservable = argsObservable.pipe(map(latLng => {
+ let valid = false;
+ if (Array.isArray(latLng) && latLng.length === 2) {
+ if (typeof latLng[0] === 'number' && typeof latLng[1] === 'number') {
+ valid = true;
+ }
+ }
+ if (valid) {
+ return latLng;
+ } else {
+ throw new Error('Location function did not return valid array of latitude/longitude!');
+ }
+ }));
+ break;
+ case WidgetMobileActionType.makePhoneCall:
+ const getPhoneNumberFunctionString = mobileAction.getPhoneNumberFunction;
+ const getPhoneNumberFunction = new Function('$event', 'widgetContext', 'entityId',
+ 'entityName', 'additionalParams', 'entityLabel', getPhoneNumberFunctionString);
+ const phoneNumberArg = getPhoneNumberFunction($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel);
+ if (phoneNumberArg && phoneNumberArg instanceof Observable) {
+ argsObservable = phoneNumberArg.pipe(map(phoneNumber => [phoneNumber]));
+ } else {
+ argsObservable = of([phoneNumberArg]);
+ }
+ argsObservable = argsObservable.pipe(map(phoneNumberArr => {
+ let valid = false;
+ if (Array.isArray(phoneNumberArr) && phoneNumberArr.length === 1) {
+ if (phoneNumberArr[0] !== null) {
+ valid = true;
+ }
+ }
+ if (valid) {
+ return phoneNumberArr;
+ } else {
+ throw new Error('Phone number function did not return valid number!');
+ }
+ }));
+ break;
+ }
+ argsObservable.subscribe((args) => {
+ this.mobileService.handleWidgetMobileAction(type, ...args).subscribe(
+ (result) => {
+ if (result) {
+ if (result.hasError) {
+ this.handleWidgetMobileActionError(result.error, $event, mobileAction, entityId, entityName, additionalParams, entityLabel);
+ } else if (result.hasResult) {
+ const actionResult = result.result;
+ switch (type) {
+ case WidgetMobileActionType.takePictureFromGallery:
+ case WidgetMobileActionType.takePhoto:
+ case WidgetMobileActionType.takeScreenshot:
+ const imageUrl = actionResult.imageUrl;
+ if (mobileAction.processImageFunction && mobileAction.processImageFunction.length) {
+ try {
+ const processImageFunction = new Function('imageUrl', '$event', 'widgetContext', 'entityId',
+ 'entityName', 'additionalParams', 'entityLabel', mobileAction.processImageFunction);
+ processImageFunction(imageUrl, $event, this.widgetContext, entityId, entityName, additionalParams, entityLabel);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ break;
+ case WidgetMobileActionType.scanQrCode:
+ const code = actionResult.code;
+ const format = actionResult.format;
+ if (mobileAction.processQrCodeFunction && mobileAction.processQrCodeFunction.length) {
+ try {
+ const processQrCodeFunction = new Function('code', 'format', '$event', 'widgetContext', 'entityId',
+ 'entityName', 'additionalParams', 'entityLabel', mobileAction.processQrCodeFunction);
+ processQrCodeFunction(code, format, $event, this.widgetContext, entityId, entityName, additionalParams, entityLabel);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ break;
+ case WidgetMobileActionType.getLocation:
+ const latitude = actionResult.latitude;
+ const longitude = actionResult.longitude;
+ if (mobileAction.processLocationFunction && mobileAction.processLocationFunction.length) {
+ try {
+ const processLocationFunction = new Function('latitude', 'longitude', '$event', 'widgetContext', 'entityId',
+ 'entityName', 'additionalParams', 'entityLabel', mobileAction.processLocationFunction);
+ processLocationFunction(latitude, longitude, $event, this.widgetContext,
+ entityId, entityName, additionalParams, entityLabel);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ break;
+ case WidgetMobileActionType.mapDirection:
+ case WidgetMobileActionType.mapLocation:
+ case WidgetMobileActionType.makePhoneCall:
+ const launched = actionResult.launched;
+ if (mobileAction.processLaunchResultFunction && mobileAction.processLaunchResultFunction.length) {
+ try {
+ const processLaunchResultFunction = new Function('launched', '$event', 'widgetContext', 'entityId',
+ 'entityName', 'additionalParams', 'entityLabel', mobileAction.processLaunchResultFunction);
+ processLaunchResultFunction(launched, $event, this.widgetContext,
+ entityId, entityName, additionalParams, entityLabel);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ break;
+ }
+ } else {
+ if (mobileAction.handleEmptyResultFunction && mobileAction.handleEmptyResultFunction.length) {
+ try {
+ const handleEmptyResultFunction = new Function('$event', 'widgetContext', 'entityId',
+ 'entityName', 'additionalParams', 'entityLabel', mobileAction.handleEmptyResultFunction);
+ handleEmptyResultFunction($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ }
+ }
+ }
+ );
+ },
+ (err) => {
+ let errorMessage;
+ if (err && typeof err === 'string') {
+ errorMessage = err;
+ } else if (err && err.message) {
+ errorMessage = err.message;
+ }
+ errorMessage = `Failed to get mobile action arguments${errorMessage ? `: ${errorMessage}` : '!'}`;
+ this.handleWidgetMobileActionError(errorMessage, $event, mobileAction, entityId, entityName, additionalParams, entityLabel);
+ });
+ }
+
+ private handleWidgetMobileActionError(error: string, $event: Event, mobileAction: WidgetMobileActionDescriptor,
+ entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) {
+ if (mobileAction.handleErrorFunction && mobileAction.handleErrorFunction.length) {
+ try {
+ const handleErrorFunction = new Function('error', '$event', 'widgetContext', 'entityId',
+ 'entityName', 'additionalParams', 'entityLabel', mobileAction.handleErrorFunction);
+ handleErrorFunction(error, $event, this.widgetContext, entityId, entityName, additionalParams, entityLabel);
+ } catch (e) {
+ console.error(e);
+ }
}
}
diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts
index 74b6496c67..64fbe59efa 100644
--- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts
+++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts
@@ -77,6 +77,7 @@ import { PageLink } from '@shared/models/page/page-link';
import { SortOrder } from '@shared/models/page/sort-order';
import { DomSanitizer } from '@angular/platform-browser';
import { Router } from '@angular/router';
+import { map, mergeMap } from 'rxjs/operators';
export interface IWidgetAction {
name: string;
@@ -238,7 +239,9 @@ export class WidgetContext {
rxjs = {
forkJoin,
- of
+ of,
+ map,
+ mergeMap
};
showSuccessToast(message: string, duration: number = 1000,
diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts
index 2d599bdc78..edf0eb803a 100644
--- a/ui-ngx/src/app/shared/models/widget.models.ts
+++ b/ui-ngx/src/app/shared/models/widget.models.ts
@@ -332,7 +332,19 @@ export enum WidgetActionType {
updateDashboardState = 'updateDashboardState',
openDashboard = 'openDashboard',
custom = 'custom',
- customPretty = 'customPretty'
+ customPretty = 'customPretty',
+ mobileAction = 'mobileAction'
+}
+
+export enum WidgetMobileActionType {
+ takePictureFromGallery = 'takePictureFromGallery',
+ takePhoto = 'takePhoto',
+ mapDirection = 'mapDirection',
+ mapLocation = 'mapLocation',
+ scanQrCode = 'scanQrCode',
+ makePhoneCall = 'makePhoneCall',
+ getLocation = 'getLocation',
+ takeScreenshot = 'takeScreenshot'
}
export const widgetActionTypeTranslationMap = new Map(
@@ -341,10 +353,90 @@ export const widgetActionTypeTranslationMap = new Map(
[ WidgetActionType.updateDashboardState, 'widget-action.update-dashboard-state' ],
[ WidgetActionType.openDashboard, 'widget-action.open-dashboard' ],
[ WidgetActionType.custom, 'widget-action.custom' ],
- [ WidgetActionType.customPretty, 'widget-action.custom-pretty' ]
+ [ WidgetActionType.customPretty, 'widget-action.custom-pretty' ],
+ [ WidgetActionType.mobileAction, 'widget-action.mobile-action' ]
+ ]
+);
+
+export const widgetMobileActionTypeTranslationMap = new Map(
+ [
+ [ WidgetMobileActionType.takePictureFromGallery, 'widget-action.mobile.take-picture-from-gallery' ],
+ [ WidgetMobileActionType.takePhoto, 'widget-action.mobile.take-photo' ],
+ [ WidgetMobileActionType.mapDirection, 'widget-action.mobile.map-direction' ],
+ [ WidgetMobileActionType.mapLocation, 'widget-action.mobile.map-location' ],
+ [ WidgetMobileActionType.scanQrCode, 'widget-action.mobile.scan-qr-code' ],
+ [ WidgetMobileActionType.makePhoneCall, 'widget-action.mobile.make-phone-call' ],
+ [ WidgetMobileActionType.getLocation, 'widget-action.mobile.get-location' ],
+ [ WidgetMobileActionType.takeScreenshot, 'widget-action.mobile.take-screenshot' ]
]
);
+export interface MobileLaunchResult {
+ launched: boolean;
+}
+
+export interface MobileImageResult {
+ imageUrl: string;
+}
+
+export interface MobileQrCodeResult {
+ code: string;
+ format: string;
+}
+
+export interface MobileLocationResult {
+ latitude: number;
+ longitude: number;
+}
+
+export type MobileActionResult = MobileLaunchResult &
+ MobileImageResult &
+ MobileQrCodeResult &
+ MobileLocationResult;
+
+export interface WidgetMobileActionResult {
+ result?: T;
+ hasResult: boolean;
+ error?: string;
+ hasError: boolean;
+}
+
+export interface ProcessImageDescriptor {
+ processImageFunction: string;
+}
+
+export interface ProcessLaunchResultDescriptor {
+ processLaunchResultFunction?: string;
+}
+
+export interface LaunchMapDescriptor extends ProcessLaunchResultDescriptor {
+ getLocationFunction: string;
+}
+
+export interface ScanQrCodeDescriptor {
+ processQrCodeFunction: string;
+}
+
+export interface MakePhoneCallDescriptor extends ProcessLaunchResultDescriptor {
+ getPhoneNumberFunction: string;
+}
+
+export interface GetLocationDescriptor {
+ processLocationFunction: string;
+}
+
+export type WidgetMobileActionDescriptors = ProcessImageDescriptor &
+ LaunchMapDescriptor &
+ ScanQrCodeDescriptor &
+ MakePhoneCallDescriptor &
+ GetLocationDescriptor;
+
+export interface WidgetMobileActionDescriptor extends WidgetMobileActionDescriptors {
+ type: WidgetMobileActionType;
+ handleErrorFunction?: string;
+ handleEmptyResultFunction?: string;
+}
+
export interface CustomActionDescriptor {
customFunction?: string;
customResources?: Array;
@@ -369,6 +461,7 @@ export interface WidgetActionDescriptor extends CustomActionDescriptor {
dialogHeight?: number;
setEntityId?: boolean;
stateEntityParamName?: string;
+ mobileAction?: WidgetMobileActionDescriptor;
}
export interface WidgetComparisonSettings {
diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json
index 57727fa40f..f664dc27a7 100644
--- a/ui-ngx/src/assets/locale/locale.constant-en_US.json
+++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json
@@ -2721,6 +2721,7 @@
"open-dashboard": "Navigate to other dashboard",
"custom": "Custom action",
"custom-pretty": "Custom action (with HTML template)",
+ "mobile-action": "Mobile action",
"target-dashboard-state": "Target dashboard state",
"target-dashboard-state-required": "Target dashboard state is required",
"set-entity-from-widget": "Set entity from widget",
@@ -2732,7 +2733,19 @@
"dialog-width": "Dialog width in percents relative to viewport width",
"dialog-height": "Dialog height in percents relative to viewport height",
"dialog-size-range-error": "Dialog size percent value should be in a range from 1 to 100.",
- "open-new-browser-tab": "Open in a new browser tab"
+ "open-new-browser-tab": "Open in a new browser tab",
+ "mobile": {
+ "action-type": "Mobile action type",
+ "action-type-required": "Mobile action type is required",
+ "take-picture-from-gallery": "Take picture from gallery",
+ "take-photo": "Take photo",
+ "map-direction": "Open map directions",
+ "map-location": "Open map location",
+ "scan-qr-code": "Scan QR Code",
+ "make-phone-call": "Make phone call",
+ "get-location": "Get phone location",
+ "take-screenshot": "Take screenshot"
+ }
},
"widgets-bundle": {
"current": "Current bundle",