Browse Source

Merge with master

pull/4542/head
Andrii Shvaika 6 years ago
parent
commit
c9bdce48a1
  1. 47
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  2. 1
      common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsCertificateVerifier.java
  3. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java
  4. 20
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2MTransportMsgHandler.java
  5. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java
  6. 49
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java
  7. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java
  8. 14
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java
  9. 3
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java
  10. 8
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java
  11. 70
      ui-ngx/src/app/core/services/mobile.service.ts
  12. 6
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  13. 9
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss
  14. 23
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  15. 36
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.scss
  16. 5
      ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.ts
  17. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.ts
  18. 3
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  19. 100
      ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.html
  20. 253
      ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.ts
  21. 317
      ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.models.ts
  22. 5
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html
  23. 30
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts
  24. 182
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  25. 5
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  26. 97
      ui-ngx/src/app/shared/models/widget.models.ts
  27. 15
      ui-ngx/src/assets/locale/locale.constant-en_US.json

47
application/src/main/java/org/thingsboard/server/controller/DashboardController.java

@ -533,6 +533,37 @@ public class DashboardController extends BaseController {
}
}
@PreAuthorize("isAuthenticated()")
@RequestMapping(value = "/dashboard/home/info", method = RequestMethod.GET)
@ResponseBody
public HomeDashboardInfo getHomeDashboardInfo() throws ThingsboardException {
try {
SecurityUser securityUser = getCurrentUser();
if (securityUser.isSystemAdmin()) {
return null;
}
User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId());
JsonNode additionalInfo = user.getAdditionalInfo();
HomeDashboardInfo homeDashboardInfo;
homeDashboardInfo = extractHomeDashboardInfoFromAdditionalInfo(additionalInfo);
if (homeDashboardInfo == null) {
if (securityUser.isCustomerUser()) {
Customer customer = customerService.findCustomerById(securityUser.getTenantId(), securityUser.getCustomerId());
additionalInfo = customer.getAdditionalInfo();
homeDashboardInfo = extractHomeDashboardInfoFromAdditionalInfo(additionalInfo);
}
if (homeDashboardInfo == null) {
Tenant tenant = tenantService.findTenantById(securityUser.getTenantId());
additionalInfo = tenant.getAdditionalInfo();
homeDashboardInfo = extractHomeDashboardInfoFromAdditionalInfo(additionalInfo);
}
}
return homeDashboardInfo;
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.GET)
@ResponseBody
@ -582,6 +613,22 @@ public class DashboardController extends BaseController {
}
}
private HomeDashboardInfo extractHomeDashboardInfoFromAdditionalInfo(JsonNode additionalInfo) {
try {
if (additionalInfo != null && additionalInfo.has(HOME_DASHBOARD_ID) && !additionalInfo.get(HOME_DASHBOARD_ID).isNull()) {
String strDashboardId = additionalInfo.get(HOME_DASHBOARD_ID).asText();
DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
checkDashboardId(dashboardId, Operation.READ);
boolean hideDashboardToolbar = true;
if (additionalInfo.has(HOME_DASHBOARD_HIDE_TOOLBAR)) {
hideDashboardToolbar = additionalInfo.get(HOME_DASHBOARD_HIDE_TOOLBAR).asBoolean();
}
return new HomeDashboardInfo(dashboardId, hideDashboardToolbar);
}
} catch (Exception e) {}
return null;
}
private HomeDashboard extractHomeDashboardFromAdditionalInfo(JsonNode additionalInfo) {
try {
if (additionalInfo != null && additionalInfo.has(HOME_DASHBOARD_ID) && !additionalInfo.get(HOME_DASHBOARD_ID).isNull()) {

1
common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsCertificateVerifier.java

@ -86,6 +86,7 @@ public class TbCoapDtlsCertificateVerifier implements NewAdvancedCertificateVeri
if (!skipValidityCheckForClientCert) {
cert.checkValidity();
}
String strCert = SslUtil.getCertificateString(cert);
String sha3Hash = EncryptionUtil.getSha3Hash(strCert);
final ValidateDeviceCredentialsResponse[] deviceCredentialsResponse = new ValidateDeviceCredentialsResponse[1];

2
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java

@ -30,9 +30,9 @@ import org.eclipse.leshan.server.security.SecurityInfo;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Service;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.transport.lwm2m.secure.EndpointSecurityInfo;
import org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode;
import org.thingsboard.server.transport.lwm2m.secure.LwM2mCredentialsSecurityInfoValidator;
import org.thingsboard.server.transport.lwm2m.secure.EndpointSecurityInfo;
import org.thingsboard.server.transport.lwm2m.server.LwM2mSessionMsgListener;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContext;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServerHelper;

20
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2MTransportMsgHandler.java

@ -102,13 +102,13 @@ import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.L
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.READ;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.WRITE_ATTRIBUTES;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.WRITE_REPLACE;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.LwM2mTypeOper.WRITE_UPDATE;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertJsonArrayToSet;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromIdVerToObjectId;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromObjectIdToIdVer;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.getAckCallback;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.validateObjectVerFromKey;
@Slf4j
@Service
@TbLwM2mTransportComponent
@ -453,15 +453,17 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler
if (OBSERVE_READ_ALL != lwm2mClientRpcRequest.getTypeOper() && lwm2mClientRpcRequest.getTargetIdVer() == null) {
lwm2mClientRpcRequest.setErrorMsg(lwm2mClientRpcRequest.targetIdVerKey + " and " +
lwm2mClientRpcRequest.keyNameKey + " is null or bad format");
} else if ((EXECUTE == lwm2mClientRpcRequest.getTypeOper()
}
/**
* EXECUTE && WRITE_REPLACE - only for Resource or ResourceInstance
*/
else if ((EXECUTE == lwm2mClientRpcRequest.getTypeOper()
|| WRITE_REPLACE == lwm2mClientRpcRequest.getTypeOper())
&& lwm2mClientRpcRequest.getTargetIdVer() != null
&& !(new LwM2mPath(convertPathFromIdVerToObjectId(lwm2mClientRpcRequest.getTargetIdVer())).isResource()
|| new LwM2mPath(convertPathFromIdVerToObjectId(lwm2mClientRpcRequest.getTargetIdVer())).isResourceInstance())) {
lwm2mClientRpcRequest.setErrorMsg("Invalid parameter " + lwm2mClientRpcRequest.targetIdVerKey
+ ". Only Resource or ResourceInstance can be this operation");
} else if (WRITE_UPDATE == lwm2mClientRpcRequest.getTypeOper()) {
lwm2mClientRpcRequest.setErrorMsg("Procedures In Development...");
}
} else {
lwm2mClientRpcRequest.setErrorMsg("Params of request is bad Json format.");
@ -858,7 +860,15 @@ public class DefaultLwM2MTransportMsgHandler implements LwM2mTransportMsgHandler
* @param request -
*/
public void onWriteResponseOk(Registration registration, String path, WriteRequest request) {
this.updateResourcesValue(registration, ((LwM2mResource) request.getNode()), path);
if (request.getNode() instanceof LwM2mResource) {
this.updateResourcesValue(registration, ((LwM2mResource) request.getNode()), path);
}
else if (request.getNode() instanceof LwM2mObjectInstance) {
((LwM2mObjectInstance) request.getNode()).getResources().forEach((resId, resource) -> {
this.updateResourcesValue(registration, resource, path+ "/" + resId);
});
}
}
/**

2
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java

@ -138,6 +138,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService {
/* Create DTLS Config */
DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder();
dtlsConfig.setServerOnly(true);
dtlsConfig.setRecommendedSupportedGroupsOnly(config.isRecommendedSupportedGroups());
dtlsConfig.setRecommendedCipherSuitesOnly(config.isRecommendedCiphers());
@ -153,6 +154,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService {
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256);
}
/* Set DTLS Config */
builder.setDtlsConfig(dtlsConfig);

49
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java

@ -16,13 +16,13 @@
package org.thingsboard.server.transport.lwm2m.server;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mNode;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.node.LwM2mResource;
import org.eclipse.leshan.core.node.LwM2mSingleResource;
import org.eclipse.leshan.core.node.ObjectLink;
import org.eclipse.leshan.core.observation.Observation;
@ -57,6 +57,7 @@ import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl;
import javax.annotation.PostConstruct;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.ExecutorService;
@ -108,7 +109,7 @@ public class LwM2mTransportRequest {
* @param typeOper -
* @param contentFormatName -
*/
@SneakyThrows
public void sendAllRequest(Registration registration, String targetIdVer, LwM2mTypeOper typeOper,
String contentFormatName, Object params, long timeoutInMs, Lwm2mClientRpcRequest rpcRequest) {
try {
@ -156,8 +157,7 @@ public class LwM2mTransportRequest {
break;
case WRITE_REPLACE:
// Request to write a <b>String Single-Instance Resource</b> using the TLV content format.
resourceModel = lwM2MClient.getResourceModel(targetIdVer, this.config
.getModelProvider());
resourceModel = lwM2MClient.getResourceModel(targetIdVer, this.config.getModelProvider());
if (contentFormat.equals(ContentFormat.TLV)) {
request = this.getWriteRequestSingleResource(null, resultIds.getObjectId(),
resultIds.getObjectInstanceId(), resultIds.getResourceId(), params, resourceModel.type,
@ -171,15 +171,31 @@ public class LwM2mTransportRequest {
}
break;
case WRITE_UPDATE:
// LwM2mNode node = null;
// if (resultIds.isObjectInstance()) {
// node = new LwM2mObjectInstance(resultIds.getObjectInstanceId(), lwM2MClient.
// getNewResourcesForInstance(targetIdVer, this.lwM2mTransportContextServer.getLwM2MTransportConfigServer().getModelProvider(),
// this.converter));
// request = new WriteRequest(WriteRequest.Mode.UPDATE, contentFormat, target, node);
// } else if (resultIds.getObjectId() >= 0) {
// request = new ObserveRequest(resultIds.getObjectId());
// }
if (resultIds.isResource()) {
/**
* send request: path = '/3/0' node == wM2mObjectInstance
* with params == "\"resources\": {15: resource:{id:15. value:'+01'...}}
**/
Collection<LwM2mResource> resources = lwM2MClient.getNewResourcesForInstance(
targetIdVer, params,
this.config.getModelProvider(),
this.converter);
request = new WriteRequest(WriteRequest.Mode.UPDATE, contentFormat, resultIds.getObjectId(),
resultIds.getObjectInstanceId(), resources);
}
/**
* params = "{\"id\":0,\"resources\":[{\"id\":14,\"value\":\"+5\"},{\"id\":15,\"value\":\"+9\"}]}"
*
* int rscId = resultIds.getObjectInstanceId();
*/
else if (resultIds.isObjectInstance()) {
String content = (String) params;
// node = Gson.fromJson((content, LwM2mNode.class);
} else if (resultIds.getObjectId() >= 0) {
request = new ObserveRequest(resultIds.getObjectId());
}
break;
case WRITE_ATTRIBUTES:
request = createWriteAttributeRequest(target, params);
@ -232,7 +248,11 @@ public class LwM2mTransportRequest {
String msg = String.format("%s: type operation %s %s", LOG_LW2M_ERROR,
typeOper.name(), e.getMessage());
serviceImpl.sendLogsToThingsboard(msg, registration.getId());
throw new Exception(e);
try {
throw new Exception(e);
} catch (Exception exception) {
exception.printStackTrace();
}
}
}
@ -244,6 +264,7 @@ public class LwM2mTransportRequest {
@SuppressWarnings("unchecked")
private void sendRequest(Registration registration, LwM2mClient lwM2MClient, DownlinkRequest request, long timeoutInMs, Lwm2mClientRpcRequest rpcRequest) {
context.getServer().send(registration, request, timeoutInMs, (ResponseCallback<?>) response -> {
if (!lwM2MClient.isInit()) {
lwM2MClient.initReadValue(this.serviceImpl, convertPathFromObjectIdToIdVer(request.getPath().toString(), registration));

2
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mVersionedModelProvider.java

@ -114,9 +114,7 @@ public class LwM2mVersionedModelProvider implements LwM2mModelProvider {
private ObjectModel getObjectModelDynamic(Integer objectId, String version) {
String key = getKeyIdVer(objectId, version);
Optional<TbResource> tbResource = context.getTransportResourceCache().get(this.tenantId, LWM2M_MODEL, key);
return tbResource.map(resource -> helper.parseFromXmlToObjectModel(
Base64.getDecoder().decode(resource.getData()),
key + ".xml",

14
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.transport.lwm2m.server.client;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@ -28,9 +27,8 @@ import org.eclipse.leshan.server.registration.Registration;
import org.eclipse.leshan.server.security.SecurityInfo;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto;
import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto;
import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg;
import org.thingsboard.server.transport.lwm2m.server.DefaultLwM2MTransportMsgHandler;
import org.thingsboard.server.transport.lwm2m.server.LwM2mQueuedRequest;
@ -189,15 +187,17 @@ public class LwM2mClient implements Cloneable {
.getResourceModel(pathIds.getObjectId(), pathIds.getResourceId()) : null;
}
public Collection<LwM2mResource> getNewResourcesForInstance(String pathRezIdVer, LwM2mModelProvider modelProvider,
public Collection<LwM2mResource> getNewResourcesForInstance(String pathRezIdVer, Object params, LwM2mModelProvider modelProvider,
LwM2mValueConverterImpl converter) {
LwM2mPath pathIds = new LwM2mPath(convertPathFromIdVerToObjectId(pathRezIdVer));
Collection<LwM2mResource> resources = ConcurrentHashMap.newKeySet();
Map<Integer, ResourceModel> resourceModels = modelProvider.getObjectModel(registration)
.getObjectModel(pathIds.getObjectId()).resources;
resourceModels.forEach((k, resourceModel) -> {
resources.add(LwM2mSingleResource.newResource(k, converter.convertValue("0", ResourceModel.Type.STRING, resourceModel.type, pathIds), resourceModel.type));
});
resourceModels.forEach((resId, resourceModel) -> {
if (resId == pathIds.getResourceId()) {
resources.add(LwM2mSingleResource.newResource(resId, converter.convertValue(params, ResourceModel.Type.STRING, resourceModel.type, pathIds), resourceModel.type));
}});
return resources;
}

3
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java

@ -23,9 +23,9 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.util.TbLwM2mTransportComponent;
import org.thingsboard.server.transport.lwm2m.secure.EndpointSecurityInfo;
import org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode;
import org.thingsboard.server.transport.lwm2m.secure.LwM2mCredentialsSecurityInfoValidator;
import org.thingsboard.server.transport.lwm2m.secure.EndpointSecurityInfo;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportContext;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil;
@ -36,7 +36,6 @@ import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import static org.thingsboard.server.transport.lwm2m.secure.LwM2MSecurityMode.NO_SEC;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportUtil.convertPathFromObjectIdToIdVer;

8
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java

@ -60,11 +60,14 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter {
case INTEGER:
switch (currentType) {
case FLOAT:
log.debug("Trying to convert float value [{}] to integer", value);
log.debug("Trying to convert float value [{}] to Integer", value);
Long longValue = ((Double) value).longValue();
if ((double) value == longValue.doubleValue()) {
return longValue;
}
case STRING:
log.debug("Trying to convert String value [{}] to Integer", value);
return Long.parseLong((String) value);
default:
break;
}
@ -77,6 +80,9 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter {
if ((long) value == floatValue.longValue()) {
return floatValue;
}
case STRING:
log.debug("Trying to convert String value [{}] to Float", value);
return Float.valueOf((String) value);
default:
break;
}

70
ui-ngx/src/app/core/services/mobile.service.ts

@ -0,0 +1,70 @@
///
/// 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 { Inject, Injectable } from '@angular/core';
import { WINDOW } from '@core/services/window.service';
import { isDefined } from '@core/utils';
import { MobileActionResult, WidgetMobileActionResult, WidgetMobileActionType } from '@shared/models/widget.models';
import { from, of } from 'rxjs';
import { Observable } from 'rxjs/internal/Observable';
import { catchError } from 'rxjs/operators';
const dashboardStateNameHandler = 'tbMobileDashboardStateNameHandler';
const mobileHandler = 'tbMobileHandler';
// @dynamic
@Injectable({
providedIn: 'root'
})
export class MobileService {
private readonly mobileApp;
private readonly mobileChannel;
constructor(@Inject(WINDOW) private window: Window) {
const w = (this.window as any);
this.mobileChannel = w.flutter_inappwebview;
this.mobileApp = isDefined(this.mobileChannel);
}
public isMobileApp(): boolean {
return this.mobileApp;
}
public handleDashboardStateName(name: string) {
if (this.mobileApp) {
this.mobileChannel.callHandler(dashboardStateNameHandler, name);
}
}
public handleWidgetMobileAction<T extends MobileActionResult>(type: WidgetMobileActionType, ...args: any[]):
Observable<WidgetMobileActionResult<T>> {
if (this.mobileApp) {
return from(
this.mobileChannel.callHandler(mobileHandler, type, ...args) as Promise<WidgetMobileActionResult<T>>).pipe(
catchError((err: Error) => {
return of({
hasError: true,
error: err?.message ? err.message : `Failed to execute mobile action ${type}`
} as WidgetMobileActionResult<any>);
})
);
} else {
return of(null);
}
}
}

6
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<div class="tb-dashboard-page mat-content" style="padding-top: 150px;"
<div class="tb-dashboard-page mat-content" [ngClass]="{'mobile-app': isMobileApp && !isEdit}" style="padding-top: 150px;"
fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen">
<tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet>
<section class="tb-dashboard-toolbar"
@ -25,7 +25,7 @@
[toolbarOpened]="toolbarOpened" (triggerClick)="openToolbar()">
<div class="tb-dashboard-action-panels" fxLayout="column" fxLayout.gt-sm="row"
fxLayoutAlign="center stretch" fxLayoutAlign.gt-sm="space-between center">
<div class="tb-dashboard-action-panel" fxFlex="auto" fxLayout="row-reverse"
<div class="tb-dashboard-action-panel" fxFlex="auto" fxLayout="row-reverse" [fxHide]="isMobileApp && !isEdit"
fxLayoutAlign.gt-sm="end center" fxLayoutAlign="space-between center" fxLayoutGap="12px">
<tb-user-menu *ngIf="!isPublicUser() && forceFullscreen" fxHide.gt-sm displayUserInfo="true">
</tb-user-menu>
@ -49,7 +49,7 @@
<tb-states-component fxFlex.lt-md
[statesControllerId]="isEdit ? 'default' : dashboardConfiguration.settings.stateControllerId"
[dashboardCtrl]="this"
[dashboardId]="(!embedded && dashboard.id) ? dashboard.id.id : ''"
[dashboardId]="setStateDashboardId ? dashboard.id.id : ''"
[isMobile]="isMobile"
[state]="dashboardCtx.state"
[currentState]="currentState"

9
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss

@ -18,6 +18,7 @@
$toolbar-height: 50px !default;
$fullscreen-toolbar-height: 64px !default;
$mobile-toolbar-height: 84px !default;
$mobile-app-toolbar-height: 40px !default;
tb-dashboard-page {
display: flex;
@ -101,6 +102,14 @@ div.tb-dashboard-page {
}
}
&.mobile-app {
.tb-dashboard-container {
&.tb-dashboard-toolbar-opened {
margin-top: $mobile-app-toolbar-height;
}
}
}
mat-drawer-container.tb-dashboard-drawer-container {
mat-drawer-container.tb-dashboard-layouts {
width: 100%;

23
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -120,7 +120,8 @@ import {
DisplayWidgetTypesPanelData
} from '@home/components/dashboard-page/widget-types-panel.component';
import { DashboardWidgetSelectComponent } from '@home/components/dashboard-page/dashboard-widget-select.component';
import {AliasEntityType, EntityType} from "@shared/models/entity-type.models";
import { AliasEntityType, EntityType } from '@shared/models/entity-type.models';
import { MobileService } from '@core/services/mobile.service';
// @dynamic
@Component({
@ -159,6 +160,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
singlePageMode: boolean;
forceFullscreen = this.authState.forceFullscreen;
isMobileApp = this.mobileService.isMobileApp();
isFullscreen = false;
isEdit = false;
isEditingWidget = false;
@ -189,6 +191,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
currentCustomerId: string;
currentDashboardScope: DashboardPageScope;
setStateDashboardId = false;
addingLayoutCtx: DashboardPageLayoutContext;
logo = 'assets/logo_title_white.svg';
@ -284,6 +288,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private dashboardService: DashboardService,
private itembuffer: ItemBufferService,
private importExport: ImportExportService,
private mobileService: MobileService,
private fb: FormBuilder,
private dialog: MatDialog,
private translate: TranslateService,
@ -322,6 +327,19 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.reset();
this.dashboard = data.dashboard;
if (!this.embedded && this.dashboard.id) {
this.setStateDashboardId = true;
}
if (this.route.snapshot.queryParamMap.has('hideToolbar')) {
this.hideToolbar = this.route.snapshot.queryParamMap.get('hideToolbar') === 'true';
}
if (this.route.snapshot.queryParamMap.has('embedded')) {
this.embedded = this.route.snapshot.queryParamMap.get('embedded') === 'true';
}
this.currentDashboardId = data.currentDashboardId;
if (this.route.snapshot.params.customerId) {
@ -332,7 +350,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.currentCustomerId = this.authUser.customerId;
}
this.dashboard = data.dashboard;
this.dashboardConfiguration = this.dashboard.configuration;
this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow;
this.layouts.main.layoutCtx.widgets = new LayoutWidgetsArray(this.dashboardCtx);
@ -388,6 +405,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.currentCustomerId = null;
this.currentDashboardScope = null;
this.setStateDashboardId = false;
this.dashboardCtx.state = null;
}

36
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.scss

@ -21,6 +21,42 @@ $mobile-toolbar-height: 80px !default;
$half-mobile-toolbar-height: 40px !default;
$mobile-toolbar-height-total: 84px !default;
$mobile-app-toolbar-height: 40px !default;
div.tb-dashboard-page.mobile-app {
tb-dashboard-toolbar {
mat-fab-toolbar {
.mat-fab-toolbar-wrapper {
height: $mobile-app-toolbar-height;
mat-toolbar {
height: $mobile-app-toolbar-height;
min-height: $mobile-app-toolbar-height;
.mat-toolbar-tools {
height: $mobile-app-toolbar-height;
min-height: $mobile-app-toolbar-height;
}
mat-fab-actions {
height: $mobile-app-toolbar-height;
max-height: $mobile-app-toolbar-height;
.mat-fab-action-item {
height: $mobile-app-toolbar-height;
.tb-dashboard-action-panels {
height: $mobile-app-toolbar-height;
.tb-dashboard-action-panel {
height: $mobile-app-toolbar-height;
> div {
height: $mobile-app-toolbar-height;
}
}
}
}
}
}
}
}
}
}
tb-dashboard-toolbar {
mat-fab-toolbar {
mat-fab-trigger {

5
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();
}

3
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();
}

3
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,

100
ui-ngx/src/app/modules/home/components/widget/action/mobile-action-editor.component.html

@ -0,0 +1,100 @@
<!--
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.
-->
<div [formGroup]="mobileActionFormGroup">
<mat-form-field class="mat-block">
<mat-label translate>widget-action.mobile.action-type</mat-label>
<mat-select required formControlName="type">
<mat-option *ngFor="let actionType of mobileActionTypes" [value]="actionType">
{{ mobileActionTypeTranslations.get(mobileActionType[actionType]) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="mobileActionFormGroup.get('type').hasError('required')">
{{ 'widget-action.mobile.action-type-required' | translate }}
</mat-error>
</mat-form-field>
<div [formGroup]="mobileActionTypeFormGroup" [ngSwitch]="mobileActionFormGroup.get('type').value">
<ng-template [ngSwitchCase]="mobileActionFormGroup.get('type').value === mobileActionType.mapDirection ||
mobileActionFormGroup.get('type').value === mobileActionType.mapLocation ?
mobileActionFormGroup.get('type').value : ''">
<tb-js-func
formControlName="getLocationFunction"
functionName="getLocation"
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[editorCompleter]="customActionEditorCompleter"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="mobileActionType.makePhoneCall">
<tb-js-func
formControlName="getPhoneNumberFunction"
functionName="getPhoneNumber"
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[editorCompleter]="customActionEditorCompleter"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="mobileActionFormGroup.get('type').value === mobileActionType.takePhoto ||
mobileActionFormGroup.get('type').value === mobileActionType.takePictureFromGallery ||
mobileActionFormGroup.get('type').value === mobileActionType.takeScreenshot ?
mobileActionFormGroup.get('type').value : ''">
<tb-js-func
formControlName="processImageFunction"
functionName="processImage"
[functionArgs]="['imageUrl', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[editorCompleter]="customActionEditorCompleter"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="mobileActionType.scanQrCode">
<tb-js-func
formControlName="processQrCodeFunction"
functionName="processQrCode"
[functionArgs]="['code', 'format', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[editorCompleter]="customActionEditorCompleter"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="mobileActionType.getLocation">
<tb-js-func
formControlName="processLocationFunction"
functionName="processLocation"
[functionArgs]="['latitude', 'longitude', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[editorCompleter]="customActionEditorCompleter"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="mobileActionFormGroup.get('type').value === mobileActionType.mapDirection ||
mobileActionFormGroup.get('type').value === mobileActionType.mapLocation ||
mobileActionFormGroup.get('type').value === mobileActionType.makePhoneCall ?
mobileActionFormGroup.get('type').value : ''">
<tb-js-func
formControlName="processLaunchResultFunction"
functionName="processLaunchResult"
[functionArgs]="['launched', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[editorCompleter]="customActionEditorCompleter"
></tb-js-func>
</ng-template>
</div>
<tb-js-func *ngIf="mobileActionFormGroup.get('type').value"
formControlName="handleEmptyResultFunction"
functionName="handleEmptyResult"
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[editorCompleter]="customActionEditorCompleter"
></tb-js-func>
<tb-js-func *ngIf="mobileActionFormGroup.get('type').value"
formControlName="handleErrorFunction"
functionName="handleError"
[functionArgs]="['error', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[editorCompleter]="customActionEditorCompleter"
></tb-js-func>
</div>

253
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<JsFuncComponent>;
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<AppState>,
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();
}
}
}

317
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' +
' \'<div aria-label="Image">\' +\n' +
' \'<form #theForm="ngForm">\' +\n' +
' \'<mat-toolbar fxLayout="row" color="primary">\' +\n' +
' \'<h2>{{title}}</h2>\' +\n' +
' \'<span fxFlex></span>\' +\n' +
' \'<button mat-icon-button (click)="close()">\' +\n' +
' \'<mat-icon>close</mat-icon>\' +\n' +
' \'</button>\' +\n' +
' \'</mat-toolbar>\' +\n' +
' \'<div mat-dialog-content>\' +\n' +
' \'<div class="mat-content mat-padding">\' +\n' +
' \'<div fxLayout="column" fxFlex>\' +\n' +
' \'<div style="padding-top: 20px;">\' +\n' +
' \'<img [src]="imageUrl" style="height: 300px;"/>\' +\n' +
' \'</div>\' +\n' +
' \'</div>\' +\n' +
' \'</div>\' +\n' +
' \'</div>\' +\n' +
' \'<div mat-dialog-actions fxLayout="row">\' +\n' +
' \'<span fxFlex></span>\' +\n' +
' \'<button mat-button (click)="close()" style="margin-right:20px;">Close</button>\' +\n' +
' \'</div>\' +\n' +
' \'</form>\' +\n' +
' \'</div>\';\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+\']<br>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+\'<br>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);
}

5
ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html

@ -180,6 +180,11 @@
formControlName="customAction">
</tb-custom-action-pretty-editor>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionType.mobileAction">
<tb-mobile-action-editor #mobileActionEditor
formControlName="mobileAction">
</tb-mobile-action-editor>
</ng-template>
</section>
</fieldset>
</div>

30
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<WidgetActionDia
@ViewChild('dashboardStateInput') dashboardStateInput: ElementRef;
@ViewChild('mobileActionEditor', {static: false}) mobileActionEditor: MobileActionEditorComponent;
widgetActionFormGroup: FormGroup;
actionTypeFormGroup: FormGroup;
@ -197,6 +200,12 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
this.fb.control(toCustomAction(action), [Validators.required])
);
break;
case WidgetActionType.mobileAction:
this.actionTypeFormGroup.addControl(
'mobileAction',
this.fb.control(action ? action.mobileAction : null, [Validators.required])
);
break;
}
}
}
@ -313,14 +322,19 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
save(): void {
this.submitted = true;
const type: WidgetActionType = this.widgetActionFormGroup.get('type').value;
let result: WidgetActionDescriptorInfo;
if (type === WidgetActionType.customPretty) {
result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.get('customAction').value};
} else {
result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.value};
if (this.mobileActionEditor != null) {
this.mobileActionEditor.validateOnSubmit();
}
if (this.widgetActionFormGroup.valid && this.actionTypeFormGroup.valid) {
const type: WidgetActionType = this.widgetActionFormGroup.get('type').value;
let result: WidgetActionDescriptorInfo;
if (type === WidgetActionType.customPretty) {
result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.get('customAction').value};
} else {
result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.value};
}
result.id = this.action.id;
this.dialogRef.close(result);
}
result.id = this.action.id;
this.dialogRef.close(result);
}
}

182
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -39,12 +39,12 @@ import {
defaultLegendConfig,
LegendConfig,
LegendData,
LegendPosition,
LegendPosition, MobileActionResult,
Widget,
WidgetActionDescriptor,
widgetActionSources,
WidgetActionType,
WidgetComparisonSettings,
WidgetComparisonSettings, WidgetMobileActionDescriptor, WidgetMobileActionType,
WidgetResource,
widgetType,
WidgetTypeParameters
@ -77,7 +77,7 @@ import { EntityId } from '@shared/models/id/entity-id';
import { ActivatedRoute, Router } from '@angular/router';
import cssjs from '@core/css/css';
import { ResourcesService } from '@core/services/resources.service';
import { catchError, switchMap } from 'rxjs/operators';
import { catchError, map, switchMap } from 'rxjs/operators';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { TimeService } from '@core/services/time.service';
import { DeviceService } from '@app/core/http/device.service';
@ -97,6 +97,8 @@ import { AlarmDataService } from '@core/api/alarm-data.service';
import { MatDialog } from '@angular/material/dialog';
import { ComponentType } from '@angular/cdk/portal';
import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token';
import { MobileService } from '@core/services/mobile.service';
import { DialogService } from '@core/services/dialog.service';
@Component({
selector: 'tb-widget',
@ -177,6 +179,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
private alarmDataService: AlarmDataService,
private translate: TranslateService,
private utils: UtilsService,
private mobileService: MobileService,
private dialogs: DialogService,
private raf: RafService,
private ngZone: NgZone,
private cd: ChangeDetectorRef) {
@ -1094,6 +1098,178 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
);
break;
case WidgetActionType.mobileAction:
const mobileAction = descriptor.mobileAction;
this.handleMobileAction($event, mobileAction, entityId, entityName, additionalParams, entityLabel);
break;
}
}
private handleMobileAction($event: Event, mobileAction: WidgetMobileActionDescriptor,
entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) {
const type = mobileAction.type;
let argsObservable: Observable<any[]>;
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);
}
}
}

5
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,

97
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<WidgetActionType, string>(
@ -341,10 +353,90 @@ export const widgetActionTypeTranslationMap = new Map<WidgetActionType, string>(
[ 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, string>(
[
[ 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<T extends MobileActionResult> {
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<WidgetResource>;
@ -369,6 +461,7 @@ export interface WidgetActionDescriptor extends CustomActionDescriptor {
dialogHeight?: number;
setEntityId?: boolean;
stateEntityParamName?: string;
mobileAction?: WidgetMobileActionDescriptor;
}
export interface WidgetComparisonSettings {

15
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",

Loading…
Cancel
Save