From 66d263f81b33d0033726043570bb9867aa80b47f Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 19 Mar 2026 18:09:17 +0200 Subject: [PATCH 1/4] UI: Fix string-items-list autocomplete selection and blur handling Replace addOnBlur with matChipInputAddOnBlur directive and dedicated onOptionSelected handler for proper autocomplete behavior. Prevent mousedown default on options to avoid premature blur, and conditionally hide "not found" when user values are allowed. --- .../string-items-list.component.html | 11 +++-- .../components/string-items-list.component.ts | 45 ++++++++++--------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.html b/ui-ngx/src/app/shared/components/string-items-list.component.html index 7a0ac10e83..7a0489345f 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.html +++ b/ui-ngx/src/app/shared/components/string-items-list.component.html @@ -31,7 +31,6 @@ close + [matAutocompleteDisabled]="!predefinedValues?.length && !fetchOptionsFn"> @for (value of filteredValues | async; track value.value) { - + } @empty { - {{ 'common.not-found' | translate }} + @if (!allowUserValue) { + {{ 'common.not-found' | translate }} + } } diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.ts b/ui-ngx/src/app/shared/components/string-items-list.component.ts index 4f9f9cc178..fe9c2746c3 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.ts +++ b/ui-ngx/src/app/shared/components/string-items-list.component.ts @@ -29,7 +29,7 @@ import { FloatLabelType, MatFormFieldAppearance, SubscriptSizing } from '@angula import { coerceArray, coerceBoolean } from '@shared/decorators/coercion'; import { Observable, of } from 'rxjs'; import { filter, mergeMap, share, tap } from 'rxjs/operators'; -import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; +import { MatAutocompleteSelectedEvent, MatAutocompleteTrigger } from '@angular/material/autocomplete'; import { isDefined, isUndefined } from '@core/utils'; export interface StringItemsOption { @@ -37,18 +37,18 @@ export interface StringItemsOption { value: any; } @Component({ - selector: 'tb-string-items-list', - templateUrl: './string-items-list.component.html', - styleUrls: [], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => StringItemsListComponent), - multi: true - } - ], - encapsulation: ViewEncapsulation.None, - standalone: false + selector: 'tb-string-items-list', + templateUrl: './string-items-list.component.html', + styleUrls: [], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => StringItemsListComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None, + standalone: false }) export class StringItemsListComponent implements ControlValueAccessor, OnInit { @@ -149,9 +149,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { this.filteredValues = this.itemControl.valueChanges .pipe( tap((value) => { - if (value && typeof value !== 'string') { - this.add(value); - } else if (value === null) { + if (value === null) { this.clear(); } }), @@ -209,16 +207,17 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { this.dirty = true; } - addOnBlur(event: FocusEvent) { - const target: HTMLElement = event.relatedTarget as HTMLElement; - if (target && target.tagName !== 'MAT-OPTION') { - this.addItem(this.stringItemInput.nativeElement.value ?? '') + onOptionSelected(event: MatAutocompleteSelectedEvent): void { + if (event.option.value) { + this.add(event.option.value); + } else { + this.clear(); } - this.onTouched(); } addOnEnd(event: MatChipInputEvent): void { - this.addItem(event.value ?? '') + this.addItem(event.value ?? ''); + this.onTouched(); } removeItems(item: StringItemsOption) { @@ -295,6 +294,8 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { this.stringItemInput.nativeElement.value = value; this.itemControl.patchValue(value, {emitEvent: true}); setTimeout(() => { + this.stringItemInput.nativeElement.value = value; + this.itemControl.patchValue(null, {emitEvent: false}); this.stringItemInput.nativeElement.blur(); this.stringItemInput.nativeElement.focus(); }, 0); From 93bc81e5b8b886e577efe504dabe58c9e2dd4059 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 19 Mar 2026 18:16:46 +0200 Subject: [PATCH 2/4] UI: Use explicit null check in onOptionSelected to handle falsy values Truthiness check on event.option.value would incorrectly route to clear() for valid falsy values like 0 or "". Use != null instead. --- ui-ngx/src/app/shared/components/string-items-list.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/string-items-list.component.ts b/ui-ngx/src/app/shared/components/string-items-list.component.ts index fe9c2746c3..6d0c57ab0a 100644 --- a/ui-ngx/src/app/shared/components/string-items-list.component.ts +++ b/ui-ngx/src/app/shared/components/string-items-list.component.ts @@ -208,7 +208,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { } onOptionSelected(event: MatAutocompleteSelectedEvent): void { - if (event.option.value) { + if (event.option.value != null) { this.add(event.option.value); } else { this.clear(); From 46ef7c51a087cc230f6cab291a851bf769829825 Mon Sep 17 00:00:00 2001 From: Vladyslav Prykhodko Date: Tue, 24 Mar 2026 15:34:11 +0200 Subject: [PATCH 3/4] Fixed CVE-2026-33228 --- ui-ngx/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index 721b8b8006..8aafb1a243 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -6033,9 +6033,9 @@ flat@^5.0.2: integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== flatted@^3.2.9: - version "3.4.1" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.1.tgz#84ccd9579e76e9cc0d246c11d8be0beb019143e6" - integrity sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ== + version "3.4.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726" + integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== "flot.curvedlines@https://github.com/MichaelZinsmaier/CurvedLines.git#master": version "1.1.1" From 00e35f745e8b462084426cbf7c031bbffbca6e3f Mon Sep 17 00:00:00 2001 From: Vladyslav Prykhodko Date: Tue, 24 Mar 2026 15:16:06 +0200 Subject: [PATCH 4/4] Fix proxy error handling for 502/503/504 HTTP status codes Move 504 handling from interceptor to parseHttpErrorMessage, add user-friendly messages for 502/503/504 via httpStatusMessageMap, and scope isProxyError to only match proxy status codes to avoid breaking error handling for other HTTP errors. --- .../interceptors/global-http-interceptor.ts | 4 ---- ui-ngx/src/app/core/utils.ts | 18 ++++++++++++++---- ui-ngx/src/app/shared/models/constants.ts | 6 ++++++ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts index c210c54a0b..9d48e3b2ab 100644 --- a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts +++ b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts @@ -125,10 +125,6 @@ export class GlobalHttpInterceptor implements HttpInterceptor { this.showError(req.method + ': ' + req.url + '
' + errorResponse.status + ': ' + errorResponse.statusText); } - } else if (errorResponse.status === 504) { - if (!ignoreErrors) { - this.showError('Request timeout'); - } } else { unhandled = true; } diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 4c7a241931..348d4e7e9b 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -23,7 +23,7 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; import { baseDetailsPageByEntityType, EntityType } from '@shared/models/entity-type.models'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { TranslateService } from '@ngx-translate/core'; -import { serverErrorCodesTranslations } from '@shared/models/constants'; +import { httpStatusMessageMap, serverErrorCodesTranslations } from '@shared/models/constants'; import { SubscriptionEntityInfo } from '@core/api/widget-api.models'; import { CompiledTbFunction, @@ -825,11 +825,13 @@ export function parseHttpErrorMessage(errorResponse: HttpErrorResponse, } else { error = errorResponse.error; } - if (error && !error.message) { - errorMessage = prepareMessageFromData(error); - } else if (error && error.message) { + if (error && error.message) { errorMessage = error.message; timeout = error.timeout ? error.timeout : 0; + } else if (isProxyError(errorResponse)) { + errorMessage = httpStatusMessageMap.get(errorResponse.status); + } else if (error) { + errorMessage = prepareMessageFromData(error); } else { errorMessage = `Unhandled error code ${error ? error.status : '\'Unknown\''}`; } @@ -866,6 +868,14 @@ function prepareMessageFromData(data): string { } } +function isProxyError(errorResponse: HttpErrorResponse): boolean { + if (!httpStatusMessageMap.has(errorResponse.status)) { + return false; + } + const error = errorResponse.error; + return !error || typeof error === 'string' || (typeof error === 'object' && !error.message); +} + export const genNextLabel = (name: string, datasources: Datasource[]): string => { let label = name; let i = 1; diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 9f3889d277..429bfe217a 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -54,6 +54,12 @@ export const serverErrorCodesTranslations = new Map([ [Constants.serverErrorCode.tooManyUpdates, 'server-error.too-many-updates'], ]); +export const httpStatusMessageMap = new Map([ + [502, 'Server is temporarily unavailable (Bad Gateway)'], + [503, 'Server is temporarily unavailable'], + [504, 'Server did not respond in time (Gateway Timeout)'], +]); + export const MediaBreakpoints = { xs: 'screen and (max-width: 599px)', sm: 'screen and (min-width: 600px) and (max-width: 959px)',