UI: Add JS modules support for mobile action functions.

This commit is contained in:
Igor Kulikov 2024-12-03 13:23:22 +02:00
parent a411213d6c
commit 3db9d68c12
5 changed files with 225 additions and 157 deletions

View File

@ -42,6 +42,7 @@
<tb-js-func
formControlName="getLocationFunction"
functionName="getLocation"
withModules
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
@ -53,6 +54,7 @@
<tb-js-func
formControlName="getPhoneNumberFunction"
functionName="getPhoneNumber"
withModules
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
@ -67,6 +69,7 @@
<tb-js-func
formControlName="processImageFunction"
functionName="processImage"
withModules
[functionArgs]="['imageUrl', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
@ -78,6 +81,7 @@
<tb-js-func
formControlName="processQrCodeFunction"
functionName="processQrCode"
withModules
[functionArgs]="['code', 'format', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
@ -89,6 +93,7 @@
<tb-js-func
formControlName="processLocationFunction"
functionName="processLocation"
withModules
[functionArgs]="['latitude', 'longitude', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
@ -103,6 +108,7 @@
<tb-js-func
formControlName="processLaunchResultFunction"
functionName="processLaunchResult"
withModules
[functionArgs]="['launched', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
@ -114,6 +120,7 @@
<tb-js-func *ngIf="mobileActionFormGroup.get('type').value"
formControlName="handleEmptyResultFunction"
functionName="handleEmptyResult"
withModules
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
@ -123,6 +130,7 @@
<tb-js-func *ngIf="mobileActionFormGroup.get('type').value"
formControlName="handleErrorFunction"
functionName="handleError"
withModules
[functionArgs]="['error', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"

View File

