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 77dc03f84..2815b6ae9 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 @@ -598,4 +598,29 @@ describe('DocumentListComponent', () => { { rule_type: FILTER_FULLTEXT_MORELIKE, value: '99' }, ]) }) + + it('should support hotkeys', () => { + fixture.detectChanges() + const resetSpy = jest.spyOn(component['filterEditor'], 'resetSelected') + 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() + + 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]) + }) }) diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index 7d27f4e3e..ff604cbc0 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -36,6 +36,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', @@ -56,6 +57,7 @@ export class DocumentListComponent private consumerStatusService: ConsumerStatusService, public openDocumentsService: OpenDocumentsService, private settingsService: SettingsService, + private hotKeyService: HotKeyService, public permissionService: PermissionsService ) { super() @@ -184,6 +186,33 @@ export class DocumentListComponent this.unmodifiedFilterRules = [] } }) + + this.hotKeyService + .addShortcut({ keys: 'escape', description: $localize`Clear selection` }) + .pipe(takeUntil(this.unsubscribeNotifier)) + .subscribe(() => { + if (this.list.selected.size > 0) { + this.list.selectNone() + } else { + this.filterEditor.resetSelected() + } + }) + + this.hotKeyService + .addShortcut({ keys: 'a', description: $localize`Select all` }) + .pipe(takeUntil(this.unsubscribeNotifier)) + .subscribe(() => { + this.list.selectAll() + }) + + this.hotKeyService + .addShortcut({ keys: 'o', description: $localize`Open first document` }) + .pipe(takeUntil(this.unsubscribeNotifier)) + .subscribe(() => { + if (this.list.documents.length > 0) { + this.openDocumentDetail(this.list.documents[0]) + } + }) } ngOnDestroy() { 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 7926ce48d..5a84b16e2 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 @@ -7,12 +7,18 @@ 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 'rxjs' +import { + debounceTime, + distinctUntilChanged, + filter, + 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' @@ -163,7 +169,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) { @@ -256,6 +262,8 @@ export class FilterEditorComponent _moreLikeId: number _moreLikeDoc: Document + unsubscribeNotifier: Subject = new Subject() + get textFilterTargets() { if (this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_MORELIKE) { return DEFAULT_TEXT_FILTER_TARGET_OPTIONS.concat([ @@ -862,7 +870,6 @@ export class FilterEditorComponent } textFilterDebounce: Subject - subscription: Subscription ngOnInit() { if ( @@ -908,8 +915,9 @@ export class FilterEditorComponent this.textFilterDebounce = new Subject() - this.subscription = this.textFilterDebounce + this.textFilterDebounce .pipe( + takeUntil(this.unsubscribeNotifier), debounceTime(400), distinctUntilChanged(), filter((query) => !query.length || query.length > 2) @@ -919,8 +927,12 @@ export class FilterEditorComponent if (this._textFilter) this.documentService.searchQuery = this._textFilter } + ngAfterViewInit() { + this.textFilterInput.nativeElement.focus() + } + ngOnDestroy() { - this.textFilterDebounce.complete() + this.unsubscribeNotifier.next(true) } resetSelected() {