Merge pull request #12987 from vvlladd28/map-widget/improvement/place-item/example

Add new map widgets helps/example
This commit is contained in:
Igor Kulikov 2025-03-25 18:28:49 +02:00 committed by GitHub
commit 48c3828d5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 494 additions and 60 deletions

View File

@ -37,6 +37,7 @@
<div class="tb-custom-action-editor" [class.tb-fullscreen-editor]="fullscreen"> <div class="tb-custom-action-editor" [class.tb-fullscreen-editor]="fullscreen">
<div *ngIf="!fullscreen; else fullscreenEditor"> <div *ngIf="!fullscreen; else fullscreenEditor">
<tb-custom-action-pretty-resources-tabs [hasCustomFunction]="true" <tb-custom-action-pretty-resources-tabs [hasCustomFunction]="true"
[helpId]="helpId"
[action]="action" (actionUpdated)="onActionUpdated($event ? true : false)"> [action]="action" (actionUpdated)="onActionUpdated($event ? true : false)">
</tb-custom-action-pretty-resources-tabs> </tb-custom-action-pretty-resources-tabs>
</div> </div>
@ -44,6 +45,7 @@
<div class="tb-fullscreen-panel tb-layout-fill flex flex-row"> <div class="tb-fullscreen-panel tb-layout-fill flex flex-row">
<div #leftPanel class="tb-split tb-content"> <div #leftPanel class="tb-split tb-content">
<tb-custom-action-pretty-resources-tabs [hasCustomFunction]="false" <tb-custom-action-pretty-resources-tabs [hasCustomFunction]="false"
[helpId]="helpId"
[action]="action" (actionUpdated)="onActionUpdated($event ? true : false)"> [action]="action" (actionUpdated)="onActionUpdated($event ? true : false)">
</tb-custom-action-pretty-resources-tabs> </tb-custom-action-pretty-resources-tabs>
</div> </div>
@ -58,7 +60,7 @@
[validationArgs]="[]" [validationArgs]="[]"
[editorCompleter]="customPrettyActionEditorCompleter" [editorCompleter]="customPrettyActionEditorCompleter"
functionTitle="{{ 'widget-action.custom-pretty-function' | translate }}" functionTitle="{{ 'widget-action.custom-pretty-function' | translate }}"
helpId="widget/action/custom_pretty_action_fn"> [helpId]="helpId">
</tb-js-func> </tb-js-func>
</div> </div>
</div> </div>

View File

