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..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
@@ -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, isUndefined } from '@core/utils';
export interface StringItemsOption {
name: string;
@@ -116,6 +117,13 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit {
@coerceArray()
predefinedValues: StringItemsOption[];
+ @Input()
+ fetchOptionsFn: (searchText?: string) => Observable>;
+
+ @Input()
+ @coerceBoolean()
+ allowUserValue = false;
+
get itemsControl(): AbstractControl {
return this.stringItemsForm.get('items');
}
@@ -124,7 +132,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 +144,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 +155,8 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit {
}
}),
filter((value) => typeof value === 'string'),
- mergeMap(name => this.fetchValues(name)),
+ tap(name => this.searchText = name),
+ mergeMap(name => this.fetchOptionsFn ? this.fetchOptionsFn(name) : this.fetchValues(name)),
share()
);
}
@@ -180,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) {
@@ -199,19 +208,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 +245,27 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit {
return values ? values.name : undefined;
}
+ 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(searchText.toLowerCase()));
+ if (findItems.length === 1) {
+ this.add(findItems[0]);
+ }
+ } else if (isDefined(this.fetchOptionsFn)) {
+ this.fetchOptionsFn(searchText).subscribe((findItems) => {
+ if (findItems.length === 1) {
+ this.add(findItems[0]);
+ }
+ })
+ }
+ }
+ }
+
private add(item: StringItemsOption) {
if (!this.modelValue || this.modelValue.indexOf(item.value) === -1) {
if (!this.modelValue) {
@@ -256,7 +283,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()));