Merge pull request #12987 from vvlladd28/map-widget/improvement/place-item/example
Add new map widgets helps/example
This commit is contained in:
commit
48c3828d5d
@ -37,6 +37,7 @@
|
||||
<div class="tb-custom-action-editor" [class.tb-fullscreen-editor]="fullscreen">
|
||||
<div *ngIf="!fullscreen; else fullscreenEditor">
|
||||
<tb-custom-action-pretty-resources-tabs [hasCustomFunction]="true"
|
||||
[helpId]="helpId"
|
||||
[action]="action" (actionUpdated)="onActionUpdated($event ? true : false)">
|
||||
</tb-custom-action-pretty-resources-tabs>
|
||||
</div>
|
||||
@ -44,6 +45,7 @@
|
||||
<div class="tb-fullscreen-panel tb-layout-fill flex flex-row">
|
||||
<div #leftPanel class="tb-split tb-content">
|
||||
<tb-custom-action-pretty-resources-tabs [hasCustomFunction]="false"
|
||||
[helpId]="helpId"
|
||||
[action]="action" (actionUpdated)="onActionUpdated($event ? true : false)">
|
||||
</tb-custom-action-pretty-resources-tabs>
|
||||
</div>
|
||||
@ -58,7 +60,7 @@
|
||||
[validationArgs]="[]"
|
||||
[editorCompleter]="customPrettyActionEditorCompleter"
|
||||
functionTitle="{{ 'widget-action.custom-pretty-function' | translate }}"
|
||||
helpId="widget/action/custom_pretty_action_fn">
|
||||
[helpId]="helpId">
|
||||
</tb-js-func>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -14,28 +14,22 @@
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
||||
/// <reference path="../../../../../../../../../../src/typings/split.js.typings.d.ts" />
|
||||
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
forwardRef,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
QueryList,
|
||||
ViewChildren,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { PageComponent } from '@shared/components/page.component';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { CustomActionDescriptor } from '@shared/models/widget.models';
|
||||
import { CustomPrettyActionEditorCompleter } from '@home/components/widget/lib/settings/common/action/custom-action.models';
|
||||
import { CustomActionDescriptor, WidgetActionType } from '@shared/models/widget.models';
|
||||
import {
|
||||
CustomPrettyActionEditorCompleter
|
||||
} from '@home/components/widget/lib/settings/common/action/custom-action.models';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-custom-action-pretty-editor',
|
||||
@ -50,7 +44,7 @@ import { CustomPrettyActionEditorCompleter } from '@home/components/widget/lib/s
|
||||
],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class CustomActionPrettyEditorComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy, ControlValueAccessor {
|
||||
export class CustomActionPrettyEditorComponent implements AfterViewInit, ControlValueAccessor {
|
||||
|
||||
@Input() disabled: boolean;
|
||||
|
||||
@ -58,6 +52,17 @@ export class CustomActionPrettyEditorComponent extends PageComponent implements
|
||||
|
||||
fullscreen = false;
|
||||
|
||||
helpId= 'widget/action/custom_pretty_action_fn';
|
||||
|
||||
@Input()
|
||||
set widgetActionType(type: WidgetActionType) {
|
||||
if (type === WidgetActionType.placeMapItem) {
|
||||
this.helpId = 'widget/action/place_map_item/place_map_item_action';
|
||||
} else {
|
||||
this.helpId = 'widget/action/custom_pretty_action_fn';
|
||||
}
|
||||
}
|
||||
|
||||
@ViewChildren('leftPanel')
|
||||
leftPanelElmRef: QueryList<ElementRef<HTMLElement>>;
|
||||
|
||||
@ -68,15 +73,11 @@ export class CustomActionPrettyEditorComponent extends PageComponent implements
|
||||
|
||||
private propagateChange = (_: any) => {};
|
||||
|
||||
constructor(protected store: Store<AppState>) {
|
||||
super(store);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
combineLatest(this.leftPanelElmRef.changes, this.rightPanelElmRef.changes).subscribe(() => {
|
||||
combineLatest([this.leftPanelElmRef.changes, this.rightPanelElmRef.changes]).subscribe(() => {
|
||||
if (this.leftPanelElmRef.length && this.rightPanelElmRef.length) {
|
||||
this.initSplitLayout(this.leftPanelElmRef.first.nativeElement,
|
||||
this.rightPanelElmRef.first.nativeElement);
|
||||
@ -92,14 +93,11 @@ export class CustomActionPrettyEditorComponent extends PageComponent implements
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
}
|
||||
|
||||
registerOnChange(fn: any): void {
|
||||
this.propagateChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: any): void {
|
||||
registerOnTouched(_fn: any): void {
|
||||
}
|
||||
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
|
||||
@ -101,7 +101,7 @@
|
||||
[validationArgs]="[]"
|
||||
[editorCompleter]="customPrettyActionEditorCompleter"
|
||||
functionTitle="{{ 'widget-action.custom-pretty-function' | translate }}"
|
||||
helpId="widget/action/custom_pretty_action_fn">
|
||||
[helpId]="helpId">
|
||||
</tb-js-func>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
|
||||
@ -27,16 +27,15 @@ import {
|
||||
ViewChild,
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { PageComponent } from '@shared/components/page.component';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@core/core.state';
|
||||
import { CustomActionDescriptor } from '@shared/models/widget.models';
|
||||
import { Ace } from 'ace-builds';
|
||||
import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
|
||||
import { CustomPrettyActionEditorCompleter } from '@home/components/widget/lib/settings/common/action/custom-action.models';
|
||||
import {
|
||||
CustomPrettyActionEditorCompleter
|
||||
} from '@home/components/widget/lib/settings/common/action/custom-action.models';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { forkJoin, from } from 'rxjs';
|
||||
import { forkJoin } from 'rxjs';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
import { getAce } from '@shared/models/ace/ace.models';
|
||||
import { beautifyCss, beautifyHtml } from '@shared/models/beautify.models';
|
||||
@ -55,6 +54,9 @@ export class CustomActionPrettyResourcesTabsComponent extends PageComponent impl
|
||||
@Input()
|
||||
hasCustomFunction: boolean;
|
||||
|
||||
@Input()
|
||||
helpId: string;
|
||||
|
||||
@Output()
|
||||
actionUpdated: EventEmitter<CustomActionDescriptor> = new EventEmitter<CustomActionDescriptor>();
|
||||
|
||||
@ -76,10 +78,8 @@ export class CustomActionPrettyResourcesTabsComponent extends PageComponent impl
|
||||
|
||||
customPrettyActionEditorCompleter = CustomPrettyActionEditorCompleter;
|
||||
|
||||
constructor(protected store: Store<AppState>,
|
||||
private translate: TranslateService,
|
||||
private raf: RafService) {
|
||||
super(store);
|
||||
constructor(private raf: RafService) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
@ -22,6 +22,8 @@ import { deepClone, isDefined, isUndefined } from '@core/utils';
|
||||
import customSampleJs from './custom-sample-js.raw';
|
||||
import customSampleCss from './custom-sample-css.raw';
|
||||
import customSampleHtml from './custom-sample-html.raw';
|
||||
import placeMapItemSampleHtml from './place-map-item-sample-html.raw';
|
||||
import placeMapItemSampleJs from './place-map-item-sample-js.raw';
|
||||
|
||||
const customActionCompletions: TbEditorCompletions = {
|
||||
...{
|
||||
@ -96,5 +98,15 @@ export const toCustomAction = (action: WidgetAction): CustomActionDescriptor =>
|
||||
return result;
|
||||
};
|
||||
|
||||
export const toPlaceMapItemAction = (action: WidgetAction): CustomActionDescriptor => {
|
||||
const result: CustomActionDescriptor = {
|
||||
customHtml: action?.customHtml ?? placeMapItemSampleHtml,
|
||||
customCss: action?.customCss ?? '',
|
||||
customFunction: action?.customFunction ?? placeMapItemSampleJs
|
||||
};
|
||||
result.customResources = isDefined(action?.customResources) ? deepClone(action.customResources) : [];
|
||||
return result;
|
||||
};
|
||||
|
||||
export const CustomActionEditorCompleter = new TbEditorCompleter(customActionCompletions);
|
||||
export const CustomPrettyActionEditorCompleter = new TbEditorCompleter(customPrettyActionCompletions);
|
||||
|
||||
@ -0,0 +1,82 @@
|
||||
<!--========================================================================-->
|
||||
<!--========================= Add entity example =========================-->
|
||||
<!--========================================================================-->
|
||||
|
||||
<form #addEntityForm="ngForm" [formGroup]="addEntityFormGroup"
|
||||
(ngSubmit)="save()" style="width: 552px">
|
||||
<mat-toolbar class="flex flex-row" color="primary">
|
||||
<h2>Add entity</h2>
|
||||
<span class="flex-1"></span>
|
||||
<button mat-icon-button (click)="cancel()" type="button">
|
||||
<mat-icon class="material-icons">close</mat-icon>
|
||||
</button>
|
||||
</mat-toolbar>
|
||||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
|
||||
</mat-progress-bar>
|
||||
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
|
||||
<div mat-dialog-content class="flex flex-col" style="padding-bottom: 0">
|
||||
<div class="flex flex-row gap-2 xs:flex-col xs:gap-0">
|
||||
<mat-form-field class="mat-block flex-1" appearance="outline">
|
||||
<mat-label>Entity Name</mat-label>
|
||||
<input matInput formControlName="entityName" required>
|
||||
<mat-error *ngIf="addEntityFormGroup.get('entityName').hasError('required')">
|
||||
Entity name is required.
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="mat-block flex-1" appearance="outline">
|
||||
<mat-label>Entity Label</mat-label>
|
||||
<input matInput formControlName="entityLabel" >
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 xs:flex-col xs:gap-0">
|
||||
<tb-entity-type-select
|
||||
class="mat-block flex-1"
|
||||
formControlName="entityType"
|
||||
[showLabel]="true"
|
||||
[appearance]="'outline'"
|
||||
[allowedEntityTypes]="allowedEntityTypes"
|
||||
></tb-entity-type-select>
|
||||
<tb-entity-subtype-autocomplete
|
||||
*ngIf="addEntityFormGroup.get('entityType').value == 'ASSET'"
|
||||
class="mat-block flex-1"
|
||||
formControlName="type"
|
||||
[required]="true"
|
||||
[entityType]="'ASSET'"
|
||||
[appearance]="'outline'"
|
||||
></tb-entity-subtype-autocomplete>
|
||||
<tb-entity-subtype-autocomplete
|
||||
*ngIf="addEntityFormGroup.get('entityType').value != 'ASSET'"
|
||||
class="mat-block flex-1"
|
||||
formControlName="type"
|
||||
[required]="true"
|
||||
[entityType]="'DEVICE'"
|
||||
[appearance]="'outline'"
|
||||
></tb-entity-subtype-autocomplete>
|
||||
</div>
|
||||
<div formGroupName="attributes" class="flex flex-col">
|
||||
<div class="flex flex-row gap-2 xs:flex-col xs:gap-0">
|
||||
<mat-form-field class="mat-block flex-1" appearance="outline">
|
||||
<mat-label>Address</mat-label>
|
||||
<input matInput formControlName="address">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="mat-block flex-1" appearance="outline">
|
||||
<mat-label>Owner</mat-label>
|
||||
<input matInput formControlName="owner">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div mat-dialog-actions class="flex flex-row items-center justify-end">
|
||||
<button mat-button color="primary"
|
||||
type="button"
|
||||
[disabled]="(isLoading$ | async)"
|
||||
(click)="cancel()" cdkFocusInitial>
|
||||
Cancel
|
||||
</button>
|
||||
<button mat-button mat-raised-button color="primary"
|
||||
type="submit"
|
||||
[disabled]="(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty">
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -0,0 +1,89 @@
|
||||
/*========================================================================*/
|
||||
/*========================= Add entity example =========================*/
|
||||
/*========================================================================*/
|
||||
|
||||
let $injector = widgetContext.$scope.$injector;
|
||||
let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));
|
||||
let assetService = $injector.get(widgetContext.servicesMap.get('assetService'));
|
||||
let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));
|
||||
let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));
|
||||
|
||||
openAddEntityDialog();
|
||||
|
||||
function openAddEntityDialog() {
|
||||
customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();
|
||||
}
|
||||
|
||||
function AddEntityDialogController(instance) {
|
||||
let vm = instance;
|
||||
|
||||
vm.allowedEntityTypes = ['ASSET', 'DEVICE'];
|
||||
|
||||
vm.addEntityFormGroup = vm.fb.group({
|
||||
entityName: ['', [vm.validators.required]],
|
||||
entityType: ['DEVICE'],
|
||||
entityLabel: [null],
|
||||
type: ['', [vm.validators.required]],
|
||||
attributes: vm.fb.group({
|
||||
address: [null],
|
||||
owner: [null]
|
||||
})
|
||||
});
|
||||
|
||||
vm.cancel = function() {
|
||||
vm.dialogRef.close(null);
|
||||
};
|
||||
|
||||
vm.save = function() {
|
||||
vm.addEntityFormGroup.markAsPristine();
|
||||
saveEntityObservable().pipe(
|
||||
widgetContext.rxjs.switchMap((entity) => saveAttributes(entity.id))
|
||||
).subscribe(() => {
|
||||
widgetContext.updateAliases();
|
||||
vm.dialogRef.close(null);
|
||||
});
|
||||
};
|
||||
|
||||
function saveEntityObservable() {
|
||||
const formValues = vm.addEntityFormGroup.value;
|
||||
let entity = {
|
||||
name: formValues.entityName,
|
||||
type: formValues.type,
|
||||
label: formValues.entityLabel
|
||||
};
|
||||
if (formValues.entityType == 'ASSET') {
|
||||
return assetService.saveAsset(entity);
|
||||
} else if (formValues.entityType == 'DEVICE') {
|
||||
return deviceService.saveDevice(entity);
|
||||
}
|
||||
}
|
||||
|
||||
function saveAttributes(entityId) {
|
||||
let attributes = vm.addEntityFormGroup.get('attributes').value;
|
||||
let attributesArray = getMapItemLocationAttributes();
|
||||
for (let key in attributes) {
|
||||
if(attributes[key] !== null) {
|
||||
attributesArray.push({key: key, value: attributes[key]});
|
||||
}
|
||||
}
|
||||
if (attributesArray.length > 0) {
|
||||
return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray);
|
||||
}
|
||||
return widgetContext.rxjs.of([]);
|
||||
}
|
||||
|
||||
function getMapItemLocationAttributes() {
|
||||
const attributes = [];
|
||||
const mapItemType = $event.shape;
|
||||
if (mapItemType === 'Marker') {
|
||||
const mapType = widgetContext.mapInstance.type();
|
||||
attributes.push({key: mapType === 'image' ? 'xPos' : 'latitude', value: additionalParams.coordinates.x});
|
||||
attributes.push({key: mapType === 'image' ? 'yPos' : 'longitude', value: additionalParams.coordinates.y});
|
||||
} else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon') {
|
||||
attributes.push({key: 'perimeter', value: additionalParams.coordinates});
|
||||
} else if (mapItemType === 'Circle') {
|
||||
attributes.push({key: 'circle', value: additionalParams.coordinates});
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
}
|
||||
@ -274,6 +274,7 @@
|
||||
|| widgetActionFormGroup.get('type').value === widgetActionType.placeMapItem
|
||||
? widgetActionFormGroup.get('type').value : ''">
|
||||
<tb-custom-action-pretty-editor
|
||||
[widgetActionType]="widgetActionFormGroup.get('type').value"
|
||||
formControlName="customAction">
|
||||
</tb-custom-action-pretty-editor>
|
||||
</ng-template>
|
||||
|
||||
@ -48,7 +48,8 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { PopoverPlacement, PopoverPlacements } from '@shared/components/popover.models';
|
||||
import {
|
||||
CustomActionEditorCompleter,
|
||||
toCustomAction
|
||||
toCustomAction,
|
||||
toPlaceMapItemAction
|
||||
} from '@home/components/widget/lib/settings/common/action/custom-action.models';
|
||||
import { coerceBoolean } from '@shared/decorators/coercion';
|
||||
|
||||
@ -336,7 +337,7 @@ export class WidgetActionComponent implements ControlValueAccessor, OnInit, Vali
|
||||
);
|
||||
this.actionTypeFormGroup.addControl(
|
||||
'customAction',
|
||||
this.fb.control(toCustomAction(action), [Validators.required])
|
||||
this.fb.control(toPlaceMapItemAction(action), [Validators.required])
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -430,7 +430,7 @@
|
||||
<ng-container *ngTemplateOutlet="behavior"></ng-container>
|
||||
}
|
||||
<div class="tb-form-panel">
|
||||
<div class="tb-form-panel-title">{{ 'widgets.maps.data-layer.groups' | translate }}</div>
|
||||
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'widgets.maps.data-layer.groups-hint' | translate }}">{{ 'widgets.maps.data-layer.groups' | translate }}</div>
|
||||
<tb-string-items-list class="tb-inline-chips"
|
||||
editable
|
||||
placeholder="{{'widgets.maps.data-layer.groups' | translate}}"
|
||||
@ -462,7 +462,9 @@
|
||||
</div>
|
||||
<div class="tb-form-row">
|
||||
<mat-slide-toggle class="mat-slide" formControlName="snappable">
|
||||
<span tb-hint-tooltip-icon="{{ 'widgets.maps.data-layer.enable-snapping-hint' | translate }}">
|
||||
{{ 'widgets.maps.data-layer.enable-snapping' | translate }}
|
||||
</span>
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
</div>
|
||||
<div class="tb-form-panel">
|
||||
<div class="flex flex-row items-center justify-between xs:flex-col xs:items-start xs:gap-3">
|
||||
<div class="tb-form-panel-title">
|
||||
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'widgets.maps.overlays.overlays-hint' | translate }}">
|
||||
{{ 'widgets.maps.overlays.overlays' | translate }}
|
||||
</div>
|
||||
<tb-toggle-select [(ngModel)]="dataLayerMode"
|
||||
@ -70,7 +70,7 @@
|
||||
[mapType]="mapSettingsFormGroup.get('mapType').value"></tb-map-data-layers>
|
||||
</div>
|
||||
<div class="tb-form-panel">
|
||||
<div class="tb-form-panel-title">
|
||||
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'widgets.maps.data-layer.additional-datasources-hint' | translate }}">
|
||||
{{ 'widgets.maps.data-layer.additional-datasources' | translate }}
|
||||
</div>
|
||||
<tb-map-data-sources formControlName="additionalDataSources"
|
||||
|
||||
@ -15,9 +15,9 @@
|
||||
limitations under the License.
|
||||
|
||||
-->
|
||||
<mat-form-field [formGroup]="entityTypeFormGroup">
|
||||
<mat-form-field [formGroup]="entityTypeFormGroup" [appearance]="appearance">
|
||||
<mat-label *ngIf="showLabel">{{ 'entity.type' | translate }}</mat-label>
|
||||
<mat-select [required]="required" matInput formControlName="entityType">
|
||||
<mat-select [required]="required" formControlName="entityType">
|
||||
<mat-option *ngFor="let type of entityTypes" [value]="type">
|
||||
{{ displayEntityTypeFn(type) }}
|
||||
</mat-option>
|
||||
|
||||
@ -14,24 +14,14 @@
|
||||
/// limitations under the License.
|
||||
///
|
||||
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
DestroyRef,
|
||||
forwardRef,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
SimpleChanges
|
||||
} from '@angular/core';
|
||||
import { Component, DestroyRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { AppState } from '@app/core/core.state';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { AliasEntityType, EntityType, entityTypeTranslations } from '@app/shared/models/entity-type.models';
|
||||
import { EntityService } from '@core/http/entity.service';
|
||||
import { coerceBoolean } from '@shared/decorators/coercion';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { MatFormFieldAppearance } from '@angular/material/form-field';
|
||||
|
||||
@Component({
|
||||
selector: 'tb-entity-type-select',
|
||||
@ -43,7 +33,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
multi: true
|
||||
}]
|
||||
})
|
||||
export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges {
|
||||
export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, OnChanges {
|
||||
|
||||
entityTypeFormGroup: UntypedFormGroup;
|
||||
|
||||
@ -72,12 +62,14 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit,
|
||||
@Input()
|
||||
additionEntityTypes: {[key in string]: string} = {};
|
||||
|
||||
@Input()
|
||||
appearance: MatFormFieldAppearance = 'fill';
|
||||
|
||||
entityTypes: Array<EntityType | AliasEntityType | string>;
|
||||
|
||||
private propagateChange = (v: any) => { };
|
||||
|
||||
constructor(private store: Store<AppState>,
|
||||
private entityService: EntityService,
|
||||
constructor(private entityService: EntityService,
|
||||
public translate: TranslateService,
|
||||
private fb: UntypedFormBuilder,
|
||||
private destroyRef: DestroyRef) {
|
||||
@ -136,9 +128,6 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit,
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
}
|
||||
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
this.disabled = isDisabled;
|
||||
if (this.disabled) {
|
||||
|
||||
@ -32,6 +32,13 @@ An optional key/value object holding additional entity parameters depending on w
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Map widgets (<i>On marker/polygon/circle click</i> or <i>Tag action</i>) - <b>additionalParams</b>: <code><a href="https://github.com/thingsboard/thingsboard/blob/b881f1c2985399f9665e033e2479549e97da1f36/ui-ngx/src/app/shared/models/widget.models.ts#L513" target="_blank">FormattedData</a></code>:
|
||||
<ul>
|
||||
<li><b>additionalParams:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/b881f1c2985399f9665e033e2479549e97da1f36/ui-ngx/src/app/shared/models/widget.models.ts#L513" target="_blank">FormattedData</a></code> - An object associated with a data layer (markers, polygons, circles) or with a specific data point of a route (for trips data layers).<br/>
|
||||
It contains basic entity properties (ex. <code>entityId</code>, <code>entityName</code>) and provides access to additional attributes and timeseries defined in datasource of the data layer configuration.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Entities hierarchy widget (<i>On node selected</i>) - <b>additionalParams:</b> <code>{ nodeCtx: <a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts#L35" target="_blank">HierarchyNodeContext</a> }</code>:
|
||||
<ul>
|
||||
<li><b>nodeCtx:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/e264f7b8ddff05bda85c4833bf497f47f447496e/ui-ngx/src/app/modules/home/components/widget/lib/entities-hierarchy-widget.models.ts#L35" target="_blank">HierarchyNodeContext</a></code> - An
|
||||
|
||||
@ -0,0 +1,87 @@
|
||||
#### HTML template of dialog to create a device or an asset
|
||||
|
||||
```html
|
||||
{:code-style="max-height: 400px;"}
|
||||
<form #addEntityForm="ngForm" [formGroup]="addEntityFormGroup"
|
||||
(ngSubmit)="save()" style="width: 552px">
|
||||
<mat-toolbar class="flex flex-row" color="primary">
|
||||
<h2>Add entity</h2>
|
||||
<span class="flex-1"></span>
|
||||
<button mat-icon-button (click)="cancel()" type="button">
|
||||
<mat-icon class="material-icons">close</mat-icon>
|
||||
</button>
|
||||
</mat-toolbar>
|
||||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
|
||||
</mat-progress-bar>
|
||||
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
|
||||
<div mat-dialog-content class="flex flex-col" style="padding-bottom: 0">
|
||||
<div class="flex flex-row gap-2 xs:flex-col xs:gap-0">
|
||||
<mat-form-field class="mat-block flex-1" appearance="outline">
|
||||
<mat-label>Entity Name</mat-label>
|
||||
<input matInput formControlName="entityName" required>
|
||||
<mat-error *ngIf="addEntityFormGroup.get('entityName').hasError('required')">
|
||||
Entity name is required.
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="mat-block flex-1" appearance="outline">
|
||||
<mat-label>Entity Label</mat-label>
|
||||
<input matInput formControlName="entityLabel" >
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 xs:flex-col xs:gap-0">
|
||||
<tb-entity-type-select
|
||||
class="mat-block flex-1"
|
||||
formControlName="entityType"
|
||||
[showLabel]="true"
|
||||
[appearance]="'outline'"
|
||||
[allowedEntityTypes]="allowedEntityTypes"
|
||||
></tb-entity-type-select>
|
||||
<tb-entity-subtype-autocomplete
|
||||
*ngIf="addEntityFormGroup.get('entityType').value == 'ASSET'"
|
||||
class="mat-block flex-1"
|
||||
formControlName="type"
|
||||
[required]="true"
|
||||
[entityType]="'ASSET'"
|
||||
[appearance]="'outline'"
|
||||
></tb-entity-subtype-autocomplete>
|
||||
<tb-entity-subtype-autocomplete
|
||||
*ngIf="addEntityFormGroup.get('entityType').value != 'ASSET'"
|
||||
class="mat-block flex-1"
|
||||
formControlName="type"
|
||||
[required]="true"
|
||||
[entityType]="'DEVICE'"
|
||||
[appearance]="'outline'"
|
||||
></tb-entity-subtype-autocomplete>
|
||||
</div>
|
||||
<div formGroupName="attributes" class="flex flex-col">
|
||||
<div class="flex flex-row gap-2 xs:flex-col xs:gap-0">
|
||||
<mat-form-field class="mat-block flex-1" appearance="outline">
|
||||
<mat-label>Address</mat-label>
|
||||
<input matInput formControlName="address">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="mat-block flex-1" appearance="outline">
|
||||
<mat-label>Owner</mat-label>
|
||||
<input matInput formControlName="owner">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div mat-dialog-actions class="flex flex-row items-center justify-end">
|
||||
<button mat-button color="primary"
|
||||
type="button"
|
||||
[disabled]="(isLoading$ | async)"
|
||||
(click)="cancel()" cdkFocusInitial>
|
||||
Cancel
|
||||
</button>
|
||||
<button mat-button mat-raised-button color="primary"
|
||||
type="submit"
|
||||
[disabled]="(isLoading$ | async) || addEntityForm.invalid || !addEntityForm.dirty">
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{:copy-code}
|
||||
```
|
||||
|
||||
<br>
|
||||
<br>
|
||||
@ -0,0 +1,94 @@
|
||||
#### Function displaying dialog to create a device or an asset
|
||||
|
||||
```javascript
|
||||
{:code-style="max-height: 400px;"}
|
||||
let $injector = widgetContext.$scope.$injector;
|
||||
let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));
|
||||
let assetService = $injector.get(widgetContext.servicesMap.get('assetService'));
|
||||
let deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));
|
||||
let attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));
|
||||
|
||||
openAddEntityDialog();
|
||||
|
||||
function openAddEntityDialog() {
|
||||
customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();
|
||||
}
|
||||
|
||||
function AddEntityDialogController(instance) {
|
||||
let vm = instance;
|
||||
|
||||
vm.allowedEntityTypes = ['ASSET', 'DEVICE'];
|
||||
|
||||
vm.addEntityFormGroup = vm.fb.group({
|
||||
entityName: ['', [vm.validators.required]],
|
||||
entityType: ['DEVICE'],
|
||||
entityLabel: [null],
|
||||
type: ['', [vm.validators.required]],
|
||||
attributes: vm.fb.group({
|
||||
address: [null],
|
||||
owner: [null]
|
||||
})
|
||||
});
|
||||
|
||||
vm.cancel = function() {
|
||||
vm.dialogRef.close(null);
|
||||
};
|
||||
|
||||
vm.save = function() {
|
||||
vm.addEntityFormGroup.markAsPristine();
|
||||
saveEntityObservable().pipe(
|
||||
widgetContext.rxjs.switchMap((entity) => saveAttributes(entity.id))
|
||||
).subscribe(() => {
|
||||
widgetContext.updateAliases();
|
||||
vm.dialogRef.close(null);
|
||||
});
|
||||
};
|
||||
|
||||
function saveEntityObservable() {
|
||||
const formValues = vm.addEntityFormGroup.value;
|
||||
let entity = {
|
||||
name: formValues.entityName,
|
||||
type: formValues.type,
|
||||
label: formValues.entityLabel
|
||||
};
|
||||
if (formValues.entityType == 'ASSET') {
|
||||
return assetService.saveAsset(entity);
|
||||
} else if (formValues.entityType == 'DEVICE') {
|
||||
return deviceService.saveDevice(entity);
|
||||
}
|
||||
}
|
||||
|
||||
function saveAttributes(entityId) {
|
||||
let attributes = vm.addEntityFormGroup.get('attributes').value;
|
||||
let attributesArray = getMapItemLocationAttributes();
|
||||
for (let key in attributes) {
|
||||
if(attributes[key] !== null) {
|
||||
attributesArray.push({key: key, value: attributes[key]});
|
||||
}
|
||||
}
|
||||
if (attributesArray.length > 0) {
|
||||
return attributeService.saveEntityAttributes(entityId, "SERVER_SCOPE", attributesArray);
|
||||
}
|
||||
return widgetContext.rxjs.of([]);
|
||||
}
|
||||
|
||||
function getMapItemLocationAttributes() {
|
||||
const attributes = [];
|
||||
const mapItemType = $event.shape;
|
||||
if (mapItemType === 'Marker') {
|
||||
const mapType = widgetContext.mapInstance.type();
|
||||
attributes.push({key: mapType === 'image' ? 'xPos' : 'latitude', value: additionalParams.coordinates.x});
|
||||
attributes.push({key: mapType === 'image' ? 'yPos' : 'longitude', value: additionalParams.coordinates.y});
|
||||
} else if (mapItemType === 'Rectangle' || mapItemType === 'Polygon') {
|
||||
attributes.push({key: 'perimeter', value: additionalParams.coordinates});
|
||||
} else if (mapItemType === 'Circle') {
|
||||
attributes.push({key: 'circle', value: additionalParams.coordinates});
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
}
|
||||
{:copy-code}
|
||||
```
|
||||
|
||||
<br>
|
||||
<br>
|
||||
@ -0,0 +1,66 @@
|
||||
#### Place map item function
|
||||
|
||||
<div class="divider"></div>
|
||||
<br/>
|
||||
|
||||
*function ($event, widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel): void*
|
||||
|
||||
A JavaScript function triggered after a map item is placed. Optionally uses an HTML template to render dialog.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
<ul style="width: 700px">
|
||||
<li><b>$event:</b> <code>{shape: <a href="https://github.com/geoman-io/leaflet-geoman/blob/6335a8c6cbebfcd06707d3c5da9d3d393cd2d942/leaflet-geoman.d.ts#L829" target="_blank">PM.SUPPORTED_SHAPES</a>; layer: <a href="https://leafletjs.com/reference.html#layer" target="_blank">L.Layer</a>}</code> - Event payload containing the created shape type and its associated map layer.
|
||||
</li>
|
||||
<li><b>widgetContext:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a></code> - A reference to <a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107" target="_blank">WidgetContext</a> that has all necessary API
|
||||
and data used by widget instance.
|
||||
</li>
|
||||
<li><b>entityId:</b> <code>string</code> - An optional string id of the target entity.
|
||||
</li>
|
||||
<li><b>entityName:</b> <code>string</code> - An optional string name of the target entity.
|
||||
</li>
|
||||
<li><b>htmlTemplate:</b> <code>string</code> - An optional HTML template string defined in <b>HTML</b> tab.<br/> Used to render custom dialog (see <b>Examples</b> for more details).
|
||||
</li>
|
||||
<li><b>additionalParams</b>: <code>{coordinates: Coordinates; layer: <a href="https://leafletjs.com/reference.html#layer" target="_blank">L.Layer</a>}</code>:
|
||||
<ul>
|
||||
<li><b>coordinates:</b> <code>Coordinates</code> - Represents geographical coordinates of the placed map item. The actual format of this parameter depends on the type of the selected map item:
|
||||
<ul>
|
||||
<li><b>Marker:</b> <code>{x: number; y: number}</code>, where <code>x</code> represents latitude, and <code>y</code> represents longitude.</li>
|
||||
<li><b>Polygon, Rectangle:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/61254a68507c6def8c055b7b3ae70413c456a4ac/ui-ngx/src/app/shared/models/widget/maps/map.models.ts#L1099" target="_blank">TbPolygonRawCoordinates</a></code> contains an array of points defining the shape boundaries.</li>
|
||||
<li><b>Circle:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/61254a68507c6def8c055b7b3ae70413c456a4ac/ui-ngx/src/app/shared/models/widget/maps/map.models.ts#L1104" target="_blank">TbCircleData</a></code> contains center coordinates and radius information.</li>
|
||||
</ul>
|
||||
Note: The coordinates will be automatically converted according to the selected map type.
|
||||
</li>
|
||||
<li><b>layer:</b> <code><a href="https://leafletjs.com/reference.html#layer" target="_blank">L.Layer</a></code> - The Leaflet map layer instance (e.g., marker, polygon, circle) associated with the placed map item. This object provides access to layer properties and methods defined in Leaflet's API.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><b>entityLabel:</b> <code>string</code> - An optional string label of the target entity.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
##### Examples
|
||||
|
||||
###### Display dialog to create a device or an asset
|
||||
|
||||
<br>
|
||||
|
||||
<div style="padding-left: 64px;"
|
||||
tb-help-popup="widget/action/place_map_item/create_dialog_js"
|
||||
tb-help-popup-placement="top"
|
||||
[tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
|
||||
trigger-style="font-size: 16px;"
|
||||
trigger-text="JavaScript function">
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div style="padding-left: 64px;"
|
||||
tb-help-popup="widget/action/place_map_item/create_dialog_html"
|
||||
tb-help-popup-placement="top"
|
||||
[tb-help-popup-style]="{maxHeight: '50vh', maxWidth: '50vw'}"
|
||||
trigger-style="font-size: 16px;"
|
||||
trigger-text="HTML code">
|
||||
</div>
|
||||
@ -7976,6 +7976,7 @@
|
||||
},
|
||||
"overlays": {
|
||||
"overlays": "Overlays",
|
||||
"overlays-hint": "Configure datasources, appearance, behavior, editing options, and grouping for map entities",
|
||||
"trips": "Trips",
|
||||
"markers": "Markers",
|
||||
"polygons": "Polygons",
|
||||
@ -7985,6 +7986,7 @@
|
||||
"source": "Source",
|
||||
"additional-data-keys": "Additional data keys",
|
||||
"additional-datasources": "Additional datasources",
|
||||
"additional-datasources-hint": "Datasource for accessing attributes or telemetry from entities not displayed on the map, usable in map overlay functions.",
|
||||
"data-keys": "Data keys",
|
||||
"add-datasource": "Add datasource",
|
||||
"no-datasources": "No datasources configured",
|
||||
@ -7993,6 +7995,7 @@
|
||||
"on-click": "On click",
|
||||
"on-click-hint": "Action invoked when user clicks on the map item.",
|
||||
"groups": "Groups",
|
||||
"groups-hint": "List of group names assigned to the overlay, used to toggle its visibility on the map.",
|
||||
"color": "Color",
|
||||
"fill-color": "Fill color",
|
||||
"stroke": "Stroke",
|
||||
@ -8030,6 +8033,7 @@
|
||||
"edit-instruments": "Instruments",
|
||||
"persist-location-attribute-scope": "Scope of the attribute to persist location",
|
||||
"enable-snapping": "Enable snapping to other vertices for precision drawing",
|
||||
"enable-snapping-hint": "Automatically aligns new points with existing shapes to make drawing easier and more accurate.",
|
||||
"drag-drop-mode": "Drag-drop mode",
|
||||
"trip": {
|
||||
"no-trips": "No trips configured",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user