From 482cb6d43f17d0677e7018cd7cd5342a5ebcf6ba Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 4 Feb 2026 15:48:10 +0200 Subject: [PATCH 1/6] Fixed loading and placement of Material icons on Power Button widget --- .../lib/rpc/power-button-widget.models.ts | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts index f538e8cfc8..4836cc3804 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts @@ -28,7 +28,7 @@ import { Circle, Effect, Element, G, Gradient, Path, Runner, Svg, Text, Timeline import '@svgdotjs/svg.filter.js'; import tinycolor from 'tinycolor2'; import { WidgetContext } from '@home/models/widget-component.models'; -import { Observable, of } from 'rxjs'; +import { Observable, of, shareReplay } from 'rxjs'; import { isSvgIcon, splitIconName } from '@shared/models/icon.models'; import { catchError, map, take } from 'rxjs/operators'; import { MatIconRegistry } from '@angular/material/icon'; @@ -336,6 +336,11 @@ export abstract class PowerButtonShape { protected onPowerSymbolCircle: Path; protected onPowerSymbolLine: Path; + private onIcon$: Observable; + private offIcon$: Observable; + protected onIconOffsetX: number = 0; + protected onIconOffsetY: number = 0; + protected constructor(protected widgetContext: WidgetContext, protected svgShape: Svg, protected iconRegistry: MatIconRegistry, @@ -360,10 +365,14 @@ export abstract class PowerButtonShape { take(1), map((svgElement) => { const element = new Element(svgElement.firstChild); + const iconGroup = this.svgShape.group(); + element.addTo(iconGroup); const box = element.bbox(); const scale = size / box.height; element.scale(scale); - return element; + const scaledBox = iconGroup.bbox(); + iconGroup.translate(-scaledBox.cx, -scaledBox.cy); + return iconGroup; }), catchError(() => of(null) )); @@ -414,8 +423,15 @@ export abstract class PowerButtonShape { public drawOffShape(centerGroup: G, label: boolean, labelWeight?: string, circleStroke?: boolean) { if (this.icons.offButtonIcon.showIcon) { - this.createIconElement(this.icons.offButtonIcon.icon, this.icons.offButtonIcon.iconSize).subscribe(icon => - this.offPowerSymbolIcon = icon.center(cx, cy).addTo(centerGroup)); + this.offIcon$ = this.createIconElement(this.icons.offButtonIcon.icon, this.icons.offButtonIcon.iconSize) + .pipe(shareReplay(1)); + + this.offIcon$.pipe(take(1)).subscribe(icon => { + this.offPowerSymbolIcon = icon; + icon.translate(cx, cy); + centerGroup.add(icon); + }); + } else { if (label) { this.offLabelShape = this.createOffLabel(labelWeight).addTo(centerGroup); @@ -428,10 +444,16 @@ export abstract class PowerButtonShape { public drawOnShape(onCenterGroup?: G, label?: boolean, labelWeight?: string, circleStroke?: boolean, mask?: Circle) { if (this.icons.onButtonIcon.showIcon) { - this.createIconElement(this.icons.onButtonIcon.icon, this.icons.onButtonIcon.iconSize).subscribe(icon => { - this.onPowerSymbolIcon = icon.center(cx, cy); + this.onIcon$ = this.createIconElement(this.icons.onButtonIcon.icon, this.icons.onButtonIcon.iconSize) + .pipe(shareReplay(1)); + + this.onIcon$.subscribe(icon => { + const iconBox = icon.bbox(); + this.onIconOffsetX = iconBox.cx; + this.onIconOffsetY = iconBox.cy; + this.onPowerSymbolIcon = icon.translate(cx, cy); if (isDefinedAndNotNull(onCenterGroup)) { - this.onPowerSymbolIcon.addTo(onCenterGroup); + onCenterGroup.add(this.onPowerSymbolIcon); } if (isDefinedAndNotNull(mask)) { this.createMask(mask, [this.onPowerSymbolIcon]); @@ -462,7 +484,9 @@ export abstract class PowerButtonShape { public onCenterTimeLine(timeline: Timeline, label: boolean) { if (this.icons.onButtonIcon.showIcon) { - this.onPowerSymbolIcon.timeline(timeline); + if (this.onIcon$) { + this.onIcon$.subscribe(icon => icon.timeline(timeline)); + } } else { if (label) { this.onLabelShape.timeline(timeline); @@ -475,7 +499,7 @@ export abstract class PowerButtonShape { public offCenterColor(mainColor: PowerButtonColor, label: boolean) { if (this.icons.offButtonIcon.showIcon) { - this.offPowerSymbolIcon.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offIcon$.subscribe(icon => icon.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity})) } else { if (label) { this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); @@ -488,7 +512,7 @@ export abstract class PowerButtonShape { public onCenterColor(mainColor: PowerButtonColor, label: boolean) { if (this.icons.onButtonIcon.showIcon) { - this.onPowerSymbolIcon.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.onIcon$.subscribe((icon)=> icon.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity})) } else { if (label) { this.onLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); @@ -501,7 +525,12 @@ export abstract class PowerButtonShape { public buttonAnimation(scale: number, label: boolean) { if (this.icons.onButtonIcon.showIcon) { - powerButtonAnimation(this.onPowerSymbolIcon).transform({scale}); + const translateX = cx - this.onIconOffsetX; + const translateY = cy - this.onIconOffsetY; + powerButtonAnimation(this.onPowerSymbolIcon).transform({ + scale: scale, + translate: [translateX, translateY] + }); } else { if (label) { powerButtonAnimation(this.onLabelShape).transform({scale, origin: {x: cx, y: cy}}); From 4e01efd64c3156ce0515220aa8dfa7bd4f01fc25 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Fri, 6 Feb 2026 17:36:44 +0200 Subject: [PATCH 2/6] Fixed Redirect Url encoding --- ui-ngx/src/app/modules/login/pages/login/login.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.ts b/ui-ngx/src/app/modules/login/pages/login/login.component.ts index afe029791b..5f83872261 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.ts @@ -78,7 +78,7 @@ export class LoginComponent extends PageComponent implements OnInit { getOAuth2Uri(oauth2Client: OAuth2ClientLoginInfo): string { let result = ""; if (this.authService.redirectUrl) { - result += "?prevUri=" + this.authService.redirectUrl; + result += "?prevUri=" + encodeURIComponent(this.authService.redirectUrl); } return oauth2Client.url + result; } From 6380a5d9c73a85761d7aa1eda9911acc358c06cd Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Thu, 5 Feb 2026 14:46:42 +0200 Subject: [PATCH 3/6] Fixed Popover placement for Marker, Polygon and Circle overlay config --- .../settings/common/map/marker-image-settings.component.ts | 5 +++-- .../common/map/shape-fill-image-settings.component.ts | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.ts index 6da6c809e0..989e9ef0c7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/marker-image-settings.component.ts @@ -82,11 +82,12 @@ export class MarkerImageSettingsComponent implements ControlValueAccessor { renderer: this.renderer, componentType: MarkerImageSettingsPanelComponent, hostView: this.viewContainerRef, - preferredPlacement: 'left', + preferredPlacement: 'leftOnly', context: { markerImageSettings: this.modelValue, }, - isModal: true + isModal: true, + overlayStyle: {padding: '10px'} }); markerImageSettingsPanelPopover.tbComponentRef.instance.popover = markerImageSettingsPanelPopover; markerImageSettingsPanelPopover.tbComponentRef.instance.markerImageSettingsApplied.subscribe((markerImageSettings) => { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.ts index 5c9bbb260f..74f4ddd1eb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/shape-fill-image-settings.component.ts @@ -82,11 +82,12 @@ export class ShapeFillImageSettingsComponent implements ControlValueAccessor { renderer: this.renderer, componentType: ShapeFillImageSettingsPanelComponent, hostView: this.viewContainerRef, - preferredPlacement: 'left', + preferredPlacement: 'leftOnly', context: { shapeFillImageSettings: this.modelValue, }, - isModal: true + isModal: true, + overlayStyle: {padding: '10px'} }).tbComponentRef.instance.shapeFillImageSettingsApplied.subscribe((shapeFillImageSettings) => { this.modelValue = shapeFillImageSettings; this.propagateChange(this.modelValue); From 774886d64ae5a8c682346d2c092857d457ae2486 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 10 Feb 2026 12:37:34 +0200 Subject: [PATCH 4/6] UI: Fixed dynamic dialog after migration to Angular 20 --- .../dialog/dynamic/dynamic-dialog.module.ts | 7 ++- .../dialog/dynamic/dynamic-dialog.ts | 45 ++++--------------- .../dialog/dynamic/dynamic-overlay.ts | 43 +++--------------- 3 files changed, 18 insertions(+), 77 deletions(-) diff --git a/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.module.ts b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.module.ts index c1defae012..078234dbda 100644 --- a/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.module.ts +++ b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.module.ts @@ -14,9 +14,9 @@ /// limitations under the License. /// -import { OverlayModule } from '@angular/cdk/overlay'; +import { Overlay, OverlayContainer, OverlayModule } from '@angular/cdk/overlay'; import { NgModule } from '@angular/core'; -import { DEFAULT_DIALOG_CONFIG, DialogConfig, DialogModule } from '@angular/cdk/dialog'; +import { DEFAULT_DIALOG_CONFIG, Dialog, DialogConfig, DialogModule } from '@angular/cdk/dialog'; import { MatDialogModule } from '@angular/material/dialog'; import { DynamicDialog, DynamicMatDialog } from './dynamic-dialog'; import { DynamicOverlay } from './dynamic-overlay'; @@ -24,8 +24,11 @@ import { DynamicOverlayContainer } from './dynamic-overlay-container'; export const DYNAMIC_MAT_DIALOG_PROVIDERS = [ DynamicOverlayContainer, + { provide: OverlayContainer, useExisting: DynamicOverlayContainer }, DynamicOverlay, + { provide: Overlay, useExisting: DynamicOverlay }, DynamicDialog, + { provide: Dialog, useExisting: DynamicDialog }, DynamicMatDialog, { provide: DEFAULT_DIALOG_CONFIG, diff --git a/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.ts b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.ts index 2fdc63f381..f717ccb0a7 100644 --- a/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.ts +++ b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.ts @@ -14,18 +14,11 @@ /// limitations under the License. /// -import { Location } from "@angular/common"; -import { Inject, Injectable, Injector, Optional, SkipSelf, TemplateRef } from '@angular/core'; -import { - MAT_DIALOG_DEFAULT_OPTIONS, - MAT_DIALOG_SCROLL_STRATEGY, - MatDialog, - MatDialogConfig, MatDialogRef -} from '@angular/material/dialog'; -import { DynamicOverlay } from "./dynamic-overlay"; -import { DynamicOverlayContainer } from '@shared/components/dialog/dynamic/dynamic-overlay-container'; -import { ComponentType, ScrollStrategy } from '@angular/cdk/overlay'; -import { DEFAULT_DIALOG_CONFIG, Dialog, DialogConfig } from '@angular/cdk/dialog'; +import { inject, Injectable, TemplateRef } from '@angular/core'; +import { MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog'; +import { DynamicOverlay } from './dynamic-overlay'; +import { ComponentType } from '@angular/cdk/overlay'; +import { Dialog } from '@angular/cdk/dialog'; export interface DynamicMatDialogConfig extends MatDialogConfig { containerElement?: HTMLElement; @@ -34,25 +27,12 @@ export interface DynamicMatDialogConfig extends MatDialogConfig { @Injectable() export class DynamicMatDialog extends MatDialog { - private _customOverlay: DynamicOverlay; + private _customOverlay = inject(DynamicOverlay); - constructor( _overlay: DynamicOverlay, - _injector: Injector, - @Optional() location: Location, - @Inject( MAT_DIALOG_DEFAULT_OPTIONS ) _defaultOptions: MatDialogConfig, - @Inject( MAT_DIALOG_SCROLL_STRATEGY ) _scrollStrategy: ScrollStrategy, - @Optional() @SkipSelf() _parentDialog:DynamicMatDialog, - _overlayContainer: DynamicOverlayContainer) { - - super( _overlay, _injector, location, _defaultOptions, _scrollStrategy, _parentDialog, _overlayContainer ); - this._dialog = _injector.get(DynamicDialog); - this._customOverlay = _overlay; - } - - public open(component: ComponentType | TemplateRef, config?: DynamicMatDialogConfig): MatDialogRef { + public override open(component: ComponentType | TemplateRef, config?: DynamicMatDialogConfig): MatDialogRef { if (config?.containerElement) { config.containerElement.style.transform = 'translateZ(0)'; - this._customOverlay.setContainerElement( config.containerElement ); + this._customOverlay.setContainerElement(config.containerElement); } const ref = super.open(component, config); if (config?.containerElement) { @@ -73,13 +53,4 @@ export class DynamicMatDialog extends MatDialog { @Injectable() export class DynamicDialog extends Dialog { - constructor( _overlay: DynamicOverlay, - _injector: Injector, - @Inject( DEFAULT_DIALOG_CONFIG ) _defaultOptions: DialogConfig, - @Inject( MAT_DIALOG_SCROLL_STRATEGY ) _scrollStrategy: ScrollStrategy, - @Optional() @SkipSelf() _parentDialog: DynamicDialog, - _overlayContainer: DynamicOverlayContainer) { - - super( _overlay, _injector, _defaultOptions, _parentDialog, _overlayContainer, _scrollStrategy ); - } } diff --git a/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay.ts b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay.ts index 5ec5408be7..4d5a6836c9 100644 --- a/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay.ts +++ b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay.ts @@ -14,49 +14,16 @@ /// limitations under the License. /// -import { - Overlay, - ScrollStrategyOptions, - OverlayKeyboardDispatcher, OverlayOutsideClickDispatcher, OverlayPositionBuilder -} from '@angular/cdk/overlay'; -import { ComponentFactoryResolver, Inject, Injectable, Injector, NgZone, DOCUMENT } from '@angular/core'; +import { Overlay } from '@angular/cdk/overlay'; +import { inject, Injectable } from '@angular/core'; import { DynamicOverlayContainer } from './dynamic-overlay-container'; -import { Location } from '@angular/common'; -import { Directionality } from '@angular/cdk/bidi'; @Injectable() export class DynamicOverlay extends Overlay { - private _dynamicOverlayContainer: DynamicOverlayContainer; + private _dynamicOverlayContainer = inject(DynamicOverlayContainer); - constructor( scrollStrategies: ScrollStrategyOptions, - _overlayContainer: DynamicOverlayContainer, - _componentFactoryResolver: ComponentFactoryResolver, - _positionBuilder: OverlayPositionBuilder, - _keyboardDispatcher: OverlayKeyboardDispatcher, - _injector: Injector, - _ngZone: NgZone, - @Inject(DOCUMENT) document: Document, - _directionality: Directionality, - _location: Location, - _outsideClickDispatcher: OverlayOutsideClickDispatcher) { - - super( scrollStrategies, - _overlayContainer, - _componentFactoryResolver, - _positionBuilder, - _keyboardDispatcher, - _injector, - _ngZone, - document, - _directionality, - _location, - _outsideClickDispatcher); - - this._dynamicOverlayContainer = _overlayContainer; - } - - public setContainerElement(containerElement:HTMLElement ): void { - this._dynamicOverlayContainer.setContainerElement( containerElement ); + public setContainerElement(containerElement: HTMLElement): void { + this._dynamicOverlayContainer.setContainerElement(containerElement); } } From 685bddbf8def902734367c7c11b74ea94e97e1be Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 10 Feb 2026 16:17:15 +0200 Subject: [PATCH 5/6] UI: Fixed ngx-hm-carousel after migration to Angular 20 --- ui-ngx/patches/ngx-hm-carousel+19.0.0.patch | 31 +++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 ui-ngx/patches/ngx-hm-carousel+19.0.0.patch diff --git a/ui-ngx/patches/ngx-hm-carousel+19.0.0.patch b/ui-ngx/patches/ngx-hm-carousel+19.0.0.patch new file mode 100644 index 0000000000..0dfee707e1 --- /dev/null +++ b/ui-ngx/patches/ngx-hm-carousel+19.0.0.patch @@ -0,0 +1,31 @@ +diff --git a/node_modules/ngx-hm-carousel/fesm2022/ngx-hm-carousel.mjs b/node_modules/ngx-hm-carousel/fesm2022/ngx-hm-carousel.mjs +index 117f782..70e49a7 100644 +--- a/node_modules/ngx-hm-carousel/fesm2022/ngx-hm-carousel.mjs ++++ b/node_modules/ngx-hm-carousel/fesm2022/ngx-hm-carousel.mjs +@@ -1,5 +1,5 @@ + import * as i0 from '@angular/core'; +-import { Directive, inject, ViewContainerRef, TemplateRef, input, PLATFORM_ID, DestroyRef, Renderer2, NgZone, ChangeDetectorRef, viewChild, ElementRef, contentChildren, contentChild, computed, signal, effect, forwardRef, Component, ChangeDetectionStrategy } from '@angular/core'; ++import { Directive, inject, ViewContainerRef, TemplateRef, input, PLATFORM_ID, DestroyRef, Renderer2, NgZone, ChangeDetectorRef, viewChild, ElementRef, contentChildren, contentChild, computed, signal, effect, forwardRef, Component, ChangeDetectionStrategy, afterNextRender } from '@angular/core'; + import { DOCUMENT, isPlatformBrowser, NgTemplateOutlet, AsyncPipe } from '@angular/common'; + import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop'; + import { NG_VALUE_ACCESSOR } from '@angular/forms'; +@@ -340,7 +340,7 @@ class NgxHmCarouselComponent { + }); + }); + }); +- const effectRef = effect(() => { ++ afterNextRender(() => { + this.rootElm = this.container().nativeElement; + this.containerElm = this.rootElm.children[0]; + this.init(); +@@ -365,10 +365,6 @@ class NgxHmCarouselComponent { + ]) + .pipe(takeUntilDestroyed(this._destroyRef)) + .subscribe(); +- // only exec once +- effectRef.destroy(); +- }, { +- allowSignalWrites: true, + }); + } + ngOnDestroy() { From 95d07fc626473163eaaed9c6ea286ef6bad7d8d4 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 10 Feb 2026 18:27:33 +0200 Subject: [PATCH 6/6] UI: Fixed minor bug after migration to Angular 20 --- .../modules/home/pages/mobile/common/editor-panel.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts index 9f2e850b53..dbc1a5d9c8 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts @@ -48,7 +48,7 @@ export class EditorPanelComponent implements OnInit { tinyMceOptions: Partial = { base_url: '/assets/tinymce', suffix: '.min', - plugins: ['link', 'table', 'image', 'imagetools', 'lists', 'fullscreen'], + plugins: ['link', 'table', 'image', 'lists', 'fullscreen'], menubar: 'edit insert view format', toolbar: ['fontfamily fontsize | bold italic underline strikethrough forecolor backcolor', 'alignleft aligncenter alignright alignjustify | bullist | link table image | fullscreen'],