Browse Source

Fix conflicts

pull/12807/head
Igor Kulikov 2 years ago
parent
commit
7f59751471
  1. 2
      application/src/main/data/json/system/widget_types/image_map.json
  2. 2
      application/src/main/data/json/system/widget_types/map.json
  3. 1
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html
  4. 3
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts
  5. 7
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts
  6. 3
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html
  7. 2
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts
  8. 3
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts
  9. 109
      ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts
  10. 53
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss
  11. 289
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts
  12. 23
      ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts
  13. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.html
  14. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.ts
  15. 13
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings.component.ts
  16. 16
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html
  17. 42
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts
  18. 47
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.html
  19. 100
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.ts
  20. 63
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.html
  21. 107
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.ts
  22. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html
  23. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts
  24. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  25. 10
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  26. 1
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  27. 1
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  28. 27
      ui-ngx/src/app/shared/models/widget.models.ts
  29. 46
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  30. 13
      ui-ngx/src/typings/leaflet-extend-tb.d.ts

2
application/src/main/data/json/system/widget_types/image_map.json

@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "<tb-map-widget \n [ctx]=\"ctx\"\n [widgetTitlePanel]=\"widgetTitlePanel\">\n</tb-map-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true\n };\n}\n",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true,\n additionalWidgetActionTypes: ['placeMapItem']\n };\n}\n",
"settingsForm": [],
"dataKeySettingsForm": [],
"settingsDirective": "tb-map-widget-settings",

2
application/src/main/data/json/system/widget_types/map.json

@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "<tb-map-widget \n [ctx]=\"ctx\"\n [widgetTitlePanel]=\"widgetTitlePanel\">\n</tb-map-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true\n };\n}\n",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.mapWidget.onInit();\n};\n\nself.typeParameters = function() {\n return {\n hideDataTab: true,\n hideDataSettings: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n datasourcesOptional: true,\n additionalWidgetActionTypes: ['placeMapItem']\n };\n}\n",
"settingsForm": [],
"dataKeySettingsForm": [],
"settingsDirective": "tb-map-widget-settings",

1
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html

@ -33,6 +33,7 @@
[callbacks]="data.callbacks"
[widgetType] = "data.widgetType"
[actionSources]="actionSources"
[additionalWidgetActionTypes]="data.additionalWidgetActionTypes"
formControlName="actions">
</tb-manage-widget-actions>
</div>

