From e9b440428b5685eafdd7e0fe3d432cda5556ee59 Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Fri, 5 Apr 2024 23:42:20 -0700
Subject: [PATCH] Add back autocomplete to filter editor, where it belongs
---
.../e2e/document-list/document-list.spec.ts | 10 ++--
.../document-list.component.spec.ts | 2 +
.../filter-editor.component.html | 8 ++-
.../filter-editor.component.spec.ts | 47 ++++++++++++++-
.../filter-editor/filter-editor.component.ts | 60 +++++++++++++++++--
5 files changed, 112 insertions(+), 15 deletions(-)
diff --git a/src-ui/e2e/document-list/document-list.spec.ts b/src-ui/e2e/document-list/document-list.spec.ts
index 6d21d2835..449683110 100644
--- a/src-ui/e2e/document-list/document-list.spec.ts
+++ b/src-ui/e2e/document-list/document-list.spec.ts
@@ -45,8 +45,8 @@ test('basic filtering', async ({ page }) => {
test('text filtering', async ({ page }) => {
await page.routeFromHAR(REQUESTS_HAR2, { notFound: 'fallback' })
await page.goto('/documents')
- await page.getByRole('main').getByRole('textbox').click()
- await page.getByRole('main').getByRole('textbox').fill('test')
+ await page.getByRole('main').getByRole('combobox').click()
+ await page.getByRole('main').getByRole('combobox').fill('test')
await expect(page.locator('pngx-document-list')).toHaveText(/32 documents/)
await expect(page).toHaveURL(/title_content=test/)
await page.getByRole('button', { name: 'Title & content' }).click()
@@ -59,12 +59,12 @@ test('text filtering', async ({ page }) => {
await expect(page.locator('pngx-document-list')).toHaveText(/26 documents/)
await page.getByRole('button', { name: 'Advanced search' }).click()
await page.getByRole('button', { name: 'ASN' }).click()
- await page.getByRole('main').getByRole('textbox').fill('1123')
+ await page.getByRole('main').getByRole('combobox').nth(1).fill('1123')
await expect(page).toHaveURL(/archive_serial_number=1123/)
await expect(page.locator('pngx-document-list')).toHaveText(/one document/i)
await page.locator('select').selectOption('greater')
- await page.getByRole('main').getByRole('textbox').click()
- await page.getByRole('main').getByRole('textbox').fill('1123')
+ await page.getByRole('main').getByRole('combobox').nth(1).click()
+ await page.getByRole('main').getByRole('combobox').nth(1).fill('1123')
await expect(page).toHaveURL(/archive_serial_number__gt=1123/)
await expect(page.locator('pngx-document-list')).toHaveText(/5 documents/)
await page.locator('select').selectOption('less')
diff --git a/src-ui/src/app/components/document-list/document-list.component.spec.ts b/src-ui/src/app/components/document-list/document-list.component.spec.ts
index 2815b6ae9..34c4f2a40 100644
--- a/src-ui/src/app/components/document-list/document-list.component.spec.ts
+++ b/src-ui/src/app/components/document-list/document-list.component.spec.ts
@@ -19,6 +19,7 @@ import {
NgbModalRef,
NgbPopoverModule,
NgbTooltipModule,
+ NgbTypeaheadModule,
} from '@ng-bootstrap/ng-bootstrap'
import { ClearableBadgeComponent } from '../common/clearable-badge/clearable-badge.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
@@ -152,6 +153,7 @@ describe('DocumentListComponent', () => {
NgbTooltipModule,
NgxBootstrapIconsModule.pick(allIcons),
NgSelectModule,
+ NgbTypeaheadModule,
],
}).compileComponents()
diff --git a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html
index 89900e087..c6c287ef5 100644
--- a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html
+++ b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.html
@@ -22,7 +22,13 @@
}
-
+
diff --git a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.spec.ts b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.spec.ts
index d9e92092e..5c393168f 100644
--- a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.spec.ts
+++ b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.spec.ts
@@ -11,14 +11,14 @@ import {
} from '@angular/core/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { By } from '@angular/platform-browser'
-import { RouterTestingModule } from '@angular/router/testing'
import {
NgbDropdownModule,
NgbDatepickerModule,
NgbDropdownItem,
+ NgbTypeaheadModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgSelectComponent } from '@ng-select/ng-select'
-import { of } from 'rxjs'
+import { of, throwError } from 'rxjs'
import {
FILTER_TITLE,
FILTER_TITLE_CONTENT,
@@ -86,6 +86,8 @@ import {
PermissionsService,
} from 'src/app/services/permissions.service'
import { environment } from 'src/environments/environment'
+import { RouterModule } from '@angular/router'
+import { SearchService } from 'src/app/services/rest/search.service'
const tags: Tag[] = [
{
@@ -145,6 +147,7 @@ describe('FilterEditorComponent', () => {
let settingsService: SettingsService
let permissionsService: PermissionsService
let httpTestingController: HttpTestingController
+ let searchService: SearchService
beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
@@ -197,12 +200,13 @@ describe('FilterEditorComponent', () => {
],
imports: [
HttpClientTestingModule,
- RouterTestingModule,
+ RouterModule,
NgbDropdownModule,
FormsModule,
ReactiveFormsModule,
NgbDatepickerModule,
NgxBootstrapIconsModule.pick(allIcons),
+ NgbTypeaheadModule,
],
}).compileComponents()
@@ -210,6 +214,7 @@ describe('FilterEditorComponent', () => {
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = users[0]
permissionsService = TestBed.inject(PermissionsService)
+ searchService = TestBed.inject(SearchService)
jest
.spyOn(permissionsService, 'currentUserCan')
.mockImplementation((action, type) => {
@@ -1829,4 +1834,40 @@ describe('FilterEditorComponent', () => {
name: $localize`More like`,
})
})
+
+ it('should call autocomplete endpoint on input', fakeAsync(() => {
+ component.textFilterTarget = 'fulltext-query' // TEXT_FILTER_TARGET_FULLTEXT_QUERY
+ const autocompleteSpy = jest.spyOn(searchService, 'autocomplete')
+ component.searchAutoComplete(of('hello')).subscribe()
+ tick(250)
+ expect(autocompleteSpy).toHaveBeenCalled()
+
+ component.searchAutoComplete(of('hello world 1')).subscribe()
+ tick(250)
+ expect(autocompleteSpy).toHaveBeenCalled()
+ }))
+
+ it('should handle autocomplete backend failure gracefully', fakeAsync(() => {
+ component.textFilterTarget = 'fulltext-query' // TEXT_FILTER_TARGET_FULLTEXT_QUERY
+ const serviceAutocompleteSpy = jest.spyOn(searchService, 'autocomplete')
+ serviceAutocompleteSpy.mockReturnValue(
+ throwError(() => new Error('autcomplete failed'))
+ )
+ // serviceAutocompleteSpy.mockReturnValue(of([' world']))
+ let result
+ component.searchAutoComplete(of('hello')).subscribe((res) => {
+ result = res
+ })
+ tick(250)
+ expect(serviceAutocompleteSpy).toHaveBeenCalled()
+ expect(result).toEqual([])
+ }))
+
+ it('should support choosing a autocomplete item', () => {
+ expect(component.textFilter).toBeNull()
+ component.itemSelected({ item: 'hello', preventDefault: () => true })
+ expect(component.textFilter).toEqual('hello ')
+ component.itemSelected({ item: 'world', preventDefault: () => true })
+ expect(component.textFilter).toEqual('hello world ')
+ })
})
diff --git a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts
index 5a84b16e2..b7be4b57c 100644
--- a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts
+++ b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts
@@ -12,11 +12,14 @@ import {
import { Tag } from 'src/app/data/tag'
import { Correspondent } from 'src/app/data/correspondent'
import { DocumentType } from 'src/app/data/document-type'
-import { Observable, Subject, Subscription } from 'rxjs'
+import { Observable, Subject, Subscription, from } from 'rxjs'
import {
+ catchError,
debounceTime,
distinctUntilChanged,
filter,
+ map,
+ switchMap,
takeUntil,
} from 'rxjs/operators'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
@@ -82,6 +85,7 @@ import {
PermissionsService,
} from 'src/app/services/permissions.service'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
+import { SearchService } from 'src/app/services/rest/search.service'
const TEXT_FILTER_TARGET_TITLE = 'title'
const TEXT_FILTER_TARGET_TITLE_CONTENT = 'title-content'
@@ -240,7 +244,8 @@ export class FilterEditorComponent
private correspondentService: CorrespondentService,
private documentService: DocumentService,
private storagePathService: StoragePathService,
- public permissionsService: PermissionsService
+ public permissionsService: PermissionsService,
+ private searchService: SearchService
) {
super()
}
@@ -922,7 +927,12 @@ export class FilterEditorComponent
distinctUntilChanged(),
filter((query) => !query.length || query.length > 2)
)
- .subscribe((text) => this.updateTextFilter(text))
+ .subscribe((text) =>
+ this.updateTextFilter(
+ text,
+ this.textFilterTarget !== TEXT_FILTER_TARGET_FULLTEXT_QUERY
+ )
+ )
if (this._textFilter) this.documentService.searchQuery = this._textFilter
}
@@ -973,10 +983,12 @@ export class FilterEditorComponent
this.storagePathSelectionModel.apply()
}
- updateTextFilter(text) {
+ updateTextFilter(text, updateRules = true) {
this._textFilter = text
- this.documentService.searchQuery = text
- this.updateRules()
+ if (updateRules) {
+ this.documentService.searchQuery = text
+ this.updateRules()
+ }
}
textFilterKeyup(event: KeyboardEvent) {
@@ -1025,4 +1037,40 @@ export class FilterEditorComponent
this.updateRules()
}
}
+
+ searchAutoComplete = (text$: Observable) =>
+ text$.pipe(
+ debounceTime(200),
+ distinctUntilChanged(),
+ filter(() => this.textFilterTarget === TEXT_FILTER_TARGET_FULLTEXT_QUERY),
+ map((term) => {
+ if (term.lastIndexOf(' ') != -1) {
+ return term.substring(term.lastIndexOf(' ') + 1)
+ } else {
+ return term
+ }
+ }),
+ switchMap((term) =>
+ term.length < 2
+ ? from([[]])
+ : this.searchService.autocomplete(term).pipe(
+ catchError(() => {
+ return from([[]])
+ })
+ )
+ )
+ )
+
+ itemSelected(event) {
+ event.preventDefault()
+ let currentSearch: string = this._textFilter ?? ''
+ let lastSpaceIndex = currentSearch.lastIndexOf(' ')
+ if (lastSpaceIndex != -1) {
+ currentSearch = currentSearch.substring(0, lastSpaceIndex + 1)
+ currentSearch += event.item + ' '
+ } else {
+ currentSearch = event.item + ' '
+ }
+ this.updateTextFilter(currentSearch)
+ }
}