mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-14 00:26:21 +00:00
Feature: global search, keyboard shortcuts / hotkey support (#6449)
This commit is contained in:
@@ -21,7 +21,7 @@
|
||||
<pngx-filterable-dropdown title="Tags" icon="tag-fill" i18n-title
|
||||
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||
[items]="tags"
|
||||
[disabled]="!userCanEditAll"
|
||||
[disabled]="!userCanEditAll || disabled"
|
||||
[editing]="true"
|
||||
[manyToOne]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
@@ -29,49 +29,53 @@
|
||||
(opened)="openTagsDropdown()"
|
||||
[(selectionModel)]="tagSelectionModel"
|
||||
[documentCounts]="tagDocumentCounts"
|
||||
(apply)="setTags($event)">
|
||||
(apply)="setTags($event)"
|
||||
shortcutKey="t">
|
||||
</pngx-filterable-dropdown>
|
||||
}
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
|
||||
<pngx-filterable-dropdown title="Correspondent" icon="person-fill" i18n-title
|
||||
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
||||
[items]="correspondents"
|
||||
[disabled]="!userCanEditAll"
|
||||
[disabled]="!userCanEditAll || disabled"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
[createRef]="createCorrespondent.bind(this)"
|
||||
(opened)="openCorrespondentDropdown()"
|
||||
[(selectionModel)]="correspondentSelectionModel"
|
||||
[documentCounts]="correspondentDocumentCounts"
|
||||
(apply)="setCorrespondents($event)">
|
||||
(apply)="setCorrespondents($event)"
|
||||
shortcutKey="y">
|
||||
</pngx-filterable-dropdown>
|
||||
}
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
|
||||
<pngx-filterable-dropdown title="Document type" icon="file-earmark-fill" i18n-title
|
||||
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
||||
[items]="documentTypes"
|
||||
[disabled]="!userCanEditAll"
|
||||
[disabled]="!userCanEditAll || disabled"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
[createRef]="createDocumentType.bind(this)"
|
||||
(opened)="openDocumentTypeDropdown()"
|
||||
[(selectionModel)]="documentTypeSelectionModel"
|
||||
[documentCounts]="documentTypeDocumentCounts"
|
||||
(apply)="setDocumentTypes($event)">
|
||||
(apply)="setDocumentTypes($event)"
|
||||
shortcutKey="u">
|
||||
</pngx-filterable-dropdown>
|
||||
}
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
|
||||
<pngx-filterable-dropdown title="Storage path" icon="folder-fill" i18n-title
|
||||
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
|
||||
[items]="storagePaths"
|
||||
[disabled]="!userCanEditAll"
|
||||
[disabled]="!userCanEditAll || disabled"
|
||||
[editing]="true"
|
||||
[applyOnClose]="applyOnClose"
|
||||
[createRef]="createStoragePath.bind(this)"
|
||||
(opened)="openStoragePathDropdown()"
|
||||
[(selectionModel)]="storagePathsSelectionModel"
|
||||
[documentCounts]="storagePathDocumentCounts"
|
||||
(apply)="setStoragePaths($event)">
|
||||
(apply)="setStoragePaths($event)"
|
||||
shortcutKey="i">
|
||||
</pngx-filterable-dropdown>
|
||||
}
|
||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.CustomField)) {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core'
|
||||
import { Tag } from 'src/app/data/tag'
|
||||
import { Correspondent } from 'src/app/data/correspondent'
|
||||
import { DocumentType } from 'src/app/data/document-type'
|
||||
@@ -80,6 +80,9 @@ export class BulkEditorComponent
|
||||
downloadUseFormatting: new FormControl(false),
|
||||
})
|
||||
|
||||
@Input()
|
||||
public disabled: boolean = false
|
||||
|
||||
constructor(
|
||||
private documentTypeService: DocumentTypeService,
|
||||
private tagService: TagService,
|
||||
|
@@ -96,8 +96,8 @@
|
||||
</pngx-page-header>
|
||||
|
||||
<div class="row sticky-top py-3 mt-n2 mt-md-n3 bg-body">
|
||||
<pngx-filter-editor [hidden]="isBulkEditing" [(filterRules)]="list.filterRules" [unmodifiedFilterRules]="unmodifiedFilterRules" [selectionData]="list.selectionData" #filterEditor></pngx-filter-editor>
|
||||
<pngx-bulk-editor [hidden]="!isBulkEditing"></pngx-bulk-editor>
|
||||
<pngx-filter-editor [hidden]="isBulkEditing" [disabled]="isBulkEditing" [(filterRules)]="list.filterRules" [unmodifiedFilterRules]="unmodifiedFilterRules" [selectionData]="list.selectionData" #filterEditor></pngx-filter-editor>
|
||||
<pngx-bulk-editor [hidden]="!isBulkEditing" [disabled]="!isBulkEditing"></pngx-bulk-editor>
|
||||
</div>
|
||||
|
||||
|
||||
|
@@ -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'
|
||||
@@ -153,6 +154,7 @@ describe('DocumentListComponent', () => {
|
||||
NgbTooltipModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
NgSelectModule,
|
||||
NgbTypeaheadModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
@@ -654,4 +656,42 @@ describe('DocumentListComponent', () => {
|
||||
'Custom Field 1'
|
||||
)
|
||||
})
|
||||
|
||||
it('should support hotkeys', () => {
|
||||
fixture.detectChanges()
|
||||
const resetSpy = jest.spyOn(component['filterEditor'], 'resetSelected')
|
||||
jest.spyOn(component, 'isFiltered', 'get').mockReturnValue(true)
|
||||
component.clickTag(1)
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'escape' }))
|
||||
expect(resetSpy).toHaveBeenCalled()
|
||||
|
||||
jest
|
||||
.spyOn(documentListService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([1]))
|
||||
const clearSelectedSpy = jest.spyOn(documentListService, 'selectNone')
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'escape' }))
|
||||
expect(clearSelectedSpy).toHaveBeenCalled()
|
||||
|
||||
const selectAllSpy = jest.spyOn(documentListService, 'selectAll')
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }))
|
||||
expect(selectAllSpy).toHaveBeenCalled()
|
||||
|
||||
const selectPageSpy = jest.spyOn(documentListService, 'selectPage')
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'p' }))
|
||||
expect(selectPageSpy).toHaveBeenCalled()
|
||||
|
||||
jest.spyOn(documentListService, 'documents', 'get').mockReturnValue(docs)
|
||||
fixture.detectChanges()
|
||||
const detailSpy = jest.spyOn(component, 'openDocumentDetail')
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'o' }))
|
||||
expect(detailSpy).toHaveBeenCalledWith(docs[0])
|
||||
|
||||
jest.spyOn(documentListService, 'documents', 'get').mockReturnValue(docs)
|
||||
jest
|
||||
.spyOn(documentListService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([docs[1].id]))
|
||||
fixture.detectChanges()
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'o' }))
|
||||
expect(detailSpy).toHaveBeenCalledWith(docs[1].id)
|
||||
})
|
||||
})
|
||||
|
@@ -32,6 +32,7 @@ import { ToastService } from 'src/app/services/toast.service'
|
||||
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||
import { FilterEditorComponent } from './filter-editor/filter-editor.component'
|
||||
import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component'
|
||||
import { HotKeyService } from 'src/app/services/hot-key.service'
|
||||
|
||||
@Component({
|
||||
selector: 'pngx-document-list',
|
||||
@@ -55,6 +56,7 @@ export class DocumentListComponent
|
||||
private consumerStatusService: ConsumerStatusService,
|
||||
public openDocumentsService: OpenDocumentsService,
|
||||
public settingsService: SettingsService,
|
||||
private hotKeyService: HotKeyService,
|
||||
public permissionService: PermissionsService
|
||||
) {
|
||||
super()
|
||||
@@ -215,6 +217,50 @@ export class DocumentListComponent
|
||||
this.unmodifiedFilterRules = []
|
||||
}
|
||||
})
|
||||
|
||||
this.hotKeyService
|
||||
.addShortcut({
|
||||
keys: 'escape',
|
||||
description: $localize`Reset filters / selection`,
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
if (this.list.selected.size > 0) {
|
||||
this.list.selectNone()
|
||||
} else if (this.isFiltered) {
|
||||
this.filterEditor.resetSelected()
|
||||
}
|
||||
})
|
||||
|
||||
this.hotKeyService
|
||||
.addShortcut({ keys: 'a', description: $localize`Select all` })
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
this.list.selectAll()
|
||||
})
|
||||
|
||||
this.hotKeyService
|
||||
.addShortcut({ keys: 'p', description: $localize`Select page` })
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
this.list.selectPage()
|
||||
})
|
||||
|
||||
this.hotKeyService
|
||||
.addShortcut({
|
||||
keys: 'o',
|
||||
description: $localize`Open first [selected] document`,
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe(() => {
|
||||
if (this.list.documents.length > 0) {
|
||||
if (this.list.selected.size > 0) {
|
||||
this.openDocumentDetail(Array.from(this.list.selected)[0])
|
||||
} else {
|
||||
this.openDocumentDetail(this.list.documents[0])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -297,8 +343,11 @@ export class DocumentListComponent
|
||||
})
|
||||
}
|
||||
|
||||
openDocumentDetail(document: Document) {
|
||||
this.router.navigate(['documents', document.id])
|
||||
openDocumentDetail(document: Document | number) {
|
||||
this.router.navigate([
|
||||
'documents',
|
||||
typeof document === 'number' ? document : document.id,
|
||||
])
|
||||
}
|
||||
|
||||
toggleSelected(document: Document, event: MouseEvent): void {
|
||||
|
@@ -22,7 +22,13 @@
|
||||
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||
</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>
|
||||
@@ -38,7 +44,9 @@
|
||||
(selectionModelChange)="updateRules()"
|
||||
(opened)="onTagsDropdownOpen()"
|
||||
[documentCounts]="tagDocumentCounts"
|
||||
[allowSelectNone]="true"></pngx-filterable-dropdown>
|
||||
[allowSelectNone]="true"
|
||||
[disabled]="disabled"
|
||||
shortcutKey="t"></pngx-filterable-dropdown>
|
||||
}
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
|
||||
<pngx-filterable-dropdown class="flex-fill" title="Correspondent" icon="person-fill" i18n-title
|
||||
@@ -48,7 +56,9 @@
|
||||
(selectionModelChange)="updateRules()"
|
||||
(opened)="onCorrespondentDropdownOpen()"
|
||||
[documentCounts]="correspondentDocumentCounts"
|
||||
[allowSelectNone]="true"></pngx-filterable-dropdown>
|
||||
[allowSelectNone]="true"
|
||||
[disabled]="disabled"
|
||||
shortcutKey="y"></pngx-filterable-dropdown>
|
||||
}
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
|
||||
<pngx-filterable-dropdown class="flex-fill" title="Document type" icon="file-earmark-fill" i18n-title
|
||||
@@ -58,7 +68,9 @@
|
||||
(selectionModelChange)="updateRules()"
|
||||
(opened)="onDocumentTypeDropdownOpen()"
|
||||
[documentCounts]="documentTypeDocumentCounts"
|
||||
[allowSelectNone]="true"></pngx-filterable-dropdown>
|
||||
[allowSelectNone]="true"
|
||||
[disabled]="disabled"
|
||||
shortcutKey="u"></pngx-filterable-dropdown>
|
||||
}
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.StoragePath) && storagePaths.length > 0) {
|
||||
<pngx-filterable-dropdown class="flex-fill" title="Storage path" icon="folder-fill" i18n-title
|
||||
@@ -68,7 +80,9 @@
|
||||
(selectionModelChange)="updateRules()"
|
||||
(opened)="onStoragePathDropdownOpen()"
|
||||
[documentCounts]="storagePathDocumentCounts"
|
||||
[allowSelectNone]="true"></pngx-filterable-dropdown>
|
||||
[allowSelectNone]="true"
|
||||
[disabled]="disabled"
|
||||
shortcutKey="i"></pngx-filterable-dropdown>
|
||||
}
|
||||
|
||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.CustomField) && customFields.length > 0) {
|
||||
|
@@ -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,
|
||||
@@ -92,6 +92,8 @@ import {
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { SearchService } from 'src/app/services/rest/search.service'
|
||||
|
||||
const tags: Tag[] = [
|
||||
{
|
||||
@@ -164,6 +166,7 @@ describe('FilterEditorComponent', () => {
|
||||
let settingsService: SettingsService
|
||||
let permissionsService: PermissionsService
|
||||
let httpTestingController: HttpTestingController
|
||||
let searchService: SearchService
|
||||
|
||||
beforeEach(fakeAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
@@ -222,12 +225,13 @@ describe('FilterEditorComponent', () => {
|
||||
],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
RouterTestingModule,
|
||||
RouterModule,
|
||||
NgbDropdownModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbDatepickerModule,
|
||||
NgxBootstrapIconsModule.pick(allIcons),
|
||||
NgbTypeaheadModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
@@ -235,6 +239,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) => {
|
||||
@@ -2034,6 +2039,11 @@ describe('FilterEditorComponent', () => {
|
||||
new KeyboardEvent('keyup', { key: 'Escape' })
|
||||
)
|
||||
expect(component.textFilter).toEqual('')
|
||||
const blurSpy = jest.spyOn(component.textFilterInput.nativeElement, 'blur')
|
||||
component.textFilterInput.nativeElement.dispatchEvent(
|
||||
new KeyboardEvent('keyup', { key: 'Escape' })
|
||||
)
|
||||
expect(blurSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should adjust text filter targets if more like search', () => {
|
||||
@@ -2044,4 +2054,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 ')
|
||||
})
|
||||
})
|
||||
|
@@ -7,12 +7,21 @@ import {
|
||||
OnDestroy,
|
||||
ViewChild,
|
||||
ElementRef,
|
||||
AfterViewInit,
|
||||
} from '@angular/core'
|
||||
import { Tag } from 'src/app/data/tag'
|
||||
import { Correspondent } from 'src/app/data/correspondent'
|
||||
import { DocumentType } from 'src/app/data/document-type'
|
||||
import { Subject, Subscription } from 'rxjs'
|
||||
import { debounceTime, distinctUntilChanged, filter } from 'rxjs/operators'
|
||||
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'
|
||||
import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||
@@ -82,6 +91,7 @@ import {
|
||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { CustomField } from 'src/app/data/custom-field'
|
||||
import { SearchService } from 'src/app/services/rest/search.service'
|
||||
|
||||
const TEXT_FILTER_TARGET_TITLE = 'title'
|
||||
const TEXT_FILTER_TARGET_TITLE_CONTENT = 'title-content'
|
||||
@@ -169,7 +179,7 @@ const DEFAULT_TEXT_FILTER_MODIFIER_OPTIONS = [
|
||||
})
|
||||
export class FilterEditorComponent
|
||||
extends ComponentWithPermissions
|
||||
implements OnInit, OnDestroy
|
||||
implements OnInit, OnDestroy, AfterViewInit
|
||||
{
|
||||
generateFilterName() {
|
||||
if (this.filterRules.length == 1) {
|
||||
@@ -251,7 +261,8 @@ export class FilterEditorComponent
|
||||
private documentService: DocumentService,
|
||||
private storagePathService: StoragePathService,
|
||||
public permissionsService: PermissionsService,
|
||||
private customFieldService: CustomFieldsService
|
||||
private customFieldService: CustomFieldsService,
|
||||
private searchService: SearchService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -275,6 +286,8 @@ export class FilterEditorComponent
|
||||
_moreLikeId: number
|
||||
_moreLikeDoc: Document
|
||||
|
||||
unsubscribeNotifier: Subject<any> = new Subject()
|
||||
|
||||
get textFilterTargets() {
|
||||
if (this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_MORELIKE) {
|
||||
return DEFAULT_TEXT_FILTER_TARGET_OPTIONS.concat([
|
||||
@@ -944,7 +957,9 @@ export class FilterEditorComponent
|
||||
}
|
||||
|
||||
textFilterDebounce: Subject<string>
|
||||
subscription: Subscription
|
||||
|
||||
@Input()
|
||||
public disabled: boolean = false
|
||||
|
||||
ngOnInit() {
|
||||
if (
|
||||
@@ -1000,19 +1015,29 @@ export class FilterEditorComponent
|
||||
|
||||
this.textFilterDebounce = new Subject<string>()
|
||||
|
||||
this.subscription = this.textFilterDebounce
|
||||
this.textFilterDebounce
|
||||
.pipe(
|
||||
takeUntil(this.unsubscribeNotifier),
|
||||
debounceTime(400),
|
||||
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
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.textFilterInput.nativeElement.focus()
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.textFilterDebounce.complete()
|
||||
this.unsubscribeNotifier.next(true)
|
||||
}
|
||||
|
||||
resetSelected() {
|
||||
@@ -1057,10 +1082,12 @@ export class FilterEditorComponent
|
||||
this.customFieldSelectionModel.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) {
|
||||
@@ -1071,8 +1098,12 @@ export class FilterEditorComponent
|
||||
if (filterString.length) {
|
||||
this.updateTextFilter(filterString)
|
||||
}
|
||||
} else if (event.key == 'Escape') {
|
||||
this.resetTextField()
|
||||
} else if (event.key === 'Escape') {
|
||||
if (this._textFilter?.length) {
|
||||
this.resetTextField()
|
||||
} else {
|
||||
this.textFilterInput.nativeElement.blur()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1105,4 +1136,40 @@ export class FilterEditorComponent
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user