@ -41,6 +41,7 @@ import {
getDefaultProcessQrCodeFunction
} from '@home/components/widget/lib/settings/common/action/mobile-action-editor.models';
import { WidgetService } from '@core/http/widget.service';
import { TbFunction } from '@shared/models/js-function.models';
@Component({
selector: 'tb-mobile-action-editor',
@ -159,7 +160,7 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit
}
this.mobileActionTypeFormGroup = this.fb.group({});
if (type) {
let processLaunchResultFunction: string;
let processLaunchResultFunction: TbFunction;
switch (type) {
case WidgetMobileActionType.takePictureFromGallery:
case WidgetMobileActionType.takePhoto:

View File

@ -15,8 +15,9 @@
///
import { WidgetMobileActionType } from '@shared/models/widget.models';
import { TbFunction } from '@shared/models/js-function.models';
const processImageFunctionTemplate =
const processImageFunctionTemplate: TbFunction =
'// 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' +
@ -82,7 +83,7 @@ const processImageFunctionTemplate =
' }\n' +
'}\n';
const processLaunchResultFunctionTemplate =
const processLaunchResultFunctionTemplate: TbFunction =
// eslint-disable-next-line max-len
'// 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' +
@ -94,7 +95,7 @@ const processLaunchResultFunctionTemplate =
' }, 100);\n' +
'}\n';
const processQrCodeFunction =
const processQrCodeFunction: TbFunction =
'// Function body to process result of QR code scanning. \n' +
'// - code - scanned QR code\n' +
'// - format - scanned QR code format\n\n' +
@ -106,7 +107,7 @@ const processQrCodeFunction =
' }, 100);\n' +
'}\n';
const processLocationFunction =
const processLocationFunction: TbFunction =
'// Function body to process current location of the phone. \n' +
'// - latitude - phone location latitude\n' +
'// - longitude - phone location longitude\n\n' +
@ -137,7 +138,7 @@ const processLocationFunction =
' }, 100);\n' +
'}';
const handleEmptyResultFunctionTemplate =
const handleEmptyResultFunctionTemplate: TbFunction =
'// 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' +
@ -148,7 +149,7 @@ const handleEmptyResultFunctionTemplate =
' }, 100);\n' +
'}\n';
const handleErrorFunctionTemplate =
const handleErrorFunctionTemplate: TbFunction =
'// Optional function body to handle error occurred while mobile action execution \n' +
'// - error - Error message\n\n' +
'showErrorDialog(\'--TITLE--\', error);\n' +
@ -159,7 +160,7 @@ const handleErrorFunctionTemplate =
' }, 100);\n' +
'}\n';
const getLocationFunctionTemplate =
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' +
'return getLocationFromEntityAttributes();\n' +
@ -182,7 +183,7 @@ const getLocationFunctionTemplate =
' }\n' +
'}\n';
const getPhoneNumberFunctionTemplate =
const getPhoneNumberFunctionTemplate: TbFunction =
'// 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' +
@ -204,7 +205,7 @@ const getPhoneNumberFunctionTemplate =
' }\n' +
'}\n';
export const getDefaultProcessImageFunction = (type: WidgetMobileActionType): string => {
export const getDefaultProcessImageFunction = (type: WidgetMobileActionType): TbFunction => {
let title: string;
switch (type) {
case WidgetMobileActionType.takePictureFromGallery:
@ -220,7 +221,7 @@ export const getDefaultProcessImageFunction = (type: WidgetMobileActionType): st
return processImageFunctionTemplate.replace('--TITLE--', title);
};
export const getDefaultProcessLaunchResultFunction = (type: WidgetMobileActionType): string => {
export const getDefaultProcessLaunchResultFunction = (type: WidgetMobileActionType): TbFunction => {
let title: string;
switch (type) {
case WidgetMobileActionType.mapLocation:
@ -244,7 +245,7 @@ export const getDefaultGetLocationFunction = () => getLocationFunctionTemplate;
export const getDefaultGetPhoneNumberFunction = () => getPhoneNumberFunctionTemplate;
export const getDefaultHandleEmptyResultFunction = (type: WidgetMobileActionType): string => {
export const getDefaultHandleEmptyResultFunction = (type: WidgetMobileActionType): TbFunction => {
let message = 'Mobile action was cancelled!';
switch (type) {
case WidgetMobileActionType.takePictureFromGallery:
@ -275,7 +276,7 @@ export const getDefaultHandleEmptyResultFunction = (type: WidgetMobileActionType
return handleEmptyResultFunctionTemplate.replace('--MESSAGE--', message);
};
export const getDefaultHandleErrorFunction = (type: WidgetMobileActionType): string => {
export const getDefaultHandleErrorFunction = (type: WidgetMobileActionType): TbFunction => {
let title = 'Mobile action failed';
switch (type) {
case WidgetMobileActionType.takePictureFromGallery:

View File

@ -70,9 +70,12 @@ import {
IDynamicWidgetComponent,
ShowWidgetHeaderActionFunction,
updateEntityParams,
WidgetContext, widgetContextToken, widgetErrorMessagesToken,
WidgetContext,
widgetContextToken,
widgetErrorMessagesToken,
WidgetHeaderAction,
WidgetInfo, widgetTitlePanelToken,
WidgetInfo,
widgetTitlePanelToken,
WidgetTypeInstance
} from '@home/models/widget-component.models';
import {
@ -1204,157 +1207,212 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
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;
argsObservable = compileTbFunction(this.http, mobileAction.getLocationFunction, '$event', 'widgetContext', 'entityId',
'entityName', 'additionalParams', 'entityLabel').pipe(
switchMap(getLocationFunction => {
const locationArgs = getLocationFunction.execute($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel);
if (locationArgs && locationArgs instanceof Observable) {
return locationArgs;
} else {
return of(locationArgs);
}
}
if (valid) {
return latLng;
} else {
throw new Error('Location function did not return valid array of latitude/longitude!');
}
}));
}),
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;
argsObservable = compileTbFunction(this.http, mobileAction.getPhoneNumberFunction, '$event', 'widgetContext', 'entityId',
'entityName', 'additionalParams', 'entityLabel').pipe(
switchMap(getPhoneNumberFunction => {
const phoneNumberArg = getPhoneNumberFunction.execute($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel);
if (phoneNumberArg && phoneNumberArg instanceof Observable) {
return phoneNumberArg.pipe(map(phoneNumber => [phoneNumber]));
} else {
return of([phoneNumberArg]);
}
}
if (valid) {
return phoneNumberArr;
} else {
throw new Error('Phone number function did not return valid number!');
}
}));
}),
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);
}
argsObservable.subscribe(
{
next: (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 (isNotEmptyTbFunction(mobileAction.processImageFunction)) {
compileTbFunction(this.http, mobileAction.processImageFunction, 'imageUrl', '$event', 'widgetContext', 'entityId',
'entityName', 'additionalParams', 'entityLabel').subscribe(
{
next: (compiled) => {
try {
compiled.execute(imageUrl, $event, this.widgetContext, entityId, entityName, additionalParams, entityLabel);
} catch (e) {
console.error(e);
}
},
error: (err) => {
console.error(err);
}
}
);
}
break;
case WidgetMobileActionType.scanQrCode:
const code = actionResult.code;
const format = actionResult.format;
if (isNotEmptyTbFunction(mobileAction.processQrCodeFunction)) {
compileTbFunction(this.http, mobileAction.processQrCodeFunction, 'code', 'format', '$event', 'widgetContext', 'entityId',
'entityName', 'additionalParams', 'entityLabel').subscribe(
{
next: (compiled) => {
try {
compiled.execute(code, format, $event, this.widgetContext, entityId, entityName, additionalParams, entityLabel);
} catch (e) {
console.error(e);
}
},
error: (err) => {
console.error(err);
}
}
);
}
break;
case WidgetMobileActionType.getLocation:
const latitude = actionResult.latitude;
const longitude = actionResult.longitude;
if (isNotEmptyTbFunction(mobileAction.processLocationFunction)) {
compileTbFunction(this.http, mobileAction.processLocationFunction, 'latitude', 'longitude', '$event', 'widgetContext', 'entityId',
'entityName', 'additionalParams', 'entityLabel').subscribe(
{
next: (compiled) => {
try {
compiled.execute(latitude, longitude, $event, this.widgetContext,
entityId, entityName, additionalParams, entityLabel);
} catch (e) {
console.error(e);
}
},
error: (err) => {
console.error(err);
}
}
);
}
break;
case WidgetMobileActionType.mapDirection:
case WidgetMobileActionType.mapLocation:
case WidgetMobileActionType.makePhoneCall:
const launched = actionResult.launched;
if (isNotEmptyTbFunction(mobileAction.processLaunchResultFunction)) {
compileTbFunction(this.http, mobileAction.processLaunchResultFunction, 'launched', '$event', 'widgetContext', 'entityId',
'entityName', 'additionalParams', 'entityLabel').subscribe(
{
next: (compiled) => {
try {
compiled.execute(launched, $event, this.widgetContext,
entityId, entityName, additionalParams, entityLabel);
} catch (e) {
console.error(e);
}
},
error: (err) => {
console.error(err);
}
}
);
}
break;
}
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);
}
} else {
if (isNotEmptyTbFunction(mobileAction.handleEmptyResultFunction)) {
compileTbFunction(this.http, mobileAction.handleEmptyResultFunction, '$event', 'widgetContext', 'entityId',
'entityName', 'additionalParams', 'entityLabel').subscribe(
{
next: (compiled) => {
try {
compiled.execute($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel);
} catch (e) {
console.error(e);
}
},
error: (err) => {
console.error(err);
}
}
);
}
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);
}
}
}
);
},
error: err => {
let errorMessage: string;
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);
}
);
},
(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);
}
if (isNotEmptyTbFunction(mobileAction.handleErrorFunction)) {
compileTbFunction(this.http, mobileAction.handleErrorFunction, 'error', '$event', 'widgetContext', 'entityId',
'entityName', 'additionalParams', 'entityLabel').subscribe(
{
next: (compiled) => {
try {
compiled.execute(error, $event, this.widgetContext, entityId, entityName, additionalParams, entityLabel);
} catch (e) {
console.error(e);
}
},
error: (err) => {
console.error(err);
}
}
);
}
}