3
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { widgetType } from '@shared/models/widget.models';
import { WidgetActionType, widgetType } from '@shared/models/widget.models';
import {
WidgetActionCallbacks,
WidgetActionsData
@ -32,6 +32,7 @@ export interface ManageWidgetActionsDialogData {
actionsData: WidgetActionsData;
callbacks: WidgetActionCallbacks;
widgetType: widgetType;
additionalWidgetActionTypes?: WidgetActionType[];
}
@Component({

7
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts

@ -46,7 +46,7 @@ import {
WidgetActionsDatasource
} from '@home/components/widget/action/manage-widget-actions.component.models';
import { UtilsService } from '@core/services/utils.service';
import { WidgetActionDescriptor, WidgetActionSource, widgetType } from '@shared/models/widget.models';
import { WidgetActionDescriptor, WidgetActionSource, WidgetActionType, widgetType } from '@shared/models/widget.models';
import {
WidgetActionDialogComponent,
WidgetActionDialogData
@ -77,6 +77,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
@Input() actionSources: {[actionSourceId: string]: WidgetActionSource};
@Input() additionalWidgetActionTypes: WidgetActionType[];
innerValue: WidgetActionsData;
displayedColumns: string[];
@ -236,7 +238,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni
callbacks: this.callbacks,
actionsData,
action: deepClone(action),
widgetType: this.widgetType
widgetType: this.widgetType,
additionalWidgetActionTypes: this.additionalWidgetActionTypes
}
}).afterClosed().subscribe(
(res) => {

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

@ -121,7 +121,8 @@
<tb-widget-action
formControlName="widgetAction"
[callbacks]="data.callbacks"
[widgetType]="data.widgetType">
[widgetType]="data.widgetType"
[additionalWidgetActionTypes]="data.additionalWidgetActionTypes">
</tb-widget-action>
</div>
</fieldset>

2
ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts

@ -43,6 +43,7 @@ import {
CellClickColumnInfo,
defaultWidgetAction,
WidgetActionSource,
WidgetActionType,
widgetType
} from '@shared/models/widget.models';
import { takeUntil } from 'rxjs/operators';
@ -58,6 +59,7 @@ export interface WidgetActionDialogData {
actionsData: WidgetActionsData;
action?: WidgetActionDescriptorInfo;
widgetType: widgetType;
additionalWidgetActionTypes?: WidgetActionType[];
}
@Component({

3
ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts

@ -122,7 +122,8 @@ export class WidgetActionsPanelComponent implements ControlValueAccessor, OnInit
widgetTitle: this.widgetConfigComponent.modelValue.widgetName,
callbacks: this.widgetConfigComponent.widgetConfigCallbacks,
actionsData,
widgetType: this.widgetConfigComponent.widgetType
widgetType: this.widgetConfigComponent.widgetType,
additionalWidgetActionTypes: this.widgetConfigComponent.modelValue.typeParameters.additionalWidgetActionTypes
}
}).afterClosed().subscribe(
(res) => {

109
ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts

@ -15,10 +15,14 @@
///
import L, { TB } from 'leaflet';
import { guid } from '@core/utils';
import { guid, isNotEmptyStr } from '@core/utils';
import 'leaflet-providers';
import '@geoman-io/leaflet-geoman-free';
import 'leaflet.markercluster';
import { MatIconRegistry } from '@angular/material/icon';
import { isSvgIcon, splitIconName } from '@shared/models/icon.models';
import { catchError, take } from 'rxjs/operators';
import { of } from 'rxjs';
L.MarkerCluster = L.MarkerCluster.mergeOptions({ pmIgnore: true });
@ -278,32 +282,105 @@ class GroupsControl extends SidebarPaneControl<TB.GroupsControlOptions> {
class TopToolbarButton {
private readonly button: JQuery<HTMLElement>;
private _onClick: (e: MouseEvent) => void;
private active = false;
private disabled = false;
private _onClick: (e: MouseEvent, button: TopToolbarButton) => void;
constructor(private readonly options: TB.TopToolbarButtonOptions) {
constructor(private readonly options: TB.TopToolbarButtonOptions,
private readonly iconRegistry: MatIconRegistry) {
const iconElement = $('<div class="tb-control-button-icon"></div>');
const setIcon = isNotEmptyStr(this.options.icon);
const setTitle = isNotEmptyStr(this.options.title);
this.button = $("<a>")
.attr('class', 'tb-control-button tb-control-text-button')
.attr('href', '#')
.attr('role', 'button');
this.button.append(iconElement);
this.button.append(`<div class="tb-control-text">${this.options.title}</div>`);
this.loadIcon(iconElement);
if (setIcon) {
this.button.append(iconElement);
this.loadIcon(iconElement);
}
if (setTitle) {
this.button.append(`<div class="tb-control-text">${this.options.title}</div>`);
}
this.button.css('--tb-map-control-color', this.options.color);
this.button.css('--tb-map-control-active-color', this.options.color);
this.button.css('--tb-map-control-hover-background-color', this.options.color);
if (setIcon && !setTitle) {
this.button.css('padding', 0);
} else if (!setIcon && setTitle) {
this.button.css('padding-left', '14px');
}
this.button.on('click', (e) => {
e.stopPropagation();
e.preventDefault();
if (this._onClick) {
this._onClick(e.originalEvent);
this._onClick(e.originalEvent, this);
}
});
}
onClick(onClick: (e: MouseEvent) => void): void {
onClick(onClick: (e: MouseEvent, button: TopToolbarButton) => void): void {
this._onClick = onClick;
}
private loadIcon(iconElement: JQuery<HTMLElement>) {
// this.options.icon
iconElement.addClass(this.iconRegistry.getDefaultFontSetClass());
if (!isSvgIcon(this.options.icon)) {
iconElement.addClass('material-icon-font');
iconElement.text(this.options.icon);
return;
}
const [namespace, iconName] = splitIconName(this.options.icon);
this.iconRegistry
.getNamedSvgIcon(iconName, namespace)
.pipe(
take(1),
catchError((err: Error) => {
console.log(`Error retrieving icon ${namespace}:${iconName}! ${err.message}`)
return of(null);
})
)
.subscribe({
next: (svg) => {
iconElement.append(svg);
svg.style.height = '24px';
svg.style.width = '24px';
}
});
}
setActive(active: boolean): void {
if (this.active !== active) {
this.active = active;
if (this.active) {
L.DomUtil.addClass(this.button[0], 'active');
} else {
L.DomUtil.removeClass(this.button[0], 'active');
}
}
}
isActive(): boolean {
return this.active;
}
setDisabled(disabled: boolean): void {
if (this.disabled !== disabled) {
this.disabled = disabled;
if (this.disabled) {
L.DomUtil.addClass(this.button[0], 'leaflet-disabled');
this.button[0].setAttribute('aria-disabled', 'true');
} else {
L.DomUtil.removeClass(this.button[0], 'leaflet-disabled');
this.button[0].setAttribute('aria-disabled', 'false');
}
}
}
isDisabled(): boolean {
return this.disabled;
}
getButtonElement(): JQuery<HTMLElement> {
@ -382,20 +459,30 @@ class ToolbarButton {
class TopToolbarControl {
private readonly toolbarElement: JQuery<HTMLElement>;
private buttons: Array<TopToolbarButton> = [];
constructor(private readonly options: TB.TopToolbarControlOptions) {
const controlContainer = $('.leaflet-control-container', options.mapElement);
const controlContainer = $('.leaflet-control-container', this.options.mapElement);
this.toolbarElement = $('<div class="tb-map-top-toolbar leaflet-top"></div>');
this.toolbarElement.appendTo(controlContainer);
}
toolbarButton(options: TB.TopToolbarButtonOptions): TopToolbarButton {
const button = new TopToolbarButton(options);
const button = new TopToolbarButton(options, this.options.iconRegistry);
const buttonContainer = $('<div class="leaflet-bar leaflet-control"></div>');
button.getButtonElement().appendTo(buttonContainer);
buttonContainer.appendTo(this.toolbarElement);
this.buttons.push(button);
return button;
}
setDisabled(disabled: boolean): void {
this.buttons.forEach(button => {
if (!button.isActive()) {
button.setDisabled(disabled);
}
});
}
}
class ToolbarControl extends L.Control<L.ControlOptions> {

53
ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss

@ -44,30 +44,34 @@
border: 1px solid rgba(0,0,0,0.38);
border-radius: 15px;
box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.32);
background: #fff;
background-color: #fff;
position: relative;
a {
color: rgba(0, 0, 0, 0.54);
color: var(--tb-map-control-color, rgba(0, 0, 0, 0.54));
border-bottom: none;
position: relative;
background: transparent;
background-color: transparent;
&.leaflet-disabled {
pointer-events: none;
color: rgba(0, 0, 0, 0.18);
> div {
background: rgba(0, 0, 0, 0.18);
color: var(--tb-map-control-disable-color, rgba(0, 0, 0, 0.18));
> div:not(.material-icon-font):not(.tb-control-text) {
background-color: var(--tb-map-control-disable-color, rgba(0, 0, 0, 0.18));
svg {
fill: var(--tb-map-control-disable-color, rgba(0, 0, 0, 0.18));
}
}
}
&:not(.leaflet-disabled) {
&:hover, &.active {
border-bottom: none;
color: $tb-primary-color; // primary color
color: var(--tb-map-control-active-color, $tb-primary-color); // primary color
&:before {
content: "";
position: absolute;
inset: 0;
border-radius: 50%;
background-color: rgba(0, 105, 92, 0.10);
background-color: var(--tb-map-control-hover-background-color, rgba(0, 105, 92));
opacity: 0.1;
}
}
}
@ -100,11 +104,17 @@
border-radius: 15px;
}
> div:not(.tb-control-text):not(.tb-close) {
background: $tb-primary-color; // primary color
background-color: var(--tb-map-control-active-color, $tb-primary-color);
svg {
fill: var(--tb-map-control-active-color, $tb-primary-color);
}
}
}
> div {
background: rgba(0, 0, 0, 0.54);
> div:not(.material-icon-font):not(.tb-control-text) {
background-color: var(--tb-map-control-color, rgba(0, 0, 0, 0.54));
svg {
fill: var(--tb-map-control-color, rgba(0, 0, 0, 0.54));
}
}
}
> div {
@ -148,7 +158,7 @@
}
}
}
.tb-map-bottom-toolbar, .tb-map-top-toolbar {
.tb-map-bottom-toolbar {
left: 0;
right: 0;
display: flex;
@ -159,6 +169,25 @@
flex-direction: row;
}
}
.tb-map-top-toolbar {
left: 52px;
right: 52px;
display: flex;
flex-direction: row;
justify-content: center;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
.leaflet-control {
margin-top: 0;
}
a.tb-control-button {
.tb-control-button-icon {
background-clip: text;
}
}
}
}
.leaflet-map-pane:not(.leaflet-zoom-anim) {
.leaflet-marker-icon {

289
ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts

@ -17,6 +17,7 @@
import {
additionalMapDataSourcesToDatasources,
BaseMapSettings,
CustomActionData,
DataKeyValuePair,
MapType,
mergeMapDatasources,
@ -47,7 +48,7 @@ import {
UnplacedMapDataItem,
} from '@home/components/widget/lib/maps/data-layer/map-data-layer';
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { FormattedData, WidgetAction, WidgetActionDescriptor, widgetType } from '@shared/models/widget.models';
import { FormattedData, MapItemType, WidgetAction, WidgetActionType, widgetType } from '@shared/models/widget.models';
import { EntityDataPageLink } from '@shared/models/query/query.models';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { TbMarkersDataLayer } from '@home/components/widget/lib/maps/data-layer/markers-data-layer';
@ -379,20 +380,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
}
});
this.addMarkerButton.setDisabled(true);
createColorMarkerShapeURI(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), MarkerShape.markerShape1, tinycolor('rgba(255,255,255,0.75)')).subscribe(
((iconUrl) => {
const icon = L.icon({
iconUrl,
iconSize: [40, 40],
iconAnchor: [20, 40]
});
this.map.pm.setGlobalOptions({
markerStyle: {
icon
}
});
})
);
this.setPlaceMarkerStyle();
}
this.addPolygonDataLayers = addSupportedDataLayers.filter(dl => dl.dataLayerType() === MapDataLayerType.polygon);
if (this.addPolygonDataLayers.length) {
@ -430,86 +418,32 @@ export abstract class TbMap<S extends BaseMapSettings> {
}
}
private setupCustomActions() {
this.customActionsToolbar = L.TB.topToolbar({
mapElement: $(this.mapElement)
});
/*const customButton = this.customActionsToolbar.toolbarButton({
title: 'Super button',
icon: 'add'
});
this.customActionsToolbar.toolbarButton({
title: 'Super button 2',
icon: 'add'
});
customButton.onClick(e => {
console.log("Called!");
});*/
}
private placeMarker(e: MouseEvent, button: L.TB.ToolbarButton): void {
this.placeItem(e, button, this.addMarkerDataLayers,
(entity) => {
this.map.pm.setLang('en', {
tooltips: {
placeMarker: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker-hint', {entityName: entity.entity.entityDisplayName})
}
}, 'en');
this.map.pm.enableDraw('Marker');
// @ts-ignore
L.DomUtil.addClass(this.map.pm.Draw.Marker._hintMarker.getTooltip()._container, 'tb-place-item-label');
}
);
this.placeItem(e, button, this.addMarkerDataLayers, (entity) => this.prepareDrawMode('Marker', {
placeMarker: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker-hint-with-entity', {entityName: entity.entity.entityDisplayName})
}));
}
private drawRectangle(e: MouseEvent, button: L.TB.ToolbarButton): void {
this.placeItem(e, button, this.addPolygonDataLayers,
(entity) => {
this.map.pm.setLang('en', {
tooltips: {
firstVertex: this.ctx.translate.instant('widgets.maps.data-layer.polygon.rectangle-place-first-point-hint', {entityName: entity.entity.entityDisplayName}),
finishRect: this.ctx.translate.instant('widgets.maps.data-layer.polygon.finish-rectangle-hint', {entityName: entity.entity.entityDisplayName})
}
}, 'en');
this.map.pm.enableDraw('Rectangle');
// @ts-ignore
L.DomUtil.addClass(this.map.pm.Draw.Rectangle._hintMarker.getTooltip()._container, 'tb-place-item-label');
}
);
this.placeItem(e, button, this.addPolygonDataLayers, (entity) => this.prepareDrawMode('Rectangle', {
firstVertex: this.ctx.translate.instant('widgets.maps.data-layer.polygon.rectangle-place-first-point-hint-with-entity', {entityName: entity.entity.entityDisplayName}),
finishRect: this.ctx.translate.instant('widgets.maps.data-layer.polygon.finish-rectangle-hint-with-entity', {entityName: entity.entity.entityDisplayName})
}));
}
private drawPolygon(e: MouseEvent, button: L.TB.ToolbarButton): void {
this.placeItem(e, button, this.addPolygonDataLayers,
(entity) => {
this.map.pm.setLang('en', {
tooltips: {
firstVertex: this.ctx.translate.instant('widgets.maps.data-layer.polygon.polygon-place-first-point-hint', {entityName: entity.entity.entityDisplayName}),
continueLine: this.ctx.translate.instant('widgets.maps.data-layer.polygon.continue-polygon-hint', {entityName: entity.entity.entityDisplayName}),
finishPoly: this.ctx.translate.instant('widgets.maps.data-layer.polygon.finish-polygon-hint', {entityName: entity.entity.entityDisplayName})
}
}, 'en');
this.map.pm.enableDraw('Polygon');
// @ts-ignore
L.DomUtil.addClass(this.map.pm.Draw.Polygon._hintMarker.getTooltip()._container, 'tb-place-item-label');
}
);
this.placeItem(e, button, this.addPolygonDataLayers, (entity) => this.prepareDrawMode('Polygon', {
firstVertex: this.ctx.translate.instant('widgets.maps.data-layer.polygon.polygon-place-first-point-hint-with-entity', {entityName: entity.entity.entityDisplayName}),
continueLine: this.ctx.translate.instant('widgets.maps.data-layer.polygon.continue-polygon-hint-with-entity', {entityName: entity.entity.entityDisplayName}),
finishPoly: this.ctx.translate.instant('widgets.maps.data-layer.polygon.finish-polygon-hint-with-entity', {entityName: entity.entity.entityDisplayName})
}));
}
private drawCircle(e: MouseEvent, button: L.TB.ToolbarButton): void {
this.placeItem(e, button, this.addCircleDataLayers,
(entity) => {
this.map.pm.setLang('en', {
tooltips: {
startCircle: this.ctx.translate.instant('widgets.maps.data-layer.circle.place-circle-center-hint', {entityName: entity.entity.entityDisplayName}),
finishCircle: this.ctx.translate.instant('widgets.maps.data-layer.circle.finish-circle-hint', {entityName: entity.entity.entityDisplayName}),
}
}, 'en');
this.map.pm.enableDraw('Circle');
// @ts-ignore
L.DomUtil.addClass(this.map.pm.Draw.Circle._hintMarker.getTooltip()._container, 'tb-place-item-label');
}
);
this.placeItem(e, button, this.addCircleDataLayers, (entity) => this.prepareDrawMode('Circle', {
startCircle: this.ctx.translate.instant('widgets.maps.data-layer.circle.place-circle-center-hint-with-entity', {entityName: entity.entity.entityDisplayName}),
finishCircle: this.ctx.translate.instant('widgets.maps.data-layer.circle.finish-circle-hint-with-entity', {entityName: entity.entity.entityDisplayName}),
}));
}
private placeItem(e: MouseEvent, button: L.TB.ToolbarButton, dataLayers: TbMapDataLayer[],
@ -523,21 +457,12 @@ export abstract class TbMap<S extends BaseMapSettings> {
});
this.selectEntityToPlace(e, items).subscribe((entity) => {
if (entity) {
const finishAdd = () => {
this.map.off('pm:create');
this.map.pm.disableDraw();
this.dataLayers.forEach(dl => dl.enableEditMode());
this.updatePlaceItemState();
this.editToolbar.close();
};
this.map.once('pm:create', (e) => {
entity.dataLayer.placeItem(entity, e.layer);
// @ts-ignore
e.layer._pmTempLayer = true;
e.layer.remove();
finishAdd();
this.finishAdd();
});
prepareDrawMode(entity);
@ -550,7 +475,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
iconClass: 'tb-close',
title: this.ctx.translate.instant('action.cancel'),
showText: true,
click: finishAdd
click: this.finishAdd
}
], false);
} else {
@ -586,6 +511,159 @@ export abstract class TbMap<S extends BaseMapSettings> {
}
}
private setupCustomActions() {
if (!this.settings.mapActionButtons) {
return;
}
this.customActionsToolbar = L.TB.topToolbar({
mapElement: $(this.mapElement),
iconRegistry: this.ctx.$injector.get(MatIconRegistry)
});
const mapActionButtons = this.settings.mapActionButtons;
if (mapActionButtons.length) {
const customTranslate = this.ctx.$injector.get(CustomTranslatePipe);
if (mapActionButtons.some(actionButton => actionButton.action.mapItemType === MapItemType.marker)) {
this.setPlaceMarkerStyle();
}
mapActionButtons.forEach(actionButton => {
const actionButtonConfig = {
icon: actionButton.icon,
color: actionButton.color,
title: customTranslate.transform(actionButton.label)
};
const toolbarButton = this.customActionsToolbar.toolbarButton(actionButtonConfig);
if (actionButton.action.type !== WidgetActionType.placeMapItem) {
toolbarButton.onClick((e) => this.ctx.actionsApi.handleWidgetAction(e, actionButton.action));
} else {
switch (actionButton.action.mapItemType) {
case MapItemType.marker:
toolbarButton.onClick((e, button) => this.createMarker(e, {button, action: actionButton.action}));
break;
case MapItemType.polygon:
toolbarButton.onClick((e, button) => this.createPolygon(e, {button, action: actionButton.action}));
break;
case MapItemType.rectangle:
toolbarButton.onClick((e, button) => this.createRectangle(e, {button, action: actionButton.action}));
break;
case MapItemType.circle:
toolbarButton.onClick((e, button) => this.createCircle(e, {button, action: actionButton.action}));
break;
}
}
});
}
}
private createMarker(e: MouseEvent, actionData: CustomActionData) {
this.createItem(e, actionData, () => this.prepareDrawMode('Marker', {
placeMarker: this.ctx.translate.instant('widgets.maps.data-layer.marker.place-marker-hint')
}));
}
private createRectangle(e: MouseEvent, actionData: CustomActionData): void {
this.createItem(e, actionData, () => this.prepareDrawMode('Rectangle', {
firstVertex: this.ctx.translate.instant('widgets.maps.data-layer.polygon.rectangle-place-first-point-hint'),
finishRect: this.ctx.translate.instant('widgets.maps.data-layer.polygon.finish-rectangle-hint')
}));
}
private createPolygon(e: MouseEvent, actionData: CustomActionData): void {
this.createItem(e, actionData, () => this.prepareDrawMode('Polygon', {
firstVertex: this.ctx.translate.instant('widgets.maps.data-layer.polygon.polygon-place-first-point-hint'),
continueLine: this.ctx.translate.instant('widgets.maps.data-layer.polygon.continue-polygon-hint'),
finishPoly: this.ctx.translate.instant('widgets.maps.data-layer.polygon.finish-polygon-hint')
}));
}
private createCircle(e: MouseEvent, actionData: CustomActionData): void {
this.createItem(e, actionData, () => this.prepareDrawMode('Circle', {
startCircle: this.ctx.translate.instant('widgets.maps.data-layer.circle.place-circle-center-hint'),
finishCircle: this.ctx.translate.instant('widgets.maps.data-layer.circle.finish-circle-hint')
}));
}
private createItem(e: MouseEvent, actionData: CustomActionData, prepareDrawMode: () => void) {
if (this.isPlacingItem) {
return;
}
this.updatePlaceItemState(actionData.button);
this.map.once('pm:create', (e) => {
this.ctx.actionsApi.handleWidgetAction(e as any, actionData.action, null, null, {
coordinates: convertLayerToCoordinates(actionData.action.mapItemType, e.layer),
layer: e.layer
});
// @ts-ignore
e.layer._pmTempLayer = true;
e.layer.remove();
this.finishAdd();
});
prepareDrawMode();
this.dataLayers.forEach(dl => dl.disableEditMode());
this.editToolbar.open([
{
id: 'cancel',
iconClass: 'tb-close',
title: this.ctx.translate.instant('action.cancel'),
showText: true,
click: this.finishAdd
}
], false);
const convertLayerToCoordinates = (type: MapItemType, layer: L.Layer): {x: number; y: number} | TbPolygonRawCoordinates | TbCircleData => {
switch (type) {
case MapItemType.marker:
if (layer instanceof L.Marker) {
return this.latLngToLocationData(layer.getLatLng());
}
return null;
case MapItemType.polygon:
if (layer instanceof L.Polygon) {
let coordinates: any = layer.getLatLngs();
if (coordinates.length === 1) {
coordinates = coordinates[0];
}
return this.coordinatesToPolygonData(coordinates);
}
return null;
case MapItemType.rectangle:
if (layer instanceof L.Rectangle) {
const bounds = layer.getBounds();
return this.coordinatesToPolygonData([bounds.getNorthWest(), bounds.getSouthEast()])
}
return null;
case MapItemType.circle:
if (layer instanceof L.Circle) {
return this.coordinatesToCircleData(layer.getLatLng(), layer.getRadius());
}
return null;
}
}
}
private finishAdd = () => {
this.map.off('pm:create');
this.map.pm.disableDraw();
this.dataLayers.forEach(dl => dl.enableEditMode());
this.updatePlaceItemState();
this.editToolbar.close();
}
private prepareDrawMode(shape: 'Marker' | 'Rectangle' | 'Polygon' | 'Circle', tooltipsTranslation: Record<string, string>) {
this.map.pm.setLang('en', { tooltips: tooltipsTranslation }, 'en');
this.map.pm.enableDraw(shape);
// @ts-ignore
L.DomUtil.addClass(this.map.pm.Draw[shape]._hintMarker.getTooltip()._container, 'tb-place-item-label');
}
private updatePlaceItemState(addButton?: L.TB.ToolbarButton): void {
if (addButton) {
this.deselectItem(false, true);
@ -744,6 +822,7 @@ export abstract class TbMap<S extends BaseMapSettings> {
if (this.addCircleButton && this.addCircleButton !== this.currentAddButton) {
this.addCircleButton.setDisabled(true);
}
this.customActionsToolbar.setDisabled(true);
} else {
if (this.addMarkerButton) {
this.addMarkerButton.setDisabled(!this.addMarkerDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems()));
@ -757,9 +836,27 @@ export abstract class TbMap<S extends BaseMapSettings> {
if (this.addCircleButton) {
this.addCircleButton.setDisabled(!this.addCircleDataLayers.some(dl => dl.isEnabled() && dl.hasUnplacedItems()));
}
this.customActionsToolbar.setDisabled(false);
}
}
private setPlaceMarkerStyle() {
createColorMarkerShapeURI(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), MarkerShape.markerShape1, tinycolor('rgba(255,255,255,0.75)')).subscribe(
((iconUrl) => {
const icon = L.icon({
iconUrl,
iconSize: [40, 40],
iconAnchor: [20, 40]
});
this.map.pm.setGlobalOptions({
markerStyle: {
icon
}
});
})
);
}
protected abstract defaultSettings(): S;
protected abstract createMap(): Observable<L.Map>;

23
ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts

@ -603,6 +603,13 @@ export const mapZoomActionTranslationMap = new Map<MapZoomAction, string>(
]
);
export interface MapActionButtonSettings {
label?: string;
icon?: string;
color: string;
action: WidgetAction;
}
export interface TripTimelineSettings {
showTimelineControl: boolean;
timeStep: number;
@ -628,6 +635,7 @@ export interface BaseMapSettings {
defaultZoomLevel: number;
minZoomLevel: number;
mapPageSize: number;
mapActionButtons: MapActionButtonSettings[];
tripTimeline?: TripTimelineSettings;
}
@ -649,6 +657,7 @@ export const defaultBaseMapSettings: BaseMapSettings = {
defaultZoomLevel: null,
minZoomLevel: 16,
mapPageSize: DEFAULT_MAP_PAGE_SIZE,
mapActionButtons: [],
tripTimeline: {
showTimelineControl: false,
timeStep: 1000,
@ -660,6 +669,15 @@ export const defaultBaseMapSettings: BaseMapSettings = {
}
};
export const defaultMapActionButtonSettings: MapActionButtonSettings = {
label: '',
icon: 'add',
color: '#0000008a',
action: {
type: WidgetActionType.doNothing
}
}
export enum MapProvider {
openstreet = 'openstreet',
google = 'google',
@ -992,6 +1010,11 @@ export interface MarkerIconInfo {
size: [number, number];
}
export interface CustomActionData {
button: L.TB.TopToolbarButton;
action: WidgetAction;
}
export type MapStringFunction = (data: FormattedData<TbMapDatasource>,
dsData: FormattedData<TbMapDatasource>[]) => string;

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.html

@ -23,7 +23,8 @@
[callbacks]="callbacks"
[widgetType]="widgetType"
[withName]="withName"
[actionNames]="actionNames">
[actionNames]="actionNames"
[additionalWidgetActionTypes]="additionalWidgetActionTypes">
</tb-widget-action>
</div>
<div class="tb-action-settings-panel-buttons">

5
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.ts

@ -17,7 +17,7 @@
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { WidgetAction, widgetType } from '@shared/models/widget.models';
import { WidgetAction, WidgetActionType, widgetType } from '@shared/models/widget.models';
import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models';
import { coerceBoolean } from '@shared/decorators/coercion';
import { TranslateService } from '@ngx-translate/core';
@ -53,6 +53,9 @@ export class WidgetActionSettingsPanelComponent implements OnInit {
@Input()
applyTitle = this.translate.instant('action.apply');
@Input()
additionalWidgetActionTypes: WidgetActionType[];
@Output()
widgetActionApplied = new EventEmitter<WidgetAction>();

13
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings.component.ts

@ -29,7 +29,12 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { TranslateService } from '@ngx-translate/core';
import { WidgetAction, widgetActionTypeTranslationMap, widgetType } from '@shared/models/widget.models';
import {
WidgetAction,
WidgetActionType,
widgetActionTypeTranslationMap,
widgetType
} from '@shared/models/widget.models';
import { WidgetActionCallbacks } from '@home/components/widget/action/manage-widget-actions.component.models';
import {
WidgetActionSettingsPanelComponent
@ -65,6 +70,9 @@ export class WidgetActionSettingsComponent implements OnInit, ControlValueAccess
@Input()
disabled = false;
@Input()
additionalWidgetActionTypes: WidgetActionType[];
modelValue: WidgetAction;
displayValue: string;
@ -110,7 +118,8 @@ export class WidgetActionSettingsComponent implements OnInit, ControlValueAccess
widgetAction: this.modelValue,
panelTitle: this.panelTitle,
widgetType: this.widgetType,
callbacks: this.callbacks
callbacks: this.callbacks,
additionalWidgetActionTypes: this.additionalWidgetActionTypes
};
const widgetActionSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, WidgetActionSettingsPanelComponent,

16
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html

@ -258,7 +258,21 @@
helpId="widget/action/custom_action_fn"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionType.customPretty">
<ng-template [ngSwitchCase]="widgetActionType.placeMapItem">
<div class="tb-form-row">
<div class="fixed-title-width">{{ 'widget-action.map-item-type' | translate }}</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select placeholder="{{ 'widget-config.set' | translate }}" formControlName="mapItemType">
@for(type of mapItemTypes; track type) {
<mat-option [value]="type">{{ mapItemTypeTranslationMap.get(type) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.customPretty
|| widgetActionFormGroup.get('type').value === widgetActionType.placeMapItem
? widgetActionFormGroup.get('type').value : ''">
<tb-custom-action-pretty-editor
formControlName="customAction">
</tb-custom-action-pretty-editor>

42
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts

@ -15,17 +15,21 @@
///
import {
ControlValueAccessor, FormControl,
ControlValueAccessor,
FormControl,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validator, ValidatorFn,
Validator,
ValidatorFn,
Validators
} from '@angular/forms';
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import {
MapItemType,
mapItemTypeTranslationMap,
WidgetAction,
WidgetActionType,
widgetActionTypes,
@ -97,10 +101,25 @@ export class WidgetActionComponent implements ControlValueAccessor, OnInit, Vali
@Input()
actionNames: string[];
@Input()
set additionalWidgetActionTypes(value: WidgetActionType[]) {
if (this.widgetActionFormGroup && !widgetActionTypes.includes(this.widgetActionFormGroup.get('type').value)) {
this.widgetActionFormGroup.get('type').setValue(WidgetActionType.doNothing);
}
if (value?.length) {
this.widgetActionTypes = widgetActionTypes.concat(value);
} else {
this.widgetActionTypes = widgetActionTypes;
}
}
widgetActionTypes = widgetActionTypes;
widgetActionTypeTranslations = widgetActionTypeTranslationMap;
widgetActionType = WidgetActionType;
mapItemTypes = Object.values(MapItemType) as MapItemType[];
mapItemTypeTranslationMap = mapItemTypeTranslationMap;
allStateDisplayTypes = stateDisplayTypes;
allPopoverPlacements = PopoverPlacements;
@ -171,6 +190,9 @@ export class WidgetActionComponent implements ControlValueAccessor, OnInit, Vali
).subscribe(() => {
this.widgetActionUpdated();
});
if (this.additionalWidgetActionTypes) {
this.widgetActionTypes = this.widgetActionTypes.concat(this.additionalWidgetActionTypes);
}
}
writeValue(widgetAction?: WidgetAction): void {
@ -307,6 +329,16 @@ export class WidgetActionComponent implements ControlValueAccessor, OnInit, Vali
this.fb.control(action ? action.url : null, [Validators.required])
);
break;
case WidgetActionType.placeMapItem:
this.actionTypeFormGroup.addControl(
'mapItemType',
this.fb.control(action?.mapItemType ?? MapItemType.marker, [Validators.required])
);
this.actionTypeFormGroup.addControl(
'customAction',
this.fb.control(toCustomAction(action), [Validators.required])
);
break;
}
}
this.actionTypeFormGroupSubscriptions.push(
@ -497,6 +529,12 @@ export class WidgetActionComponent implements ControlValueAccessor, OnInit, Vali
let result: WidgetAction;
if (type === WidgetActionType.customPretty) {
result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.get('customAction').value};
} else if (type === WidgetActionType.placeMapItem) {
result = {
...this.widgetActionFormGroup.value,
...this.actionTypeFormGroup.get('customAction').value,
mapItemType: this.actionTypeFormGroup.get('mapItemType').value
};
} else {
result = {...this.widgetActionFormGroup.value, ...this.actionTypeFormGroup.value};
}

47
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.html

@ -0,0 +1,47 @@
<!--
Copyright © 2016-2024 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]="mapActionButton" class="tb-form-table-row">
<mat-form-field class="tb-inline-field tb-label-field min-w-32" style="flex: 1 1 40%" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
matTooltip="{{ 'widgets.maps.map-action.button-requires' | translate }}"
*ngIf="mapActionButton.hasError('invalidButtonConfig')"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
<tb-material-icon-select formControlName="icon" asBoxInput iconClearButton [color]="this.mapActionButton.get('color').value"></tb-material-icon-select>
<tb-color-input formControlName="color" asBoxInput required></tb-color-input>
<tb-widget-action-settings class="min-w-32" style="flex: 1 1 60%"
formControlName="action"
panelTitle="{{ 'widgets.maps.map-action.map-action-button' | translate }}"
[widgetType]="widgetType.latest"
[additionalWidgetActionTypes]="additionalWidgetActionTypes">
</tb-widget-action-settings>
<div class="tb-form-table-row-cell-buttons w-10 min-w-10">
<button type="button"
mat-icon-button
(click)="buttonRemoved.emit()"
matTooltip="{{ 'widgets.maps.map-action.remove-action-button' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>

100
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.ts

@ -0,0 +1,100 @@
///
/// Copyright © 2016-2024 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, EventEmitter, forwardRef, Output } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator
} from '@angular/forms';
import { MapActionButtonSettings } from '@home/components/widget/lib/maps/models/map.models';
import { WidgetAction, WidgetActionType, widgetType } from '@shared/models/widget.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { isEmptyStr } from '@core/utils';
@Component({
selector: 'tb-map-action-button-row',
templateUrl: 'map-action-button-row.component.html',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MapActionButtonRowComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MapActionButtonRowComponent),
multi: true
}]
})
export class MapActionButtonRowComponent implements ControlValueAccessor, Validator {
@Output()
buttonRemoved = new EventEmitter();
mapActionButton = this.fb.group({
label: [''],
icon: [''],
color: [''],
action: this.fb.control<WidgetAction>(null)
}, {validators: this.validateButtonConfig()});
additionalWidgetActionTypes = [WidgetActionType.placeMapItem];
readonly widgetType = widgetType;
private propagateChange = (_val: any) => {};
constructor(private fb: FormBuilder) {
this.mapActionButton.valueChanges.pipe(
takeUntilDestroyed()
).subscribe(value => this.propagateChange(value))
}
registerOnChange(fn: any) {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void { }
setDisabledState(isDisabled: boolean) {
if (isDisabled) {
this.mapActionButton.disable({emitEvent: false});
} else {
this.mapActionButton.enable({emitEvent: false});
}
}
validate(): ValidationErrors | null {
return this.mapActionButton.valid ? null : {
mapButtonAction: false
};
}
writeValue(value: MapActionButtonSettings) {
this.mapActionButton.patchValue(value, {emitEvent: false});
}
private validateButtonConfig() {
return (c: FormGroup) => {
return !c.value.icon && isEmptyStr(c.value.label) ? {
invalidButtonConfig: true
} : null;
};
}
}

63
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.html

@ -0,0 +1,63 @@
<!--
Copyright © 2016-2024 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.
-->
<section class="tb-form-panel">
<mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel-header>
{{ 'widgets.maps.map-action.map-action-buttons' | translate }}
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-table overflow-y-auto">
<div class="tb-form-table-header no-padding-right">
<div class="tb-form-table-header-cell min-w-32" style="flex: 1 1 40%" translate>widgets.maps.map-action.label</div>
<div class="tb-form-table-header-cell w-10 min-w-10" translate>widgets.maps.map-action.icon</div>
<div class="tb-form-table-header-cell w-10 min-w-10" translate>widgets.maps.map-action.color</div>
<div class="tb-form-table-header-cell min-w-32" style="flex: 1 1 60%" translate>widgets.maps.map-action.action</div>
<div class="tb-form-table-header-cell w-20 min-w-20 xs:w-10 xs:min-w-10"></div>
</div>
<div class="tb-form-table-body" cdkDropList cdkDropListOrientation="vertical"
[cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="buttonDrop($event)">
@for (mapAction of mapActionButtonsForm.controls; track mapAction) {
<div cdkDrag [cdkDragDisabled]="!dragEnabled" class="tb-draggable-form-table-row">
<tb-map-action-button-row class="flex-1"
[formControl]="mapAction"
(buttonRemoved)="removeButton($event)">
</tb-map-action-button-row>
<div class="tb-form-table-row-cell-buttons">
<button class="xs:!hidden"
[class.tb-hidden]="!dragEnabled"
mat-icon-button
type="button"
cdkDragHandle
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<mat-icon>drag_indicator</mat-icon>
</button>
</div>
</div>
} @empty {
<span class="tb-prompt flex items-center justify-center">{{ 'widgets.maps.map-action.no-action-buttons-configured' | translate }}</span>
}
</div>
</div>
<button type="button" mat-stroked-button color="primary" class="max-w-fit" (click)="addButton()">
{{ 'widgets.maps.map-action.add-button' | translate }}
</button>
</ng-template>
</mat-expansion-panel>
</section>

107
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.ts

@ -0,0 +1,107 @@
///
/// Copyright © 2016-2024 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 } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator
} from '@angular/forms';
import {
defaultMapActionButtonSettings,
MapActionButtonSettings
} from '@home/components/widget/lib/maps/models/map.models';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({
selector: 'tb-map-action-button-settings',
templateUrl: './map-action-buttons-settings.component.html',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MapActionButtonsSettingsComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MapActionButtonsSettingsComponent),
multi: true
}]
})
export class MapActionButtonsSettingsComponent implements ControlValueAccessor, Validator {
mapActionButtonsForm = this.fb.array<MapActionButtonSettings>([]);
private propagateChange = (_val: any) => {};
constructor(private fb: FormBuilder) {
this.mapActionButtonsForm.valueChanges.pipe(
takeUntilDestroyed()
).subscribe(value => this.propagateChange(value));
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(_fn: any): void { }
setDisabledState(isDisabled: boolean): void {
if (isDisabled) {
this.mapActionButtonsForm.disable({emitEvent: false});
} else {
this.mapActionButtonsForm.enable({emitEvent: false});
}
}
validate(): ValidationErrors | null {
return this.mapActionButtonsForm.valid ? null : {
mapActionButtons: false
};
}
writeValue(buttons: MapActionButtonSettings[] = []) {
if (buttons?.length === this.mapActionButtonsForm.length) {
this.mapActionButtonsForm.patchValue(buttons, {emitEvent: false});
} else {
this.mapActionButtonsForm.clear({emitEvent: false});
buttons.forEach(
button => this.mapActionButtonsForm.push(this.fb.control(button), {emitEvent: false})
);
}
}
get dragEnabled(): boolean {
return this.mapActionButtonsForm.length > 1;
}
buttonDrop(event: CdkDragDrop<string[]>) {
const actionButton = this.mapActionButtonsForm.at(event.previousIndex);
this.mapActionButtonsForm.removeAt(event.previousIndex, {emitEvent: false});
this.mapActionButtonsForm.insert(event.currentIndex, actionButton);
}
addButton() {
this.mapActionButtonsForm.push(this.fb.control(defaultMapActionButtonSettings));
}
removeButton(index: number) {
this.mapActionButtonsForm.removeAt(index);
}
}

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html

@ -93,6 +93,8 @@
</mat-chip-listbox>
</div>
</div>
<tb-map-action-button-settings formControlName="mapActionButtons">
</tb-map-action-button-settings>
<div class="tb-form-panel">
<div class="tb-form-panel-title">
{{ 'widgets.maps.common.common-map-settings' | translate }}

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts

@ -139,7 +139,8 @@ export class MapSettingsComponent implements OnInit, ControlValueAccessor, Valid
useDefaultCenterPosition: [null, []],
defaultCenterPosition: [null, []],
defaultZoomLevel: [null, [Validators.min(0), Validators.max(20)]],
mapPageSize: [null, [Validators.min(1), Validators.required]]
mapPageSize: [null, [Validators.min(1), Validators.required]],
mapActionButtons: [null]
});
this.mapSettingsFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)