@ -14,28 +14,22 @@
/// limitations under the License. /// limitations under the License.
/// ///
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../../../../../../../../../../src/typings/split.js.typings.d.ts" />
import { import {
AfterViewInit, AfterViewInit,
Component, Component,
ElementRef, ElementRef,
forwardRef, forwardRef,
Input, Input,
OnDestroy,
OnInit,
QueryList, QueryList,
ViewChildren, ViewChildren,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 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 { combineLatest } from 'rxjs';
import { CustomActionDescriptor } from '@shared/models/widget.models'; import { CustomActionDescriptor, WidgetActionType } from '@shared/models/widget.models';
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';
@Component({ @Component({
selector: 'tb-custom-action-pretty-editor', selector: 'tb-custom-action-pretty-editor',
@ -50,7 +44,7 @@ import { CustomPrettyActionEditorCompleter } from '@home/components/widget/lib/s
], ],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class CustomActionPrettyEditorComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy, ControlValueAccessor { export class CustomActionPrettyEditorComponent implements AfterViewInit, ControlValueAccessor {
@Input() disabled: boolean; @Input() disabled: boolean;
@ -58,6 +52,17 @@ export class CustomActionPrettyEditorComponent extends PageComponent implements
fullscreen = false; 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') @ViewChildren('leftPanel')
leftPanelElmRef: QueryList<ElementRef<HTMLElement>>; leftPanelElmRef: QueryList<ElementRef<HTMLElement>>;
@ -68,15 +73,11 @@ export class CustomActionPrettyEditorComponent extends PageComponent implements
private propagateChange = (_: any) => {}; private propagateChange = (_: any) => {};
constructor(protected store: Store<AppState>) { constructor() {
super(store);
}
ngOnInit(): void {
} }
ngAfterViewInit(): void { 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) { if (this.leftPanelElmRef.length && this.rightPanelElmRef.length) {
this.initSplitLayout(this.leftPanelElmRef.first.nativeElement, this.initSplitLayout(this.leftPanelElmRef.first.nativeElement,
this.rightPanelElmRef.first.nativeElement); this.rightPanelElmRef.first.nativeElement);
@ -92,14 +93,11 @@ export class CustomActionPrettyEditorComponent extends PageComponent implements
}); });
} }
ngOnDestroy(): void {
}
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
this.propagateChange = fn; this.propagateChange = fn;
} }
registerOnTouched(fn: any): void { registerOnTouched(_fn: any): void {
} }
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {

View File

@ -101,7 +101,7 @@
[validationArgs]="[]" [validationArgs]="[]"
[editorCompleter]="customPrettyActionEditorCompleter" [editorCompleter]="customPrettyActionEditorCompleter"
functionTitle="{{ 'widget-action.custom-pretty-function' | translate }}" functionTitle="{{ 'widget-action.custom-pretty-function' | translate }}"
helpId="widget/action/custom_pretty_action_fn"> [helpId]="helpId">
</tb-js-func> </tb-js-func>
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>

View File

@ -27,16 +27,15 @@ import {
ViewChild, ViewChild,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { PageComponent } from '@shared/components/page.component'; 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 { CustomActionDescriptor } from '@shared/models/widget.models';
import { Ace } from 'ace-builds'; import { Ace } from 'ace-builds';
import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; 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 { Observable } from 'rxjs/internal/Observable';
import { forkJoin, from } from 'rxjs'; import { forkJoin } from 'rxjs';
import { map, tap } from 'rxjs/operators'; import { map, tap } from 'rxjs/operators';
import { getAce } from '@shared/models/ace/ace.models'; import { getAce } from '@shared/models/ace/ace.models';
import { beautifyCss, beautifyHtml } from '@shared/models/beautify.models'; import { beautifyCss, beautifyHtml } from '@shared/models/beautify.models';
@ -55,6 +54,9 @@ export class CustomActionPrettyResourcesTabsComponent extends PageComponent impl
@Input() @Input()
hasCustomFunction: boolean; hasCustomFunction: boolean;
@Input()
helpId: string;
@Output() @Output()
actionUpdated: EventEmitter<CustomActionDescriptor> = new EventEmitter<CustomActionDescriptor>(); actionUpdated: EventEmitter<CustomActionDescriptor> = new EventEmitter<CustomActionDescriptor>();
@ -76,10 +78,8 @@ export class CustomActionPrettyResourcesTabsComponent extends PageComponent impl
customPrettyActionEditorCompleter = CustomPrettyActionEditorCompleter; customPrettyActionEditorCompleter = CustomPrettyActionEditorCompleter;
constructor(protected store: Store<AppState>, constructor(private raf: RafService) {
private translate: TranslateService, super();
private raf: RafService) {
super(store);
} }
ngOnInit(): void { ngOnInit(): void {

View File

@ -22,6 +22,8 @@ import { deepClone, isDefined, isUndefined } from '@core/utils';
import customSampleJs from './custom-sample-js.raw'; import customSampleJs from './custom-sample-js.raw';
import customSampleCss from './custom-sample-css.raw'; import customSampleCss from './custom-sample-css.raw';
import customSampleHtml from './custom-sample-html.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 = { const customActionCompletions: TbEditorCompletions = {
...{ ...{
@ -96,5 +98,15 @@ export const toCustomAction = (action: WidgetAction): CustomActionDescriptor =>
return result; 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 CustomActionEditorCompleter = new TbEditorCompleter(customActionCompletions);
export const CustomPrettyActionEditorCompleter = new TbEditorCompleter(customPrettyActionCompletions); export const CustomPrettyActionEditorCompleter = new TbEditorCompleter(customPrettyActionCompletions);

View File

@ -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>

View File

@ -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;
}
}

View File

@ -274,7 +274,8 @@
|| widgetActionFormGroup.get('type').value === widgetActionType.placeMapItem || widgetActionFormGroup.get('type').value === widgetActionType.placeMapItem
? widgetActionFormGroup.get('type').value : ''"> ? widgetActionFormGroup.get('type').value : ''">
<tb-custom-action-pretty-editor <tb-custom-action-pretty-editor
formControlName="customAction"> [widgetActionType]="widgetActionFormGroup.get('type').value"
formControlName="customAction">
</tb-custom-action-pretty-editor> </tb-custom-action-pretty-editor>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="widgetActionType.mobileAction"> <ng-template [ngSwitchCase]="widgetActionType.mobileAction">

View File

@ -48,7 +48,8 @@ import { TranslateService } from '@ngx-translate/core';
import { PopoverPlacement, PopoverPlacements } from '@shared/components/popover.models'; import { PopoverPlacement, PopoverPlacements } from '@shared/components/popover.models';
import { import {
CustomActionEditorCompleter, CustomActionEditorCompleter,
toCustomAction toCustomAction,
toPlaceMapItemAction
} from '@home/components/widget/lib/settings/common/action/custom-action.models'; } from '@home/components/widget/lib/settings/common/action/custom-action.models';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
@ -336,7 +337,7 @@ export class WidgetActionComponent implements ControlValueAccessor, OnInit, Vali
); );
this.actionTypeFormGroup.addControl( this.actionTypeFormGroup.addControl(
'customAction', 'customAction',
this.fb.control(toCustomAction(action), [Validators.required]) this.fb.control(toPlaceMapItemAction(action), [Validators.required])
); );
break; break;
} }

