Add shortcut keys for filters
This commit is contained in:
parent
c7a1b5b514
commit
a72adb0e75
@ -26,6 +26,7 @@ import { TagComponent } from '../tag/tag.component'
|
|||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
import { HotKeyService } from 'src/app/services/hot-key.service'
|
||||||
|
|
||||||
const items: Tag[] = [
|
const items: Tag[] = [
|
||||||
{
|
{
|
||||||
@ -53,6 +54,7 @@ let selectionModel: FilterableDropdownSelectionModel
|
|||||||
describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => {
|
describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => {
|
||||||
let component: FilterableDropdownComponent
|
let component: FilterableDropdownComponent
|
||||||
let fixture: ComponentFixture<FilterableDropdownComponent>
|
let fixture: ComponentFixture<FilterableDropdownComponent>
|
||||||
|
let hotkeyService: HotKeyService
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@ -72,6 +74,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
|
hotkeyService = TestBed.inject(HotKeyService)
|
||||||
fixture = TestBed.createComponent(FilterableDropdownComponent)
|
fixture = TestBed.createComponent(FilterableDropdownComponent)
|
||||||
component = fixture.componentInstance
|
component = fixture.componentInstance
|
||||||
selectionModel = new FilterableDropdownSelectionModel()
|
selectionModel = new FilterableDropdownSelectionModel()
|
||||||
@ -577,4 +580,14 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
|
|||||||
expect(selectionModel.getSelectedItems()).toEqual([items[0]])
|
expect(selectionModel.getSelectedItems()).toEqual([items[0]])
|
||||||
expect(selectionModel.getExcludedItems()).toEqual([items[1]])
|
expect(selectionModel.getExcludedItems()).toEqual([items[1]])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support shortcut keys', () => {
|
||||||
|
component.items = items
|
||||||
|
component.icon = 'tag-fill'
|
||||||
|
component.shortcutKey = 't'
|
||||||
|
fixture.detectChanges()
|
||||||
|
const openSpy = jest.spyOn(component.dropdown, 'open')
|
||||||
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: 't' }))
|
||||||
|
expect(openSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -5,14 +5,17 @@ import {
|
|||||||
Output,
|
Output,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
|
OnInit,
|
||||||
|
OnDestroy,
|
||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { FilterPipe } from 'src/app/pipes/filter.pipe'
|
import { FilterPipe } from 'src/app/pipes/filter.pipe'
|
||||||
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { ToggleableItemState } from './toggleable-dropdown-button/toggleable-dropdown-button.component'
|
import { ToggleableItemState } from './toggleable-dropdown-button/toggleable-dropdown-button.component'
|
||||||
import { MatchingModel } from 'src/app/data/matching-model'
|
import { MatchingModel } from 'src/app/data/matching-model'
|
||||||
import { Subject } from 'rxjs'
|
import { Subject, filter, take, takeUntil } from 'rxjs'
|
||||||
import { SelectionDataItem } from 'src/app/services/rest/document.service'
|
import { SelectionDataItem } from 'src/app/services/rest/document.service'
|
||||||
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
|
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
|
||||||
|
import { HotKeyService } from 'src/app/services/hot-key.service'
|
||||||
|
|
||||||
export interface ChangedItems {
|
export interface ChangedItems {
|
||||||
itemsToAdd: MatchingModel[]
|
itemsToAdd: MatchingModel[]
|
||||||
@ -322,7 +325,7 @@ export class FilterableDropdownSelectionModel {
|
|||||||
templateUrl: './filterable-dropdown.component.html',
|
templateUrl: './filterable-dropdown.component.html',
|
||||||
styleUrls: ['./filterable-dropdown.component.scss'],
|
styleUrls: ['./filterable-dropdown.component.scss'],
|
||||||
})
|
})
|
||||||
export class FilterableDropdownComponent {
|
export class FilterableDropdownComponent implements OnDestroy, OnInit {
|
||||||
@ViewChild('listFilterTextInput') listFilterTextInput: ElementRef
|
@ViewChild('listFilterTextInput') listFilterTextInput: ElementRef
|
||||||
@ViewChild('dropdown') dropdown: NgbDropdown
|
@ViewChild('dropdown') dropdown: NgbDropdown
|
||||||
@ViewChild('buttonItems') buttonItems: ElementRef
|
@ViewChild('buttonItems') buttonItems: ElementRef
|
||||||
@ -419,6 +422,9 @@ export class FilterableDropdownComponent {
|
|||||||
@Input()
|
@Input()
|
||||||
documentCounts: SelectionDataItem[]
|
documentCounts: SelectionDataItem[]
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
shortcutKey: string
|
||||||
|
|
||||||
get name(): string {
|
get name(): string {
|
||||||
return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
|
return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
|
||||||
}
|
}
|
||||||
@ -427,12 +433,39 @@ export class FilterableDropdownComponent {
|
|||||||
|
|
||||||
private keyboardIndex: number
|
private keyboardIndex: number
|
||||||
|
|
||||||
constructor(private filterPipe: FilterPipe) {
|
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private filterPipe: FilterPipe,
|
||||||
|
private hotkeyService: HotKeyService
|
||||||
|
) {
|
||||||
this.selectionModelChange.subscribe((updatedModel) => {
|
this.selectionModelChange.subscribe((updatedModel) => {
|
||||||
this.modelIsDirty = updatedModel.isDirty()
|
this.modelIsDirty = updatedModel.isDirty()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (this.shortcutKey) {
|
||||||
|
this.hotkeyService
|
||||||
|
.addShortcut({
|
||||||
|
keys: this.shortcutKey,
|
||||||
|
description: $localize`Open ${this.title} filter`,
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
takeUntil(this.unsubscribeNotifier),
|
||||||
|
filter(() => !this.disabled)
|
||||||
|
)
|
||||||
|
.subscribe(() => {
|
||||||
|
this.dropdown.open()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.unsubscribeNotifier.next(true)
|
||||||
|
this.unsubscribeNotifier.complete()
|
||||||
|
}
|
||||||
|
|
||||||
applyClicked() {
|
applyClicked() {
|
||||||
if (this.selectionModel.isDirty()) {
|
if (this.selectionModel.isDirty()) {
|
||||||
this.dropdown.close()
|
this.dropdown.close()
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
<pngx-filterable-dropdown title="Tags" icon="tag-fill" i18n-title
|
<pngx-filterable-dropdown title="Tags" icon="tag-fill" i18n-title
|
||||||
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
filterPlaceholder="Filter tags" i18n-filterPlaceholder
|
||||||
[items]="tags"
|
[items]="tags"
|
||||||
[disabled]="!userCanEditAll"
|
[disabled]="!userCanEditAll || disabled"
|
||||||
[editing]="true"
|
[editing]="true"
|
||||||
[manyToOne]="true"
|
[manyToOne]="true"
|
||||||
[applyOnClose]="applyOnClose"
|
[applyOnClose]="applyOnClose"
|
||||||
@ -29,49 +29,53 @@
|
|||||||
(opened)="openTagsDropdown()"
|
(opened)="openTagsDropdown()"
|
||||||
[(selectionModel)]="tagSelectionModel"
|
[(selectionModel)]="tagSelectionModel"
|
||||||
[documentCounts]="tagDocumentCounts"
|
[documentCounts]="tagDocumentCounts"
|
||||||
(apply)="setTags($event)">
|
(apply)="setTags($event)"
|
||||||
|
shortcutKey="t">
|
||||||
</pngx-filterable-dropdown>
|
</pngx-filterable-dropdown>
|
||||||
}
|
}
|
||||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
|
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
|
||||||
<pngx-filterable-dropdown title="Correspondent" icon="person-fill" i18n-title
|
<pngx-filterable-dropdown title="Correspondent" icon="person-fill" i18n-title
|
||||||
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
|
||||||
[items]="correspondents"
|
[items]="correspondents"
|
||||||
[disabled]="!userCanEditAll"
|
[disabled]="!userCanEditAll || disabled"
|
||||||
[editing]="true"
|
[editing]="true"
|
||||||
[applyOnClose]="applyOnClose"
|
[applyOnClose]="applyOnClose"
|
||||||
[createRef]="createCorrespondent.bind(this)"
|
[createRef]="createCorrespondent.bind(this)"
|
||||||
(opened)="openCorrespondentDropdown()"
|
(opened)="openCorrespondentDropdown()"
|
||||||
[(selectionModel)]="correspondentSelectionModel"
|
[(selectionModel)]="correspondentSelectionModel"
|
||||||
[documentCounts]="correspondentDocumentCounts"
|
[documentCounts]="correspondentDocumentCounts"
|
||||||
(apply)="setCorrespondents($event)">
|
(apply)="setCorrespondents($event)"
|
||||||
|
shortcutKey="y">
|
||||||
</pngx-filterable-dropdown>
|
</pngx-filterable-dropdown>
|
||||||
}
|
}
|
||||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
|
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
|
||||||
<pngx-filterable-dropdown title="Document type" icon="file-earmark-fill" i18n-title
|
<pngx-filterable-dropdown title="Document type" icon="file-earmark-fill" i18n-title
|
||||||
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
filterPlaceholder="Filter document types" i18n-filterPlaceholder
|
||||||
[items]="documentTypes"
|
[items]="documentTypes"
|
||||||
[disabled]="!userCanEditAll"
|
[disabled]="!userCanEditAll || disabled"
|
||||||
[editing]="true"
|
[editing]="true"
|
||||||
[applyOnClose]="applyOnClose"
|
[applyOnClose]="applyOnClose"
|
||||||
[createRef]="createDocumentType.bind(this)"
|
[createRef]="createDocumentType.bind(this)"
|
||||||
(opened)="openDocumentTypeDropdown()"
|
(opened)="openDocumentTypeDropdown()"
|
||||||
[(selectionModel)]="documentTypeSelectionModel"
|
[(selectionModel)]="documentTypeSelectionModel"
|
||||||
[documentCounts]="documentTypeDocumentCounts"
|
[documentCounts]="documentTypeDocumentCounts"
|
||||||
(apply)="setDocumentTypes($event)">
|
(apply)="setDocumentTypes($event)"
|
||||||
|
shortcutKey="u">
|
||||||
</pngx-filterable-dropdown>
|
</pngx-filterable-dropdown>
|
||||||
}
|
}
|
||||||
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
|
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
|
||||||
<pngx-filterable-dropdown title="Storage path" icon="folder-fill" i18n-title
|
<pngx-filterable-dropdown title="Storage path" icon="folder-fill" i18n-title
|
||||||
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
|
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
|
||||||
[items]="storagePaths"
|
[items]="storagePaths"
|
||||||
[disabled]="!userCanEditAll"
|
[disabled]="!userCanEditAll || disabled"
|
||||||
[editing]="true"
|
[editing]="true"
|
||||||
[applyOnClose]="applyOnClose"
|
[applyOnClose]="applyOnClose"
|
||||||
[createRef]="createStoragePath.bind(this)"
|
[createRef]="createStoragePath.bind(this)"
|
||||||
(opened)="openStoragePathDropdown()"
|
(opened)="openStoragePathDropdown()"
|
||||||
[(selectionModel)]="storagePathsSelectionModel"
|
[(selectionModel)]="storagePathsSelectionModel"
|
||||||
[documentCounts]="storagePathDocumentCounts"
|
[documentCounts]="storagePathDocumentCounts"
|
||||||
(apply)="setStoragePaths($event)">
|
(apply)="setStoragePaths($event)"
|
||||||
|
shortcutKey="i">
|
||||||
</pngx-filterable-dropdown>
|
</pngx-filterable-dropdown>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@ -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 { 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'
|
||||||
@ -74,6 +74,9 @@ export class BulkEditorComponent
|
|||||||
downloadUseFormatting: new FormControl(false),
|
downloadUseFormatting: new FormControl(false),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
public disabled: boolean = false
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private documentTypeService: DocumentTypeService,
|
private documentTypeService: DocumentTypeService,
|
||||||
private tagService: TagService,
|
private tagService: TagService,
|
||||||
|
@ -80,8 +80,8 @@
|
|||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
|
|
||||||
<div class="row sticky-top py-3 mt-n2 mt-md-n3 bg-body">
|
<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-filter-editor [hidden]="isBulkEditing" [disabled]="isBulkEditing" [(filterRules)]="list.filterRules" [unmodifiedFilterRules]="unmodifiedFilterRules" [selectionData]="list.selectionData" #filterEditor></pngx-filter-editor>
|
||||||
<pngx-bulk-editor [hidden]="!isBulkEditing"></pngx-bulk-editor>
|
<pngx-bulk-editor [hidden]="!isBulkEditing" [disabled]="!isBulkEditing"></pngx-bulk-editor>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,7 +44,9 @@
|
|||||||
(selectionModelChange)="updateRules()"
|
(selectionModelChange)="updateRules()"
|
||||||
(opened)="onTagsDropdownOpen()"
|
(opened)="onTagsDropdownOpen()"
|
||||||
[documentCounts]="tagDocumentCounts"
|
[documentCounts]="tagDocumentCounts"
|
||||||
[allowSelectNone]="true"></pngx-filterable-dropdown>
|
[allowSelectNone]="true"
|
||||||
|
[disabled]="disabled"
|
||||||
|
shortcutKey="t"></pngx-filterable-dropdown>
|
||||||
}
|
}
|
||||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
|
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
|
||||||
<pngx-filterable-dropdown class="flex-fill" title="Correspondent" icon="person-fill" i18n-title
|
<pngx-filterable-dropdown class="flex-fill" title="Correspondent" icon="person-fill" i18n-title
|
||||||
@ -54,7 +56,9 @@
|
|||||||
(selectionModelChange)="updateRules()"
|
(selectionModelChange)="updateRules()"
|
||||||
(opened)="onCorrespondentDropdownOpen()"
|
(opened)="onCorrespondentDropdownOpen()"
|
||||||
[documentCounts]="correspondentDocumentCounts"
|
[documentCounts]="correspondentDocumentCounts"
|
||||||
[allowSelectNone]="true"></pngx-filterable-dropdown>
|
[allowSelectNone]="true"
|
||||||
|
[disabled]="disabled"
|
||||||
|
shortcutKey="y"></pngx-filterable-dropdown>
|
||||||
}
|
}
|
||||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
|
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
|
||||||
<pngx-filterable-dropdown class="flex-fill" title="Document type" icon="file-earmark-fill" i18n-title
|
<pngx-filterable-dropdown class="flex-fill" title="Document type" icon="file-earmark-fill" i18n-title
|
||||||
@ -64,7 +68,9 @@
|
|||||||
(selectionModelChange)="updateRules()"
|
(selectionModelChange)="updateRules()"
|
||||||
(opened)="onDocumentTypeDropdownOpen()"
|
(opened)="onDocumentTypeDropdownOpen()"
|
||||||
[documentCounts]="documentTypeDocumentCounts"
|
[documentCounts]="documentTypeDocumentCounts"
|
||||||
[allowSelectNone]="true"></pngx-filterable-dropdown>
|
[allowSelectNone]="true"
|
||||||
|
[disabled]="disabled"
|
||||||
|
shortcutKey="u"></pngx-filterable-dropdown>
|
||||||
}
|
}
|
||||||
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
|
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
|
||||||
<pngx-filterable-dropdown class="flex-fill" title="Storage path" icon="folder-fill" i18n-title
|
<pngx-filterable-dropdown class="flex-fill" title="Storage path" icon="folder-fill" i18n-title
|
||||||
@ -74,7 +80,9 @@
|
|||||||
(selectionModelChange)="updateRules()"
|
(selectionModelChange)="updateRules()"
|
||||||
(opened)="onStoragePathDropdownOpen()"
|
(opened)="onStoragePathDropdownOpen()"
|
||||||
[documentCounts]="storagePathDocumentCounts"
|
[documentCounts]="storagePathDocumentCounts"
|
||||||
[allowSelectNone]="true"></pngx-filterable-dropdown>
|
[allowSelectNone]="true"
|
||||||
|
[disabled]="disabled"
|
||||||
|
shortcutKey="i"></pngx-filterable-dropdown>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
@ -876,6 +876,9 @@ export class FilterEditorComponent
|
|||||||
|
|
||||||
textFilterDebounce: Subject<string>
|
textFilterDebounce: Subject<string>
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
public disabled: boolean = false
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
if (
|
if (
|
||||||
this.permissionsService.currentUserCan(
|
this.permissionsService.currentUserCan(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user