Add back autocomplete to filter editor, where it belongs
This commit is contained in:
parent
478b6e6f94
commit
e9b440428b
@ -45,8 +45,8 @@ test('basic filtering', async ({ page }) => {
|
|||||||
test('text filtering', async ({ page }) => {
|
test('text filtering', async ({ page }) => {
|
||||||
await page.routeFromHAR(REQUESTS_HAR2, { notFound: 'fallback' })
|
await page.routeFromHAR(REQUESTS_HAR2, { notFound: 'fallback' })
|
||||||
await page.goto('/documents')
|
await page.goto('/documents')
|
||||||
await page.getByRole('main').getByRole('textbox').click()
|
await page.getByRole('main').getByRole('combobox').click()
|
||||||
await page.getByRole('main').getByRole('textbox').fill('test')
|
await page.getByRole('main').getByRole('combobox').fill('test')
|
||||||
await expect(page.locator('pngx-document-list')).toHaveText(/32 documents/)
|
await expect(page.locator('pngx-document-list')).toHaveText(/32 documents/)
|
||||||
await expect(page).toHaveURL(/title_content=test/)
|
await expect(page).toHaveURL(/title_content=test/)
|
||||||
await page.getByRole('button', { name: 'Title & content' }).click()
|
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 expect(page.locator('pngx-document-list')).toHaveText(/26 documents/)
|
||||||
await page.getByRole('button', { name: 'Advanced search' }).click()
|
await page.getByRole('button', { name: 'Advanced search' }).click()
|
||||||
await page.getByRole('button', { name: 'ASN' }).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).toHaveURL(/archive_serial_number=1123/)
|
||||||
await expect(page.locator('pngx-document-list')).toHaveText(/one document/i)
|
await expect(page.locator('pngx-document-list')).toHaveText(/one document/i)
|
||||||
await page.locator('select').selectOption('greater')
|
await page.locator('select').selectOption('greater')
|
||||||
await page.getByRole('main').getByRole('textbox').click()
|
await page.getByRole('main').getByRole('combobox').nth(1).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__gt=1123/)
|
await expect(page).toHaveURL(/archive_serial_number__gt=1123/)
|
||||||
await expect(page.locator('pngx-document-list')).toHaveText(/5 documents/)
|
await expect(page.locator('pngx-document-list')).toHaveText(/5 documents/)
|
||||||
await page.locator('select').selectOption('less')
|
await page.locator('select').selectOption('less')
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
NgbModalRef,
|
NgbModalRef,
|
||||||
NgbPopoverModule,
|
NgbPopoverModule,
|
||||||
NgbTooltipModule,
|
NgbTooltipModule,
|
||||||
|
NgbTypeaheadModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { ClearableBadgeComponent } from '../common/clearable-badge/clearable-badge.component'
|
import { ClearableBadgeComponent } from '../common/clearable-badge/clearable-badge.component'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
@ -152,6 +153,7 @@ describe('DocumentListComponent', () => {
|
|||||||
NgbTooltipModule,
|
NgbTooltipModule,
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
NgSelectModule,
|
NgSelectModule,
|
||||||
|
NgbTypeaheadModule,
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
|
@ -22,7 +22,13 @@
|
|||||||
<i-bs width="1em" height="1em" name="x"></i-bs>
|
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
<input #textFilterInput class="form-control form-control-sm" type="text" [disabled]="textFilterModifierIsNull" [(ngModel)]="textFilter" (keyup)="textFilterKeyup($event)" [readonly]="textFilterTarget === 'fulltext-morelike'">
|
<input #textFilterInput class="form-control form-control-sm" type="text"
|
||||||
|
[disabled]="textFilterModifierIsNull"
|
||||||
|
[(ngModel)]="textFilter"
|
||||||
|
(keyup)="textFilterKeyup($event)"
|
||||||
|
[ngbTypeahead]="searchAutoComplete"
|
||||||
|
(selectItem)="itemSelected($event)"
|
||||||
|
[readonly]="textFilterTarget === 'fulltext-morelike'">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,14 +11,14 @@ import {
|
|||||||
} from '@angular/core/testing'
|
} from '@angular/core/testing'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { By } from '@angular/platform-browser'
|
import { By } from '@angular/platform-browser'
|
||||||
import { RouterTestingModule } from '@angular/router/testing'
|
|
||||||
import {
|
import {
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
NgbDatepickerModule,
|
NgbDatepickerModule,
|
||||||
NgbDropdownItem,
|
NgbDropdownItem,
|
||||||
|
NgbTypeaheadModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgSelectComponent } from '@ng-select/ng-select'
|
import { NgSelectComponent } from '@ng-select/ng-select'
|
||||||
import { of } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import {
|
import {
|
||||||
FILTER_TITLE,
|
FILTER_TITLE,
|
||||||
FILTER_TITLE_CONTENT,
|
FILTER_TITLE_CONTENT,
|
||||||
@ -86,6 +86,8 @@ import {
|
|||||||
PermissionsService,
|
PermissionsService,
|
||||||
} from 'src/app/services/permissions.service'
|
} from 'src/app/services/permissions.service'
|
||||||
import { environment } from 'src/environments/environment'
|
import { environment } from 'src/environments/environment'
|
||||||
|
import { RouterModule } from '@angular/router'
|
||||||
|
import { SearchService } from 'src/app/services/rest/search.service'
|
||||||
|
|
||||||
const tags: Tag[] = [
|
const tags: Tag[] = [
|
||||||
{
|
{
|
||||||
@ -145,6 +147,7 @@ describe('FilterEditorComponent', () => {
|
|||||||
let settingsService: SettingsService
|
let settingsService: SettingsService
|
||||||
let permissionsService: PermissionsService
|
let permissionsService: PermissionsService
|
||||||
let httpTestingController: HttpTestingController
|
let httpTestingController: HttpTestingController
|
||||||
|
let searchService: SearchService
|
||||||
|
|
||||||
beforeEach(fakeAsync(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@ -197,12 +200,13 @@ describe('FilterEditorComponent', () => {
|
|||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
HttpClientTestingModule,
|
HttpClientTestingModule,
|
||||||
RouterTestingModule,
|
RouterModule,
|
||||||
NgbDropdownModule,
|
NgbDropdownModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgbDatepickerModule,
|
NgbDatepickerModule,
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
NgbTypeaheadModule,
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
@ -210,6 +214,7 @@ describe('FilterEditorComponent', () => {
|
|||||||
settingsService = TestBed.inject(SettingsService)
|
settingsService = TestBed.inject(SettingsService)
|
||||||
settingsService.currentUser = users[0]
|
settingsService.currentUser = users[0]
|
||||||
permissionsService = TestBed.inject(PermissionsService)
|
permissionsService = TestBed.inject(PermissionsService)
|
||||||
|
searchService = TestBed.inject(SearchService)
|
||||||
jest
|
jest
|
||||||
.spyOn(permissionsService, 'currentUserCan')
|
.spyOn(permissionsService, 'currentUserCan')
|
||||||
.mockImplementation((action, type) => {
|
.mockImplementation((action, type) => {
|
||||||
@ -1829,4 +1834,40 @@ describe('FilterEditorComponent', () => {
|
|||||||
name: $localize`More like`,
|
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 ')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -12,11 +12,14 @@ import {
|
|||||||
import { Tag } from 'src/app/data/tag'
|
import { Tag } from 'src/app/data/tag'
|
||||||
import { Correspondent } from 'src/app/data/correspondent'
|
import { Correspondent } from 'src/app/data/correspondent'
|
||||||
import { DocumentType } from 'src/app/data/document-type'
|
import { DocumentType } from 'src/app/data/document-type'
|
||||||
import { Observable, Subject, Subscription } from 'rxjs'
|
import { Observable, Subject, Subscription, from } from 'rxjs'
|
||||||
import {
|
import {
|
||||||
|
catchError,
|
||||||
debounceTime,
|
debounceTime,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
filter,
|
filter,
|
||||||
|
map,
|
||||||
|
switchMap,
|
||||||
takeUntil,
|
takeUntil,
|
||||||
} from 'rxjs/operators'
|
} from 'rxjs/operators'
|
||||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||||
@ -82,6 +85,7 @@ import {
|
|||||||
PermissionsService,
|
PermissionsService,
|
||||||
} from 'src/app/services/permissions.service'
|
} from 'src/app/services/permissions.service'
|
||||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
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 = 'title'
|
||||||
const TEXT_FILTER_TARGET_TITLE_CONTENT = 'title-content'
|
const TEXT_FILTER_TARGET_TITLE_CONTENT = 'title-content'
|
||||||
@ -240,7 +244,8 @@ export class FilterEditorComponent
|
|||||||
private correspondentService: CorrespondentService,
|
private correspondentService: CorrespondentService,
|
||||||
private documentService: DocumentService,
|
private documentService: DocumentService,
|
||||||
private storagePathService: StoragePathService,
|
private storagePathService: StoragePathService,
|
||||||
public permissionsService: PermissionsService
|
public permissionsService: PermissionsService,
|
||||||
|
private searchService: SearchService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
@ -922,7 +927,12 @@ export class FilterEditorComponent
|
|||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
filter((query) => !query.length || query.length > 2)
|
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
|
if (this._textFilter) this.documentService.searchQuery = this._textFilter
|
||||||
}
|
}
|
||||||
@ -973,10 +983,12 @@ export class FilterEditorComponent
|
|||||||
this.storagePathSelectionModel.apply()
|
this.storagePathSelectionModel.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTextFilter(text) {
|
updateTextFilter(text, updateRules = true) {
|
||||||
this._textFilter = text
|
this._textFilter = text
|
||||||
this.documentService.searchQuery = text
|
if (updateRules) {
|
||||||
this.updateRules()
|
this.documentService.searchQuery = text
|
||||||
|
this.updateRules()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
textFilterKeyup(event: KeyboardEvent) {
|
textFilterKeyup(event: KeyboardEvent) {
|
||||||
@ -1025,4 +1037,40 @@ export class FilterEditorComponent
|
|||||||
this.updateRules()
|
this.updateRules()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
searchAutoComplete = (text$: Observable<string>) =>
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user