View File

@ -430,7 +430,7 @@
<ng-container *ngTemplateOutlet="behavior"></ng-container> <ng-container *ngTemplateOutlet="behavior"></ng-container>
} }
<div class="tb-form-panel"> <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" <tb-string-items-list class="tb-inline-chips"
editable editable
placeholder="{{'widgets.maps.data-layer.groups' | translate}}" placeholder="{{'widgets.maps.data-layer.groups' | translate}}"
@ -462,7 +462,9 @@
</div> </div>
<div class="tb-form-row"> <div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="snappable"> <mat-slide-toggle class="mat-slide" formControlName="snappable">
{{ 'widgets.maps.data-layer.enable-snapping' | translate }} <span tb-hint-tooltip-icon="{{ 'widgets.maps.data-layer.enable-snapping-hint' | translate }}">
{{ 'widgets.maps.data-layer.enable-snapping' | translate }}
</span>
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
</div> </div>

View File

@ -36,7 +36,7 @@
</div> </div>
<div class="tb-form-panel"> <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="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 }} {{ 'widgets.maps.overlays.overlays' | translate }}
</div> </div>
<tb-toggle-select [(ngModel)]="dataLayerMode" <tb-toggle-select [(ngModel)]="dataLayerMode"
@ -70,7 +70,7 @@
[mapType]="mapSettingsFormGroup.get('mapType').value"></tb-map-data-layers> [mapType]="mapSettingsFormGroup.get('mapType').value"></tb-map-data-layers>
</div> </div>
<div class="tb-form-panel"> <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 }} {{ 'widgets.maps.data-layer.additional-datasources' | translate }}
</div> </div>
<tb-map-data-sources formControlName="additionalDataSources" <tb-map-data-sources formControlName="additionalDataSources"

View File

@ -15,9 +15,9 @@
limitations under the License. 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-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"> <mat-option *ngFor="let type of entityTypes" [value]="type">
{{ displayEntityTypeFn(type) }} {{ displayEntityTypeFn(type) }}
</mat-option> </mat-option>

View File

