diff --git a/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.spec.ts b/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.spec.ts index e445a73b7..c119a2c93 100644 --- a/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.spec.ts +++ b/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.spec.ts @@ -135,4 +135,10 @@ describe('DateDropdownComponent', () => { input.dispatchEvent(event) expect(eventSpy).toHaveBeenCalled() }) + + it('should correctly pass open state', () => { + expect(component.isOpen()).toBeFalsy() + component.dropdown.open() + expect(component.isOpen()).toBeTruthy() + }) }) diff --git a/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.ts b/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.ts index f47489699..73337086f 100644 --- a/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.ts +++ b/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.ts @@ -5,8 +5,9 @@ import { Output, OnInit, OnDestroy, + ViewChild, } from '@angular/core' -import { NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap' +import { NgbDateAdapter, NgbDropdown } from '@ng-bootstrap/ng-bootstrap' import { Subject, Subscription } from 'rxjs' import { debounceTime } from 'rxjs/operators' import { SettingsService } from 'src/app/services/settings.service' @@ -88,6 +89,8 @@ export class DateDropdownComponent implements OnInit, OnDestroy { @Input() disabled: boolean = false + @ViewChild(NgbDropdown) dropdown: NgbDropdown + get isActive(): boolean { return ( this.relativeDate !== null || @@ -96,6 +99,10 @@ export class DateDropdownComponent implements OnInit, OnDestroy { ) } + public isOpen(): boolean { + return this.dropdown.isOpen() + } + private datesSetDebounce$ = new Subject() private sub: Subscription diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts index fe377cc70..22d693a53 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts +++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts @@ -577,4 +577,13 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => expect(selectionModel.getSelectedItems()).toEqual([items[0]]) expect(selectionModel.getExcludedItems()).toEqual([items[1]]) }) + + it('should correctly pass open state', () => { + component.items = items + component.icon = 'tag-fill' + fixture.detectChanges() + expect(component.isOpen()).toBeFalsy() + component.dropdown.open() + expect(component.isOpen()).toBeTruthy() + }) }) diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts index 4f39d32c3..78f9ae3a6 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts +++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts @@ -425,6 +425,10 @@ export class FilterableDropdownComponent { modelIsDirty: boolean = false + public isOpen(): boolean { + return this.dropdown.isOpen() + } + private keyboardIndex: number constructor(private filterPipe: FilterPipe) { diff --git a/src-ui/src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.spec.ts b/src-ui/src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.spec.ts index eb9dfed7b..c9a582980 100644 --- a/src-ui/src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.spec.ts +++ b/src-ui/src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.spec.ts @@ -165,4 +165,10 @@ describe('PermissionsFilterDropdownComponent', () => { userID: null, }) }) + + it('should correctly pass open state', () => { + expect(component.isOpen()).toBeFalsy() + component.dropdown.open() + expect(component.isOpen()).toBeTruthy() + }) }) diff --git a/src-ui/src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.ts b/src-ui/src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.ts index b0c3e8817..dc84467f3 100644 --- a/src-ui/src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.ts +++ b/src-ui/src/app/components/common/permissions-filter-dropdown/permissions-filter-dropdown.component.ts @@ -1,4 +1,10 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core' +import { + Component, + EventEmitter, + Input, + Output, + ViewChild, +} from '@angular/core' import { first } from 'rxjs' import { User } from 'src/app/data/user' import { @@ -9,6 +15,7 @@ import { import { UserService } from 'src/app/services/rest/user.service' import { SettingsService } from 'src/app/services/settings.service' import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' +import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' export class PermissionsSelectionModel { ownerFilter: OwnerFilterType @@ -59,6 +66,8 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions hideUnowned: boolean + @ViewChild(NgbDropdown) dropdown: NgbDropdown + get isActive(): boolean { return ( this.selectionModel.ownerFilter !== OwnerFilterType.NONE || @@ -66,6 +75,10 @@ export class PermissionsFilterDropdownComponent extends ComponentWithPermissions ) } + public isOpen(): boolean { + return this.dropdown.isOpen() + } + constructor( public permissionsService: PermissionsService, userService: UserService, diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts index 127d7ef2b..c06af89ae 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts @@ -1179,4 +1179,16 @@ describe('BulkEditorComponent', () => { ) expect(component.storagePaths).toEqual(storagePaths.results) }) + + it('should correctly pass open state from dropdowns', () => { + jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) + fixture.detectChanges() + expect(component.hasOpenMenu).toBeFalsy() + component.filterableDropdowns = [ + { + isOpen: () => true, + } as any, + ] + expect(component.hasOpenMenu).toBeTruthy() + }) }) diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts index 556a1ff13..455d9f56c 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit } from '@angular/core' +import { Component, OnDestroy, OnInit, ViewChildren } 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' @@ -6,7 +6,7 @@ import { TagService } from 'src/app/services/rest/tag.service' import { CorrespondentService } from 'src/app/services/rest/correspondent.service' import { DocumentTypeService } from 'src/app/services/rest/document-type.service' import { DocumentListViewService } from 'src/app/services/document-list-view.service' -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' +import { NgbDropdown, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' import { DocumentService, SelectionDataItem, @@ -15,6 +15,7 @@ import { OpenDocumentsService } from 'src/app/services/open-documents.service' import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component' import { ChangedItems, + FilterableDropdownComponent, FilterableDropdownSelectionModel, } from '../../common/filterable-dropdown/filterable-dropdown.component' import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component' @@ -74,6 +75,10 @@ export class BulkEditorComponent downloadUseFormatting: new FormControl(false), }) + @ViewChildren(NgbDropdown) dropdowns: NgbDropdown[] + @ViewChildren(FilterableDropdownComponent) + filterableDropdowns: FilterableDropdownComponent[] + constructor( private documentTypeService: DocumentTypeService, private tagService: TagService, @@ -121,6 +126,13 @@ export class BulkEditorComponent return ownsAll } + get hasOpenMenu(): boolean { + return ( + this.filterableDropdowns.some((d) => d.isOpen()) || + this.dropdowns.some((d) => d.isOpen()) + ) + } + ngOnInit() { if ( this.permissionService.currentUserCan( diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 3cce1496b..8f98ffe2f 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -81,7 +81,7 @@
- +
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 c4420ad3f..43b3f432a 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 @@ -630,4 +630,15 @@ describe('DocumentListComponent', () => { document.dispatchEvent(new KeyboardEvent('keydown', { key: 'o' })) expect(detailSpy).toHaveBeenCalledWith(docs[0]) }) + + it('should ignore escape hotkey if there is a filter or bulk editor menu to close', () => { + fixture.detectChanges() + jest + .spyOn(component['filterEditor'], 'hasOpenMenu', 'get') + .mockReturnValue(true) + const resetSpy = jest.spyOn(component['filterEditor'], 'resetSelected') + component.clickTag(1) + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'escape' })) + expect(resetSpy).not.toHaveBeenCalled() + }) }) 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 75658f4c7..8a1c3a634 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 @@ -7,7 +7,7 @@ import { ViewChildren, } from '@angular/core' import { ActivatedRoute, convertToParamMap, Router } from '@angular/router' -import { NgbModal } from '@ng-bootstrap/ng-bootstrap' +import { NgbDropdown, NgbModal } from '@ng-bootstrap/ng-bootstrap' import { filter, first, map, Subject, switchMap, takeUntil } from 'rxjs' import { FilterRule } from 'src/app/data/filter-rule' import { @@ -37,6 +37,7 @@ import { ComponentWithPermissions } from '../with-permissions/with-permissions.c 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' +import { BulkEditorComponent } from './bulk-editor/bulk-editor.component' @Component({ selector: 'pngx-document-list', @@ -66,8 +67,13 @@ export class DocumentListComponent @ViewChild('filterEditor') private filterEditor: FilterEditorComponent + @ViewChild('bulkEditor') + private bulkEditor: BulkEditorComponent + @ViewChildren(SortableDirective) headers: QueryList + @ViewChildren(NgbDropdown) dropdowns: QueryList + displayMode = 'smallCards' // largeCards, smallCards, details unmodifiedFilterRules: FilterRule[] = [] @@ -129,6 +135,14 @@ export class DocumentListComponent return this.list.selected.size > 0 } + get hasOpenMenu(): boolean { + return ( + this.dropdowns.some((d) => d.isOpen()) || + this.filterEditor.hasOpenMenu || + this.bulkEditor.hasOpenMenu + ) + } + saveDisplayMode() { localStorage.setItem('document-list:displayMode', this.displayMode) } @@ -192,7 +206,10 @@ export class DocumentListComponent keys: 'escape', description: $localize`Reset filters / selection`, }) - .pipe(takeUntil(this.unsubscribeNotifier)) + .pipe( + takeUntil(this.unsubscribeNotifier), + filter(() => !this.hasOpenMenu) + ) .subscribe(() => { if (this.list.selected.size > 0) { this.list.selectNone() 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 5c393168f..017767458 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 @@ -1870,4 +1870,14 @@ describe('FilterEditorComponent', () => { component.itemSelected({ item: 'world', preventDefault: () => true }) expect(component.textFilter).toEqual('hello world ') }) + + it('should correctly pass open state from dropdowns', () => { + expect(component.hasOpenMenu).toBeFalsy() + component.filterableDropdowns = [ + { + isOpen: () => true, + } as any, + ] + expect(component.hasOpenMenu).toBeTruthy() + }) }) 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 b7be4b57c..1fc7e39a5 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 @@ -8,6 +8,7 @@ import { ViewChild, ElementRef, AfterViewInit, + ViewChildren, } from '@angular/core' import { Tag } from 'src/app/data/tag' import { Correspondent } from 'src/app/data/correspondent' @@ -61,6 +62,7 @@ import { FILTER_SHARED_BY_USER, } from 'src/app/data/filter-rule-type' import { + FilterableDropdownComponent, FilterableDropdownSelectionModel, Intersection, LogicalOperator, @@ -74,9 +76,13 @@ import { import { Document } from 'src/app/data/document' import { StoragePath } from 'src/app/data/storage-path' import { StoragePathService } from 'src/app/services/rest/storage-path.service' -import { RelativeDate } from '../../common/date-dropdown/date-dropdown.component' +import { + DateDropdownComponent, + RelativeDate, +} from '../../common/date-dropdown/date-dropdown.component' import { OwnerFilterType, + PermissionsFilterDropdownComponent, PermissionsSelectionModel, } from '../../common/permissions-filter-dropdown/permissions-filter-dropdown.component' import { @@ -86,6 +92,7 @@ import { } from 'src/app/services/permissions.service' import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' import { SearchService } from 'src/app/services/rest/search.service' +import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' const TEXT_FILTER_TARGET_TITLE = 'title' const TEXT_FILTER_TARGET_TITLE_CONTENT = 'title-content' @@ -269,6 +276,13 @@ export class FilterEditorComponent unsubscribeNotifier: Subject = new Subject() + @ViewChild(NgbDropdown) textFilterDropdown: NgbDropdown + @ViewChildren(FilterableDropdownComponent) + filterableDropdowns: FilterableDropdownComponent[] + @ViewChildren(DateDropdownComponent) dateDropdowns: DateDropdownComponent[] + @ViewChild(PermissionsFilterDropdownComponent) + permissionsDropdown: PermissionsFilterDropdownComponent + get textFilterTargets() { if (this.textFilterTarget == TEXT_FILTER_TARGET_FULLTEXT_MORELIKE) { return DEFAULT_TEXT_FILTER_TARGET_OPTIONS.concat([ @@ -876,6 +890,15 @@ export class FilterEditorComponent textFilterDebounce: Subject + get hasOpenMenu(): boolean { + return ( + this.textFilterDropdown.isOpen() || + this.filterableDropdowns.some((d) => d.isOpen()) || + this.dateDropdowns.some((d) => d.isOpen()) || + this.permissionsDropdown.isOpen() + ) + } + ngOnInit() { if ( this.permissionsService.currentUserCan(