View File

@ -621,27 +621,27 @@ export interface WidgetMobileActionResult<T extends MobileActionResult> {
}
export interface ProcessImageDescriptor {
processImageFunction: string;
processImageFunction: TbFunction;
}
export interface ProcessLaunchResultDescriptor {
processLaunchResultFunction?: string;
processLaunchResultFunction?: TbFunction;
}
export interface LaunchMapDescriptor extends ProcessLaunchResultDescriptor {
getLocationFunction: string;
getLocationFunction: TbFunction;
}
export interface ScanQrCodeDescriptor {
processQrCodeFunction: string;
processQrCodeFunction: TbFunction;
}
export interface MakePhoneCallDescriptor extends ProcessLaunchResultDescriptor {
getPhoneNumberFunction: string;
getPhoneNumberFunction: TbFunction;
}
export interface GetLocationDescriptor {
processLocationFunction: string;
processLocationFunction: TbFunction;
}
export type WidgetMobileActionDescriptors = ProcessImageDescriptor &
@ -652,8 +652,8 @@ export type WidgetMobileActionDescriptors = ProcessImageDescriptor &
export interface WidgetMobileActionDescriptor extends WidgetMobileActionDescriptors {
type: WidgetMobileActionType;
handleErrorFunction?: string;
handleEmptyResultFunction?: string;
handleErrorFunction?: TbFunction;
handleEmptyResultFunction?: TbFunction;
}
export interface CustomActionDescriptor {