@ -14,24 +14,14 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { import { Component, DestroyRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
AfterViewInit,
Component,
DestroyRef,
forwardRef,
Input,
OnChanges,
OnInit,
SimpleChanges
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; 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 { TranslateService } from '@ngx-translate/core';
import { AliasEntityType, EntityType, entityTypeTranslations } from '@app/shared/models/entity-type.models'; import { AliasEntityType, EntityType, entityTypeTranslations } from '@app/shared/models/entity-type.models';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatFormFieldAppearance } from '@angular/material/form-field';
@Component({ @Component({
selector: 'tb-entity-type-select', selector: 'tb-entity-type-select',
@ -43,7 +33,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
multi: true multi: true
}] }]
}) })
export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges { export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit, OnChanges {
entityTypeFormGroup: UntypedFormGroup; entityTypeFormGroup: UntypedFormGroup;
@ -72,12 +62,14 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit,
@Input() @Input()
additionEntityTypes: {[key in string]: string} = {}; additionEntityTypes: {[key in string]: string} = {};
@Input()
appearance: MatFormFieldAppearance = 'fill';
entityTypes: Array<EntityType | AliasEntityType | string>; entityTypes: Array<EntityType | AliasEntityType | string>;
private propagateChange = (v: any) => { }; private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>, constructor(private entityService: EntityService,
private entityService: EntityService,
public translate: TranslateService, public translate: TranslateService,
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
private destroyRef: DestroyRef) { private destroyRef: DestroyRef) {
@ -136,9 +128,6 @@ export class EntityTypeSelectComponent implements ControlValueAccessor, OnInit,
} }
} }
ngAfterViewInit(): void {
}
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled; this.disabled = isDisabled;
if (this.disabled) { if (this.disabled) {

View File

@ -31,7 +31,14 @@ An optional key/value object holding additional entity parameters depending on w
presenting <code>formattedTs</code> (a string value of formatted timestamp) and <br> timeseries values for each column declared in widget datasource configuration. presenting <code>formattedTs</code> (a string value of formatted timestamp) and <br> timeseries values for each column declared in widget datasource configuration.
</li> </li>
</ul> </ul>
</li> </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>: <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> <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 <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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -7976,6 +7976,7 @@
}, },
"overlays": { "overlays": {
"overlays": "Overlays", "overlays": "Overlays",
"overlays-hint": "Configure datasources, appearance, behavior, editing options, and grouping for map entities",
"trips": "Trips", "trips": "Trips",
"markers": "Markers", "markers": "Markers",
"polygons": "Polygons", "polygons": "Polygons",
@ -7985,6 +7986,7 @@
"source": "Source", "source": "Source",
"additional-data-keys": "Additional data keys", "additional-data-keys": "Additional data keys",
"additional-datasources": "Additional datasources", "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", "data-keys": "Data keys",
"add-datasource": "Add datasource", "add-datasource": "Add datasource",
"no-datasources": "No datasources configured", "no-datasources": "No datasources configured",
@ -7993,6 +7995,7 @@
"on-click": "On click", "on-click": "On click",
"on-click-hint": "Action invoked when user clicks on the map item.", "on-click-hint": "Action invoked when user clicks on the map item.",
"groups": "Groups", "groups": "Groups",
"groups-hint": "List of group names assigned to the overlay, used to toggle its visibility on the map.",
"color": "Color", "color": "Color",
"fill-color": "Fill color", "fill-color": "Fill color",
"stroke": "Stroke", "stroke": "Stroke",
@ -8030,6 +8033,7 @@
"edit-instruments": "Instruments", "edit-instruments": "Instruments",
"persist-location-attribute-scope": "Scope of the attribute to persist location", "persist-location-attribute-scope": "Scope of the attribute to persist location",
"enable-snapping": "Enable snapping to other vertices for precision drawing", "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", "drag-drop-mode": "Drag-drop mode",
"trip": { "trip": {
"no-trips": "No trips configured", "no-trips": "No trips configured",