diff --git a/application/src/main/data/json/system/widget_types/image_map.json b/application/src/main/data/json/system/widget_types/image_map.json index 0c10599757..2d66ca43da 100644 --- a/application/src/main/data/json/system/widget_types/image_map.json +++ b/application/src/main/data/json/system/widget_types/image_map.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "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", diff --git a/application/src/main/data/json/system/widget_types/map.json b/application/src/main/data/json/system/widget_types/map.json index fb19914741..379ae10b0a 100644 --- a/application/src/main/data/json/system/widget_types/map.json +++ b/application/src/main/data/json/system/widget_types/map.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "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", diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html index 5b978aaa13..3d88f08c8d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.html +++ b/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"> diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts index cdc77f51a7..bc3de16c69 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions-dialog.component.ts +++ b/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({ diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts index b3d8c5e654..730be78656 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts +++ b/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) => { diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html index ec1b165401..db7e68516c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html @@ -121,7 +121,8 @@ + [widgetType]="data.widgetType" + [additionalWidgetActionTypes]="data.additionalWidgetActionTypes"> diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts index df8df4a882..ab2b8870d0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts +++ b/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({ diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts index 438808fb37..353ca04cb4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts +++ b/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) => { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts index 36ab663ce1..06d49d0343 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet/leaflet-tb.ts +++ b/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 { class TopToolbarButton { private readonly button: JQuery; - 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 = $('
'); + const setIcon = isNotEmptyStr(this.options.icon); + const setTitle = isNotEmptyStr(this.options.title); this.button = $("") .attr('class', 'tb-control-button tb-control-text-button') .attr('href', '#') .attr('role', 'button'); - this.button.append(iconElement); - this.button.append(`
${this.options.title}
`); - this.loadIcon(iconElement); + if (setIcon) { + this.button.append(iconElement); + this.loadIcon(iconElement); + } + if (setTitle) { + this.button.append(`
${this.options.title}
`); + } + 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) { - // 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 { @@ -382,20 +459,30 @@ class ToolbarButton { class TopToolbarControl { private readonly toolbarElement: JQuery; + private buttons: Array = []; constructor(private readonly options: TB.TopToolbarControlOptions) { - const controlContainer = $('.leaflet-control-container', options.mapElement); + const controlContainer = $('.leaflet-control-container', this.options.mapElement); this.toolbarElement = $('
'); this.toolbarElement.appendTo(controlContainer); } toolbarButton(options: TB.TopToolbarButtonOptions): TopToolbarButton { - const button = new TopToolbarButton(options); + const button = new TopToolbarButton(options, this.options.iconRegistry); const buttonContainer = $('
'); 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 { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss index ac8684e2bb..9c764cbb0e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.scss +++ b/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 { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts index 968f911c1f..7e884c473d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map.ts +++ b/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 { } }); 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 { } } - 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 { }); 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 { 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 { } } + 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) { + 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 { 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 { 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; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts index 69c38bfbf7..2361440828 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/models/map.models.ts @@ -603,6 +603,13 @@ export const mapZoomActionTranslationMap = new Map( ] ); +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, dsData: FormattedData[]) => string; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.html index 1fa1d11674..8a5684801e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.html +++ b/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">
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.ts index 445dd0e2ed..a9befcf16d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings-panel.component.ts +++ b/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(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings.component.ts index 109b25289e..7731df6407 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action-settings.component.ts +++ b/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, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html index 6da6254ed6..ca2405b8b4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.html +++ b/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" > - + +
+
{{ 'widget-action.map-item-type' | translate }}
+ + + @for(type of mapItemTypes; track type) { + {{ mapItemTypeTranslationMap.get(type) | translate }} + } + + +
+
+ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts index 347100c83e..932c0e52c3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/widget-action.component.ts +++ b/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}; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.html new file mode 100644 index 0000000000..70cc12dd27 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.html @@ -0,0 +1,47 @@ + +
+ + + + warning + + + + + + +
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-button-row.component.ts new file mode 100644 index 0000000000..a28e6fccb6 --- /dev/null +++ b/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(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; + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.html new file mode 100644 index 0000000000..ddc4777134 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.html @@ -0,0 +1,63 @@ + +
+ + + {{ 'widgets.maps.map-action.map-action-buttons' | translate }} + + +
+
+
widgets.maps.map-action.label
+
widgets.maps.map-action.icon
+
widgets.maps.map-action.color
+
widgets.maps.map-action.action
+
+
+
+ @for (mapAction of mapActionButtonsForm.controls; track mapAction) { +
+ + +
+ +
+
+ } @empty { + {{ 'widgets.maps.map-action.no-action-buttons-configured' | translate }} + } +
+
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-action-buttons-settings.component.ts new file mode 100644 index 0000000000..8c304a7ca5 --- /dev/null +++ b/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([]); + + 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) { + 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); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html index 95cc0b2de7..79858bfaf5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.html @@ -93,6 +93,8 @@
+ +
{{ 'widgets.maps.common.common-map-settings' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts index 710af3478b..c79d6b914c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-settings.component.ts +++ b/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) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts index 933536e86e..536623e86b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts +++ b/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, diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts index fd23239eca..1a397d3a1f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts +++ b/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 { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index a3d924fe08..ae4e35aa9c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/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">
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget.component.ts index ad52d57436..8a9b6e194c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget.component.ts +++ b/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; diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 1510fe6a8d..415d400523 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/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( [ @@ -601,7 +611,8 @@ export const widgetActionTypeTranslationMap = new Map( [ 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( + [ + [ 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 { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index dfe60066e0..cc6acce2b2 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/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" diff --git a/ui-ngx/src/typings/leaflet-extend-tb.d.ts b/ui-ngx/src/typings/leaflet-extend-tb.d.ts index 7eec52a0ce..de63139bd7 100644 --- a/ui-ngx/src/typings/leaflet-extend-tb.d.ts +++ b/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; + iconRegistry: MatIconRegistry; } class TopToolbarControl { constructor(options: TopToolbarControlOptions); toolbarButton(options: TopToolbarButtonOptions): TopToolbarButton; + setDisabled(disabled: boolean): void; } interface ToolbarButtonOptions {