diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html index c3194676cf..5f1954adab 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html @@ -35,117 +35,73 @@ - - - - - - - - - - - - - - - - - - - - - - + + @if (mobileActionFormGroup.get('type').value === mobileActionType.takePhoto || + mobileActionFormGroup.get('type').value === mobileActionType.takePictureFromGallery || + mobileActionFormGroup.get('type').value === mobileActionType.takeScreenshot) { +
+ + {{ 'widget-action.mobile.save-to-gallery' | translate }} + +
+ } + @if (mobileActionFormGroup.get('type').value === mobileActionType.deviceProvision) { +
+
{{ 'widget-action.mobile.provision-type' | translate }}*
+ + + + {{ provisionTypeTranslationMap.get(type) | translate }} + + + +
+ } + + @for (config of actionConfig; track config.formControlName) { +
+ + + + {{ config.title | translate }} + + + + +
+ }
- - + + @if(mobileActionFormGroup.get('type').value) { + @for (config of commonActionConfig; track config.formControlName) { +
+ + + + {{ config.title | translate }} + + + + +
+ } + } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts index 52064fdf91..c53272d75a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts @@ -24,6 +24,9 @@ import { } from '@angular/forms'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { + ActionConfig, + ProvisionType, + provisionTypeTranslationMap, WidgetActionType, WidgetMobileActionDescriptor, WidgetMobileActionType, @@ -35,6 +38,7 @@ import { getDefaultGetPhoneNumberFunction, getDefaultHandleEmptyResultFunction, getDefaultHandleErrorFunction, + getDefaultHandleNonMobileFallBackFunction, getDefaultProcessImageFunction, getDefaultProcessLaunchResultFunction, getDefaultProcessLocationFunction, @@ -68,6 +72,12 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit functionScopeVariables: string[]; + actionConfig: ActionConfig[]; + commonActionConfig: ActionConfig[]; + + provisionTypes: string[] = Object.keys(ProvisionType); + provisionTypeTranslationMap = provisionTypeTranslationMap; + private requiredValue: boolean; get required(): boolean { return this.requiredValue; @@ -99,8 +109,10 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit this.mobileActionFormGroup = this.fb.group({ type: [null, Validators.required], handleEmptyResultFunction: [null], - handleErrorFunction: [null] + handleErrorFunction: [null], + handleNonMobileFallbackFunction: [null] }); + this.getCommonActionConfigs(); this.mobileActionFormGroup.get('type').valueChanges.pipe( takeUntilDestroyed(this.destroyRef) ).subscribe((type: WidgetMobileActionType) => { @@ -109,6 +121,7 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit action = {...action, ...this.mobileActionTypeFormGroup.value}; } this.updateMobileActionType(type, action); + this.getActionConfigs(); }); this.mobileActionFormGroup.valueChanges.pipe( takeUntilDestroyed(this.destroyRef) @@ -133,10 +146,14 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit } writeValue(value: WidgetMobileActionDescriptor | null): void { - this.mobileActionFormGroup.patchValue({type: value?.type, - handleEmptyResultFunction: value?.handleEmptyResultFunction, - handleErrorFunction: value?.handleErrorFunction}, {emitEvent: false}); + this.mobileActionFormGroup.patchValue({ + type: value?.type, + handleEmptyResultFunction: value?.handleEmptyResultFunction, + handleErrorFunction: value?.handleErrorFunction, + handleNonMobileFallbackFunction: value?.handleNonMobileFallbackFunction + }, {emitEvent: false}); this.updateMobileActionType(value?.type, value); + this.getActionConfigs(); } private updateModel() { @@ -164,6 +181,12 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit handleErrorFunction = getDefaultHandleErrorFunction(type); this.mobileActionFormGroup.patchValue({handleErrorFunction}, {emitEvent: false}); } + let handleNonMobileFallbackFunction = action?.handleNonMobileFallbackFunction; + const defaultHandleNonMobileFallbackFunction = getDefaultHandleNonMobileFallBackFunction(); + if (defaultHandleNonMobileFallbackFunction !== handleNonMobileFallbackFunction) { + handleNonMobileFallbackFunction = getDefaultHandleNonMobileFallBackFunction(); + this.mobileActionFormGroup.patchValue({handleNonMobileFallbackFunction}, {emitEvent: false}); + } } this.mobileActionTypeFormGroup = this.fb.group({}); if (type) { @@ -183,6 +206,10 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit 'processImageFunction', this.fb.control(processImageFunction, []) ); + this.mobileActionTypeFormGroup.addControl( + 'saveToGallery', + this.fb.control(action?.saveToGallery || false, []) + ); break; case WidgetMobileActionType.mapDirection: case WidgetMobileActionType.mapLocation: @@ -267,6 +294,10 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit 'handleProvisionSuccessFunction', this.fb.control(handleProvisionSuccessFunction, [Validators.required]) ); + this.mobileActionTypeFormGroup.addControl( + 'provisionType', + this.fb.control(action?.provisionType || ProvisionType.auto, []) + ); } } this.mobileActionTypeFormGroup.valueChanges.pipe( @@ -276,5 +307,108 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit }); } + getActionConfigs() { + const type = this.mobileActionFormGroup.get('type').value; + this.actionConfig = []; + switch (type) { + case this.mobileActionType.deviceProvision: + this.actionConfig.push({ + title: 'widget-action.mobile.handle-provision-success-function', + formControlName: 'handleProvisionSuccessFunction', + functionName: 'handleProvisionSuccess', + functionArgs: ['deviceName', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'] + }); + break; + case this.mobileActionType.mapDirection: + case this.mobileActionType.mapLocation: + this.actionConfig.push({ + title: 'widget-action.mobile.get-location-function', + formControlName: 'getLocationFunction', + functionName: 'getLocation', + functionArgs: ['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_get_location_fn' + }); + this.actionConfig.push({ + title: 'widget-action.mobile.process-launch-result-function', + formControlName: 'processLaunchResultFunction', + functionName: 'processLaunchResult', + functionArgs: ['launched', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_process_launch_result_fn' + }); + break; + case this.mobileActionType.makePhoneCall: + this.actionConfig.push({ + title: 'widget-action.mobile.get-phone-number-function', + formControlName: 'getPhoneNumberFunction', + functionName: 'getPhoneNumber', + functionArgs: ['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_get_phone_number_fn' + }); + this.actionConfig.push({ + title: 'widget-action.mobile.process-launch-result-function', + formControlName: 'processLaunchResultFunction', + functionName: 'processLaunchResult', + functionArgs: ['launched', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_process_launch_result_fn' + }); + break; + case this.mobileActionType.takePhoto: + case this.mobileActionType.takePictureFromGallery: + case this.mobileActionType.takeScreenshot: + this.actionConfig.push({ + title: 'widget-action.mobile.process-image-function', + formControlName: 'processImageFunction', + functionName: 'processImage', + functionArgs: ['imageUrl', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_process_image_fn' + }); + break; + case this.mobileActionType.scanQrCode: + this.actionConfig.push({ + title: 'widget-action.mobile.process-qr-code-function', + formControlName: 'processQrCodeFunction', + functionName: 'processQrCode', + functionArgs: ['code', 'format', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_process_qr_code_fn' + }); + break; + case this.mobileActionType.getLocation: + this.actionConfig.push({ + title: 'widget-action.mobile.process-location-function', + formControlName: 'processLocationFunction', + functionName: 'processLocation', + functionArgs: ['latitude', 'longitude', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_process_location_fn' + }); + break; + } + } + + getCommonActionConfigs() { + this.commonActionConfig = [ + { + title: 'widget-action.mobile.handle-empty-result-function', + formControlName: 'handleEmptyResultFunction', + functionName: 'handleEmptyResult', + functionArgs: ['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_handle_empty_result_fn' + }, + { + title: 'widget-action.mobile.handle-error-function', + formControlName: 'handleErrorFunction', + functionName: 'handleError', + functionArgs: ['error', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'], + helpId: 'widget/action/mobile_handle_error_fn' + }, + { + title: 'widget-action.mobile.handle-non-mobile-fallback-function', + formControlName: 'handleNonMobileFallbackFunction', + functionName: 'handleNonMobileFallback', + functionArgs: ['$event', 'widgetContext'], + helpId: 'widget/action/mobile_handle_non_mobile_fallback_fn' + } + ]; + } + protected readonly WidgetActionType = WidgetActionType; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts index 0d4c46c589..68a97bf779 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts @@ -172,6 +172,14 @@ const handleErrorFunctionTemplate: TbFunction = ' }, 100);\n' + '}\n'; +const handleNonMobileFallbackFunctionTemplate: TbFunction = + '// Optional function body to handle non-mobile fallback \n' + + 'showFallbackToast();\n' + + '\n' + + 'function showFallbackToast(title, error) {\n' + + ' widgetContext.showWarnToast(\'This action is only available in the mobile application.\');\n' + + '}\n'; + const getLocationFunctionTemplate: TbFunction = '// 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' + @@ -326,3 +334,5 @@ export const getDefaultHandleErrorFunction = (type: WidgetMobileActionType): TbF } return handleErrorFunctionTemplate.replace('--TITLE--', title); }; + +export const getDefaultHandleNonMobileFallBackFunction = () => handleNonMobileFallbackFunctionTemplate; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index fed2615bbd..6ab13fc40b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts @@ -38,6 +38,7 @@ import { } from '@angular/core'; import { DashboardWidget } from '@home/models/dashboard-component.models'; import { + MobileImageResult, Widget, WidgetAction, WidgetActionDescriptor, @@ -126,6 +127,7 @@ import { IModulesMap } from '@modules/common/modules-map.models'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { CompiledTbFunction, compileTbFunction, isNotEmptyTbFunction } from '@shared/models/js-function.models'; import { HttpClient } from '@angular/common/http'; +import { addDiagnosticChain } from '@angular/compiler-cli/src/ngtsc/diagnostics'; @Component({ selector: 'tb-widget', @@ -1222,12 +1224,16 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, switch (type) { case WidgetMobileActionType.takePictureFromGallery: case WidgetMobileActionType.takePhoto: + case WidgetMobileActionType.takeScreenshot: + argsObservable = of([mobileAction.saveToGallery]); + break; case WidgetMobileActionType.scanQrCode: case WidgetMobileActionType.getLocation: - case WidgetMobileActionType.takeScreenshot: - case WidgetMobileActionType.deviceProvision: argsObservable = of([]); break; + case WidgetMobileActionType.deviceProvision: + argsObservable = of([mobileAction.provisionType]); + break; case WidgetMobileActionType.mapDirection: case WidgetMobileActionType.mapLocation: argsObservable = compileTbFunction(this.http, mobileAction.getLocationFunction, '$event', 'widgetContext', 'entityId', @@ -1297,6 +1303,10 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, case WidgetMobileActionType.takePhoto: case WidgetMobileActionType.takeScreenshot: const imageUrl = actionResult.imageUrl; + if (!additionalParams) { + additionalParams = {}; + } + additionalParams.imageInfo = actionResult.imageInfo; if (isNotEmptyTbFunction(mobileAction.processImageFunction)) { compileTbFunction(this.http, mobileAction.processImageFunction, 'imageUrl', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel').subscribe( @@ -1421,6 +1431,23 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges, ); } } + } else if (!this.mobileService.isMobileApp()) { + if (isNotEmptyTbFunction(mobileAction.handleNonMobileFallbackFunction)) { + compileTbFunction(this.http, mobileAction.handleNonMobileFallbackFunction, '$event', 'widgetContext',).subscribe( + { + next: (compiled) => { + try { + compiled.execute($event, this.widgetContext); + } catch (e) { + console.error(e); + } + }, + error: (err) => { + console.error(err); + } + } + ); + } } } ); diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 00b62cfb72..4c37540d07 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -33,7 +33,7 @@ import { PageComponent } from '@shared/components/page.component'; import { AfterViewInit, DestroyRef, Directive, EventEmitter, inject, Inject, OnInit, Type } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { AbstractControl, UntypedFormGroup } from '@angular/forms'; +import { AbstractControl, UntypedFormGroup, ValidatorFn } from '@angular/forms'; import { Observable } from 'rxjs'; import { Dashboard } from '@shared/models/dashboard.models'; import { IAliasController } from '@core/api/widget-api.models'; @@ -51,6 +51,7 @@ import { TbFunction } from '@shared/models/js-function.models'; import { FormProperty, jsonFormSchemaToFormProperties } from '@shared/models/dynamic-form.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { TbUnit } from '@shared/models/unit.models'; +import { ImageResourceInfo } from '@shared/models/resource.models'; export enum widgetType { timeseries = 'timeseries', @@ -622,6 +623,30 @@ export enum WidgetMobileActionType { deviceProvision = 'deviceProvision', } +export interface ActionConfig { + title: string, + formControlName: string, + functionName: string, + functionArgs: string[], + helpId?: string +} + +export enum ProvisionType { + auto = 'auto', + wiFi = 'wiFi', + ble = 'ble', + softAp = 'softAp' +} + +export const provisionTypeTranslationMap = new Map( + [ + [ ProvisionType.auto, 'widget-action.mobile.auto' ], + [ ProvisionType.wiFi, 'widget-action.mobile.wi-fi' ], + [ ProvisionType.ble, 'widget-action.mobile.ble' ], + [ ProvisionType.softAp, 'widget-action.mobile.soft-ap' ], + ] +); + export enum MapItemType { marker = 'marker', polygon = 'polygon', @@ -675,6 +700,7 @@ export interface MobileLaunchResult { export interface MobileImageResult { imageUrl: string; + imageInfo?: ImageResourceInfo; } export interface MobileQrCodeResult { @@ -706,10 +732,12 @@ export interface WidgetMobileActionResult { export interface ProvisionSuccessDescriptor { handleProvisionSuccessFunction: TbFunction; + provisionType?: string; } export interface ProcessImageDescriptor { processImageFunction: TbFunction; + saveToGallery?: boolean; } export interface ProcessLaunchResultDescriptor { @@ -743,6 +771,7 @@ export interface WidgetMobileActionDescriptor extends WidgetMobileActionDescript type: WidgetMobileActionType; handleErrorFunction?: TbFunction; handleEmptyResultFunction?: TbFunction; + handleNonMobileFallbackFunction?: TbFunction; } export interface CustomActionDescriptor { 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 b06137eba6..850da1f3e3 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6868,7 +6868,23 @@ "scan-qr-code": "Scan QR Code", "make-phone-call": "Make phone call", "get-location": "Get phone location", - "take-screenshot": "Take screenshot" + "take-screenshot": "Take screenshot", + "handle-provision-success-function": "Handle provision success function", + "get-location-function": "Get location function", + "process-launch-result-function": "Process launch result function", + "get-phone-number-function": "Get phone number function", + "process-image-function": "Process image function", + "process-qr-code-function": "Process QR code function", + "process-location-function": "Process location function", + "handle-empty-result-function": "Handle empty result function", + "handle-error-function": "Handle error function", + "handle-non-mobile-fallback-function": "Handle Non-Mobile fallback function", + "save-to-gallery": "Save to gallery", + "provision-type": "Provision type", + "auto": "Auto", + "wi-fi": "Wi-Fi", + "ble": "BLE", + "soft-ap": "Soft AP" }, "custom-action-function": "Custom action function", "custom-pretty-function": "Custom action (with HTML template) function",