8
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts

@ -236,6 +236,12 @@ import {
import {
MapTooltipTagActionsComponent
} from '@home/components/widget/lib/settings/common/map/map-tooltip-tag-actions.component';
import {
MapActionButtonsSettingsComponent
} from '@home/components/widget/lib/settings/common/map/map-action-buttons-settings.component';
import {
MapActionButtonRowComponent
} from '@home/components/widget/lib/settings/common/map/map-action-button-row.component';
@NgModule({
declarations: [
@ -313,6 +319,8 @@ import {
DataLayerColorSettingsComponent,
DataLayerColorSettingsPanelComponent,
MapTooltipTagActionsComponent,
MapActionButtonsSettingsComponent,
MapActionButtonRowComponent,
DataLayerPatternSettingsComponent,
MarkerShapeSettingsComponent,
MarkerShapesComponent,

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

@ -40,11 +40,12 @@ import {
migrateWidgetTypeToDynamicForms,
Widget,
widgetActionSources,
WidgetActionType,
WidgetControllerDescriptor,
WidgetType
} from '@shared/models/widget.models';
import { catchError, map, mergeMap, switchMap, tap } from 'rxjs/operators';
import { isFunction, isUndefined } from '@core/utils';
import { isDefinedAndNotNull, isFunction, isUndefined } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component';
import { WidgetComponentsModule } from '@home/components/widget/widget-components.module';
@ -654,6 +655,13 @@ export class WidgetComponentService {
if (isUndefined(result.typeParameters.targetDeviceOptional)) {
result.typeParameters.targetDeviceOptional = false;
}
if (isDefinedAndNotNull(result.typeParameters.additionalWidgetActionTypes)) {
if (Array.isArray(result.typeParameters.additionalWidgetActionTypes)) {
result.typeParameters.additionalWidgetActionTypes = result.typeParameters.additionalWidgetActionTypes.filter(type => WidgetActionType[type]);
} else {
result.typeParameters.additionalWidgetActionTypes = null;
}
}
if (isFunction(widgetTypeInstance.actionSources)) {
result.actionSources = widgetTypeInstance.actionSources();
} else {

1
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -164,6 +164,7 @@
[callbacks]="widgetConfigCallbacks"
[widgetType] = "modelValue.widgetType"
[actionSources]="modelValue.actionSources"
[additionalWidgetActionTypes]="modelValue.typeParameters.additionalWidgetActionTypes"
formControlName="actions">
</tb-manage-widget-actions>
</div>

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

@ -1149,6 +1149,7 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
)
}
break;
case WidgetActionType.placeMapItem:
case WidgetActionType.customPretty:
const customPrettyFunction = descriptor.customFunction;
const customHtml = descriptor.customHtml;

27
ui-ngx/src/app/shared/models/widget.models.ts

@ -192,6 +192,7 @@ export interface WidgetTypeParameters {
dataKeySettingsFunction?: DataKeySettingsFunction;
displayRpcMessageToast?: boolean;
targetDeviceOptional?: boolean;
additionalWidgetActionTypes?: WidgetActionType[];
}
export interface WidgetControllerDescriptor {
@ -576,7 +577,8 @@ export enum WidgetActionType {
custom = 'custom',
customPretty = 'customPretty',
mobileAction = 'mobileAction',
openURL = 'openURL'
openURL = 'openURL',
placeMapItem = 'placeMapItem'
}
export enum WidgetMobileActionType {
@ -590,7 +592,15 @@ export enum WidgetMobileActionType {
takeScreenshot = 'takeScreenshot'
}
export const widgetActionTypes = Object.keys(WidgetActionType) as WidgetActionType[];
export enum MapItemType {
marker = 'marker',
polygon = 'polygon',
rectangle = 'rectangle',
circle = 'circle'
}
export const widgetActionTypes = Object.keys(WidgetActionType)
.filter(value => value !== WidgetActionType.placeMapItem) as WidgetActionType[];
export const widgetActionTypeTranslationMap = new Map<WidgetActionType, string>(
[
@ -601,7 +611,8 @@ export const widgetActionTypeTranslationMap = new Map<WidgetActionType, string>(
[ WidgetActionType.custom, 'widget-action.custom' ],
[ WidgetActionType.customPretty, 'widget-action.custom-pretty' ],
[ WidgetActionType.mobileAction, 'widget-action.mobile-action' ],
[ WidgetActionType.openURL, 'widget-action.open-URL' ]
[ WidgetActionType.openURL, 'widget-action.open-URL' ],
[ WidgetActionType.placeMapItem, 'widget-action.place-map-item' ],
]
);
@ -618,6 +629,15 @@ export const widgetMobileActionTypeTranslationMap = new Map<WidgetMobileActionTy
]
);
export const mapItemTypeTranslationMap = new Map<MapItemType, string>(
[
[ MapItemType.marker, 'widget-action.map-item.marker' ],
[ MapItemType.polygon, 'widget-action.map-item.polygon' ],
[ MapItemType.rectangle, 'widget-action.map-item.rectangle' ],
[ MapItemType.circle, 'widget-action.map-item.circle' ],
]
)
export interface MobileLaunchResult {
launched: boolean;
}
@ -715,6 +735,7 @@ export interface WidgetAction extends CustomActionDescriptor {
stateEntityParamName?: string;
mobileAction?: WidgetMobileActionDescriptor;
url?: string;
mapItemType?: MapItemType;
}
export interface WidgetActionDescriptor extends WidgetAction {

46
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -6426,7 +6426,15 @@
"take-screenshot": "Take screenshot"
},
"custom-action-function": "Custom action function",
"custom-pretty-function": "Custom action (with HTML template) function"
"custom-pretty-function": "Custom action (with HTML template) function",
"map-item-type": "Map item type",
"map-item": {
"marker": "Marker",
"polygon": "Polygon",
"rectangle": "Rectangle",
"circle": "Circle"
},
"place-map-item": "Place map item"
},
"widgets-bundle": {
"current": "Current bundle",
@ -7698,6 +7706,18 @@
"zoom-double-click": "Double click",
"zoom-control-buttons": "Control buttons"
},
"map-action": {
"map-action-buttons": "Map action buttons",
"label": "Label",
"icon": "Icon",
"color": "Color",
"action": "Action",
"add-button": "Add button",
"no-action-buttons-configured": "No action buttons configured",
"remove-action-button": "Remove action button",
"map-action-button": "Map action button",
"button-requires": "Button requires label or icon"
},
"common": {
"common-map-settings": "Common map settings",
"fit-map-bounds": "Fit map bounds to cover all markers",
@ -7856,7 +7876,8 @@
"edit": "Edit marker",
"remove-marker-for": "Remove marker for '{{entityName}}'",
"place-marker": "Place marker",
"place-marker-hint": "Click to place '{{entityName}}' entity"
"place-marker-hint": "Click to place marker",
"place-marker-hint-with-entity": "Click to place '{{entityName}}' entity"
},
"path": {
"decorator-symbol-arrow-head": "Arrow",
@ -7878,11 +7899,16 @@
"polygon-place-first-point-cut-hint": "Click to place first point",
"continue-polygon-cut-hint": "Click to continue drawing",
"finish-polygon-cut-hint": "Click first marker to finish and save",
"polygon-place-first-point-hint": "Polygon for '{{entityName}}': click to place first point",
"continue-polygon-hint": "Polygon for '{{entityName}}': click to continue drawing",
"finish-polygon-hint": "Polygon for '{{entityName}}': click first marker to finish and save",
"rectangle-place-first-point-hint": "Rectangle for '{{entityName}}': click to place first point",
"finish-rectangle-hint": "Rectangle for '{{entityName}}': click to finish and save"
"polygon-place-first-point-hint": "Polygon: click to place first point",
"polygon-place-first-point-hint-with-entity": "Polygon for '{{entityName}}': click to place first point",
"continue-polygon-hint": "Polygon: click to continue drawing",
"continue-polygon-hint-with-entity": "Polygon for '{{entityName}}': click to continue drawing",
"finish-polygon-hint": "Polygon: click first marker to finish drawing",
"finish-polygon-hint-with-entity": "Polygon for '{{entityName}}': click first marker to finish and save",
"rectangle-place-first-point-hint": "Rectangle: click to place first point",
"rectangle-place-first-point-hint-with-entity": "Rectangle for '{{entityName}}': click to place first point",
"finish-rectangle-hint": "Rectangle: click to finish drawing",
"finish-rectangle-hint-with-entity": "Rectangle for '{{entityName}}': click to finish and save"
},
"circle": {
"circle-key": "Circle key",
@ -7894,8 +7920,10 @@
"edit": "Edit circle",
"remove-circle-for": "Remove circle for '{{entityName}}'",
"draw-circle": "Draw circle",
"place-circle-center-hint": "Circle for '{{entityName}}': click to place circle center",
"finish-circle-hint": "Circle for '{{entityName}}': click to finish and save circle"
"place-circle-center-hint-with-entity": "Circle for '{{entityName}}': click to place circle center",
"place-circle-center-hint": "Circle: click to place circle center",
"finish-circle-hint-with-entity": "Circle for '{{entityName}}': click to finish and save circle",
"finish-circle-hint": "Circle: click to finish drawing"
},
"select-entity": "Select entity",
"select-entity-hint": "Hint: after selection click at the map to set position"

13
ui-ngx/src/typings/leaflet-extend-tb.d.ts

@ -17,6 +17,7 @@
import { FormattedData } from '@shared/models/widget.models';
import L, { Control, ControlOptions } from 'leaflet';
import { TbMapDatasource } from '@home/components/widget/lib/maps/models/map.models';
import { MatIconRegistry } from '@angular/material/icon';
// redeclare module, maintains compatibility with @types/leaflet
declare module 'leaflet' {
@ -91,22 +92,28 @@ declare module 'leaflet' {
interface TopToolbarButtonOptions {
icon: string;
iconColor?: string;
color?: string;
title: string;
}
class TopToolbarButton {
constructor(options: TopToolbarButtonOptions);
onClick(onClick: (e: MouseEvent) => void): void;
constructor(options: TopToolbarButtonOptions, iconRegistry: MatIconRegistry);
onClick(onClick: (e: MouseEvent, button: TopToolbarButton) => void): void;
setActive(active: boolean): void;
isActive(): boolean;
setDisabled(disabled: boolean): void;
isDisabled(): boolean;
}
interface TopToolbarControlOptions {
mapElement: JQuery<HTMLElement>;
iconRegistry: MatIconRegistry;
}
class TopToolbarControl {
constructor(options: TopToolbarControlOptions);
toolbarButton(options: TopToolbarButtonOptions): TopToolbarButton;
setDisabled(disabled: boolean): void;
}
interface ToolbarButtonOptions {

Loading…
Cancel
Save