From bffa565842b38423dd65350bf97d1665a309ba4f Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 2 Apr 2025 13:04:23 +0300 Subject: [PATCH 1/3] UI: Add fetch function for string items list; fix double item addition on blur --- .../string-items-list.component.html | 16 +++---- .../components/string-items-list.component.ts | 46 +++++++++++++------ 2 files changed, 38 insertions(+), 24 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 b50e0ecce8..d467fbe5f0 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 @@ -30,7 +30,7 @@ close @@ -49,12 +48,11 @@ - - - - - {{ 'common.not-found' | translate }} - + @for (value of filteredValues | async; track value.value) { + + } @empty { + {{ 'common.not-found' | translate }} + } {{ hint }} 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 a2fa28d0ff..a4a8ebd49b 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 @@ -30,6 +30,7 @@ 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 { isDefined } from '@core/utils'; export interface StringItemsOption { name: string; @@ -116,6 +117,9 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { @coerceArray() predefinedValues: StringItemsOption[]; + @Input() + fetchOptionsFn: (searchText?: string) => Observable>; + get itemsControl(): AbstractControl { return this.stringItemsForm.get('items'); } @@ -124,7 +128,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { return this.stringItemsForm.get('item'); } - onTouched = () => {}; + private onTouched = () => {}; private propagateChange: (value: any) => void = () => {}; private dirty = false; @@ -136,7 +140,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { } ngOnInit() { - if (this.predefinedValues) { + if (this.predefinedValues || isDefined(this.fetchOptionsFn)) { this.filteredValues = this.itemControl.valueChanges .pipe( tap((value) => { @@ -147,7 +151,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { } }), filter((value) => typeof value === 'string'), - mergeMap(name => this.fetchValues(name)), + mergeMap(name => this.fetchOptionsFn ? this.fetchOptionsFn(name) : this.fetchValues(name)), share() ); } @@ -199,19 +203,16 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { this.dirty = true; } - addItem(event: MatChipInputEvent): void { - const item = event.value?.trim() ?? ''; - if (item) { - if (this.predefinedValues) { - const findItems = this.predefinedValues - .filter(value => value.name.toLowerCase().includes(item.toLowerCase())); - if (findItems.length === 1) { - this.add(findItems[0]); - } - } else { - this.add({value: item, name: item}); - } + addOnBlur(event: FocusEvent) { + const target: HTMLElement = event.relatedTarget as HTMLElement; + if (target && target.tagName !== 'MAT-OPTION') { + this.addItem(this.stringItemInput.nativeElement.value ?? '') } + this.onTouched(); + } + + addOnEnd(event: MatChipInputEvent): void { + this.addItem(event.value ?? '') } removeItems(item: StringItemsOption) { @@ -239,6 +240,21 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { return values ? values.name : undefined; } + private addItem(value: string) { + const item = value.trim(); + if (item) { + if (this.predefinedValues) { + const findItems = this.predefinedValues + .filter(value => value.name.toLowerCase().includes(item.toLowerCase())); + if (findItems.length === 1) { + this.add(findItems[0]); + } + } else { + this.add({value: item, name: item}); + } + } + } + private add(item: StringItemsOption) { if (!this.modelValue || this.modelValue.indexOf(item.value) === -1) { if (!this.modelValue) { From 44f1e139bde66aec55ba0fbace0e078fd84f3ea1 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 2 Apr 2025 13:41:59 +0300 Subject: [PATCH 2/3] UI: Add allowUserValue for string items list --- .../components/string-items-list.component.ts | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) 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 a4a8ebd49b..5929bbe924 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 @@ -14,7 +14,16 @@ /// limitations under the License. /// -import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { + Component, + DestroyRef, + ElementRef, + forwardRef, + Input, + OnInit, + ViewChild, + ViewEncapsulation +} from '@angular/core'; import { AbstractControl, ControlValueAccessor, @@ -31,6 +40,7 @@ import { Observable, of } from 'rxjs'; import { filter, mergeMap, share, tap } from 'rxjs/operators'; import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; import { isDefined } from '@core/utils'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; export interface StringItemsOption { name: string; @@ -120,6 +130,10 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { @Input() fetchOptionsFn: (searchText?: string) => Observable>; + @Input() + @coerceBoolean() + allowUserValue = false; + get itemsControl(): AbstractControl { return this.stringItemsForm.get('items'); } @@ -132,7 +146,8 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { private propagateChange: (value: any) => void = () => {}; private dirty = false; - constructor(private fb: FormBuilder) { + constructor(private fb: FormBuilder, + private destroyRef: DestroyRef) { this.stringItemsForm = this.fb.group({ item: [null], items: [null] @@ -151,6 +166,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { } }), filter((value) => typeof value === 'string'), + tap(name => this.searchText = name), mergeMap(name => this.fetchOptionsFn ? this.fetchOptionsFn(name) : this.fetchValues(name)), share() ); @@ -243,12 +259,20 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { private addItem(value: string) { const item = value.trim(); if (item) { - if (this.predefinedValues) { + if (this.predefinedValues && !this.allowUserValue) { const findItems = this.predefinedValues .filter(value => value.name.toLowerCase().includes(item.toLowerCase())); if (findItems.length === 1) { this.add(findItems[0]); } + } else if (isDefined(this.fetchOptionsFn) && !this.allowUserValue) { + this.fetchOptionsFn(item).pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe((findItems) => { + if (findItems.length === 1) { + this.add(findItems[0]); + } + }) } else { this.add({value: item, name: item}); } @@ -272,7 +296,6 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { if (!this.predefinedValues?.length) { return of([]); } - this.searchText = searchText; let result = this.predefinedValues; if (searchText && searchText.length) { result = this.predefinedValues.filter(option => option.name.toLowerCase().includes(searchText.toLowerCase())); From 555de9bb6f2c0d7bfbb76467d8c8b9beff545aaf Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 2 Apr 2025 13:58:49 +0300 Subject: [PATCH 3/3] UI: Refactoring string items list --- .../components/string-items-list.component.ts | 39 +++++++------------ 1 file changed, 13 insertions(+), 26 deletions(-) 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 5929bbe924..5fe6f44609 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 @@ -14,16 +14,7 @@ /// limitations under the License. /// -import { - Component, - DestroyRef, - ElementRef, - forwardRef, - Input, - OnInit, - ViewChild, - ViewEncapsulation -} from '@angular/core'; +import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; import { AbstractControl, ControlValueAccessor, @@ -39,8 +30,7 @@ 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 { isDefined } from '@core/utils'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { isDefined, isUndefined } from '@core/utils'; export interface StringItemsOption { name: string; @@ -146,8 +136,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { private propagateChange: (value: any) => void = () => {}; private dirty = false; - constructor(private fb: FormBuilder, - private destroyRef: DestroyRef) { + constructor(private fb: FormBuilder) { this.stringItemsForm = this.fb.group({ item: [null], items: [null] @@ -200,7 +189,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { if (value != null && value.length > 0) { this.modelValue = [...value]; this.itemList = []; - if (this.predefinedValues) { + if (this.predefinedValues && !this.allowUserValue) { value.forEach(item => { const findItem = this.predefinedValues.find(option => option.value === item); if (findItem) { @@ -256,25 +245,23 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit { return values ? values.name : undefined; } - private addItem(value: string) { - const item = value.trim(); - if (item) { - if (this.predefinedValues && !this.allowUserValue) { + private addItem(searchText: string) { + searchText = searchText.trim(); + if (searchText) { + if (this.allowUserValue || !this.predefinedValues && isUndefined(this.fetchOptionsFn)) { + this.add({value: searchText, name: searchText}); + } else if (this.predefinedValues) { const findItems = this.predefinedValues - .filter(value => value.name.toLowerCase().includes(item.toLowerCase())); + .filter(value => value.name.toLowerCase().includes(searchText.toLowerCase())); if (findItems.length === 1) { this.add(findItems[0]); } - } else if (isDefined(this.fetchOptionsFn) && !this.allowUserValue) { - this.fetchOptionsFn(item).pipe( - takeUntilDestroyed(this.destroyRef) - ).subscribe((findItems) => { + } else if (isDefined(this.fetchOptionsFn)) { + this.fetchOptionsFn(searchText).subscribe((findItems) => { if (findItems.length === 1) { this.add(findItems[0]); } }) - } else { - this.add({value: item, name: item}); } } }