diff --git a/frontend/src/app/features/work-packages/components/filters/filter-searchable-multiselect-value/filter-searchable-multiselect-value.component.ts b/frontend/src/app/features/work-packages/components/filters/filter-searchable-multiselect-value/filter-searchable-multiselect-value.component.ts index a690482dae..700859349d 100644 --- a/frontend/src/app/features/work-packages/components/filters/filter-searchable-multiselect-value/filter-searchable-multiselect-value.component.ts +++ b/frontend/src/app/features/work-packages/components/filters/filter-searchable-multiselect-value/filter-searchable-multiselect-value.component.ts @@ -8,6 +8,7 @@ import { CurrentProjectService } from 'core-app/core/current-project/current-pro import { ApiV3FilterBuilder } from 'core-app/shared/helpers/api-v3/api-v3-filter-builder'; import { map, + shareReplay, switchMap, withLatestFrom, } from 'rxjs/operators'; @@ -24,6 +25,7 @@ import { EventEmitter, Input, NgZone, + OnInit, Output, ViewChild, } from '@angular/core'; @@ -31,13 +33,14 @@ import { HalResource } from 'core-app/features/hal/resources/hal-resource'; import { I18nService } from 'core-app/core/i18n/i18n.service'; import { CurrentUserService } from 'core-app/core/current-user/current-user.service'; import { take } from 'rxjs/internal/operators/take'; +import { CollectionResource } from 'core-app/features/hal/resources/collection-resource'; @Component({ selector: 'op-filter-searchable-multiselect-value', changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './filter-searchable-multiselect-value.component.html', }) -export class FilterSearchableMultiselectValueComponent extends UntilDestroyedMixin { +export class FilterSearchableMultiselectValueComponent extends UntilDestroyedMixin implements OnInit { @Input() public filter:QueryFilterInstanceResource; @Input() public shouldFocus = false; @@ -55,7 +58,9 @@ export class FilterSearchableMultiselectValueComponent extends UntilDestroyedMix }, true, ); - autocompleterFn = (searchTerm:string):Observable => this.loadAvailable(searchTerm); + autocompleterFn = (searchTerm:string):Observable => this.autocomplete(searchTerm); + + initialRequest$:Observable; readonly text = { placeholder: this.I18n.t('js.placeholders.selection'), @@ -78,23 +83,57 @@ export class FilterSearchableMultiselectValueComponent extends UntilDestroyedMix super(); } - public loadAvailable(matching:string):Observable { - const filters:ApiV3FilterBuilder = this.createFilters(matching); - /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ - const { href } = this.filter.currentSchema!.values!.allowedValues as { href:string }; + ngOnInit():void { + this.initialRequest$ = this + .loadCollection('') + .pipe( + shareReplay(1), + ); + } - const filteredData = (this.apiV3Service.collectionFromString(href) as - ApiV3ResourceCollection) - .filtered(filters, { pageSize: '-1' }) - .get() + private autocomplete(matching:string):Observable { + return this + .initialRequest$ .pipe( - switchMap((collection) => this.withMeValue(matching, collection.elements)), + switchMap((initialLoad) => { + // If we already loaded all values, just compare in the frontend + if (initialLoad.count === initialLoad.total) { + return this.matchingItems(initialLoad.elements, matching); + } + + // Otherwise, request the matching API call + return this + .loadCollection(matching) + .pipe( + switchMap((collection) => this.withMeValue(matching, collection.elements)), + ); + }), ); + } - return filteredData; + matchingItems(elements:HalResource[], matching:string):Observable { + let filtered:HalResource[]; + + if (matching === '') { + filtered = elements; + } else { + const lowered = matching.toLowerCase(); + filtered = elements.filter((el) => el.name.toLowerCase().includes(lowered)); + } + + return this.withMeValue(matching, filtered); + } + + private loadCollection(matching:string):Observable { + const filters:ApiV3FilterBuilder = this.createFilters(matching); + + return (this.apiV3Service.collectionFromString(this.allowedValuesLink) as + ApiV3ResourceCollection) + .filtered(filters, { pageSize: '-1' }) + .get(); } - protected createFilters(matching:string):ApiV3FilterBuilder { + private createFilters(matching:string):ApiV3FilterBuilder { const filters = new ApiV3FilterBuilder(); if (matching) { @@ -110,6 +149,13 @@ export class FilterSearchableMultiselectValueComponent extends UntilDestroyedMix this.cdRef.detectChanges(); } + private get allowedValuesLink():string { + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + const { href } = this.filter.currentSchema!.values!.allowedValues as { href:string }; + + return href; + } + private withMeValue(matching:string, elements:HalResource[]):Observable { if (!this.isUserResource || (!!matching && matching !== 'me')) { return of(elements); diff --git a/frontend/src/app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component.ts b/frontend/src/app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component.ts index d4af895a86..3a87a0ab07 100644 --- a/frontend/src/app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component.ts +++ b/frontend/src/app/shared/components/autocompleter/op-autocompleter/op-autocompleter.component.ts @@ -301,10 +301,8 @@ export class OpAutocompleterComponent extends UntilDestroyedMixin implements Aft } public opened(_:unknown) { // eslint-disable-line no-unused-vars - if (this.openDirectly) { - this.typeahead.next(''); - } - + // Re-search for empty value as search value gets removed + this.typeahead.next(''); this.repositionDropdown(); this.open.emit(); }