From a1e4365ff2df1d679c9f3cab3bb9c407643b7d39 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 13 May 2024 09:12:02 -0700 Subject: [PATCH] Enhancement: global search tweaks (#6674) --- src-ui/messages.xlf | 50 ++++++------ .../global-search.component.html | 16 ++-- .../global-search.component.spec.ts | 76 ++++++++++++++----- .../global-search/global-search.component.ts | 67 ++++++++-------- src-ui/src/styles.scss | 3 +- 5 files changed, 130 insertions(+), 82 deletions(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 4be198d69..f3e43d396 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -1013,7 +1013,7 @@ src/app/components/app-frame/global-search/global-search.component.ts - 92 + 93 @@ -2113,11 +2113,11 @@ src/app/components/app-frame/global-search/global-search.component.html - 51 + 55 src/app/components/app-frame/global-search/global-search.component.html - 68 + 72 src/app/components/common/input/permissions/permissions-form/permissions-form.component.html @@ -2676,7 +2676,7 @@ Advanced search src/app/components/app-frame/global-search/global-search.component.html - 19 + 23 src/app/components/document-list/filter-editor/filter-editor.component.ts @@ -2687,25 +2687,25 @@ Open src/app/components/app-frame/global-search/global-search.component.html - 45 + 49 src/app/components/app-frame/global-search/global-search.component.html - 48 + 52 Filter documents src/app/components/app-frame/global-search/global-search.component.html - 54 + 58 Download src/app/components/app-frame/global-search/global-search.component.html - 65 + 69 src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html @@ -2732,113 +2732,113 @@ No results src/app/components/app-frame/global-search/global-search.component.html - 79 + 83 Documents src/app/components/app-frame/global-search/global-search.component.html - 82 + 86 Saved Views src/app/components/app-frame/global-search/global-search.component.html - 88 + 92 Tags src/app/components/app-frame/global-search/global-search.component.html - 95 + 99 Correspondents src/app/components/app-frame/global-search/global-search.component.html - 102 + 106 Document types src/app/components/app-frame/global-search/global-search.component.html - 109 + 113 Storage paths src/app/components/app-frame/global-search/global-search.component.html - 116 + 120 Users src/app/components/app-frame/global-search/global-search.component.html - 123 + 127 Groups src/app/components/app-frame/global-search/global-search.component.html - 130 + 134 Custom fields src/app/components/app-frame/global-search/global-search.component.html - 137 + 141 Mail accounts src/app/components/app-frame/global-search/global-search.component.html - 144 + 148 Mail rules src/app/components/app-frame/global-search/global-search.component.html - 151 + 155 Workflows src/app/components/app-frame/global-search/global-search.component.html - 158 + 162 Successfully updated object. src/app/components/app-frame/global-search/global-search.component.ts - 168 + 175 src/app/components/app-frame/global-search/global-search.component.ts - 206 + 213 Error occurred saving object. src/app/components/app-frame/global-search/global-search.component.ts - 171 + 178 src/app/components/app-frame/global-search/global-search.component.ts - 209 + 216 diff --git a/src-ui/src/app/components/app-frame/global-search/global-search.component.html b/src-ui/src/app/components/app-frame/global-search/global-search.component.html index 317303de7..eeb118967 100644 --- a/src-ui/src/app/components/app-frame/global-search/global-search.component.html +++ b/src-ui/src/app/components/app-frame/global-search/global-search.component.html @@ -6,15 +6,19 @@ + autocomplete="off" + spellcheck="false" + [(ngModel)]="query" + (ngModelChange)="this.queryDebounce.next($event)" + (keydown)="searchInputKeyDown($event)" + ngbDropdownAnchor> @if (loading) { } - @if (query && (searchResults?.documents.length === searchService.searchResultObjectLimit || searchService.searchDbOnly)) { + @if (query) { Advanced search @@ -25,7 +29,7 @@ @@ -36,7 +40,7 @@ @@ -56,7 +60,7 @@ @if (type !== DataType.SavedView && type !== DataType.Workflow && type !== DataType.CustomField && type !== DataType.Group && type !== DataType.User && type !== DataType.MailAccount && type !== DataType.MailRule) { diff --git a/src-ui/src/app/components/app-frame/global-search/global-search.component.spec.ts b/src-ui/src/app/components/app-frame/global-search/global-search.component.spec.ts index 514918584..54ea735b6 100644 --- a/src-ui/src/app/components/app-frame/global-search/global-search.component.spec.ts +++ b/src-ui/src/app/components/app-frame/global-search/global-search.component.spec.ts @@ -36,6 +36,7 @@ import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-e import { ElementRef } from '@angular/core' import { ToastService } from 'src/app/services/toast.service' import { DataType } from 'src/app/data/datatype' +import { queryParamsFromFilterRules } from 'src/app/utils/query-params' const searchResults = { total: 11, @@ -248,10 +249,7 @@ describe('GlobalSearchComponent', () => { expect(blurSpy).toHaveBeenCalled() component.searchResults = { total: 1 } as any - component.resultsDropdown.close() - const openSpy = jest.spyOn(component.resultsDropdown, 'open') - component.searchInputKeyDown(new KeyboardEvent('keydown', { key: 'Enter' })) - expect(openSpy).toHaveBeenCalled() + component.resultsDropdown.open() component.searchInputKeyDown( new KeyboardEvent('keydown', { key: 'ArrowDown' }) @@ -260,6 +258,13 @@ describe('GlobalSearchComponent', () => { const closeSpy = jest.spyOn(component.resultsDropdown, 'close') component.dropdownKeyDown(new KeyboardEvent('keydown', { key: 'Escape' })) expect(closeSpy).toHaveBeenCalled() + + component.searchResults = searchResults as any + component.resultsDropdown.open() + component.query = 'test' + const advancedSearchSpy = jest.spyOn(component, 'runAdvanedSearch') + component.searchInputKeyDown(new KeyboardEvent('keydown', { key: 'Enter' })) + expect(advancedSearchSpy).toHaveBeenCalled() }) it('should search on query debounce', fakeAsync(() => { @@ -276,7 +281,6 @@ describe('GlobalSearchComponent', () => { it('should support primary action', () => { const object = { id: 1 } const routerSpy = jest.spyOn(router, 'navigate') - const qfSpy = jest.spyOn(documentListViewService, 'quickFilter') const modalSpy = jest.spyOn(modalService, 'open') let modal: NgbModalRef @@ -289,23 +293,41 @@ describe('GlobalSearchComponent', () => { expect(routerSpy).toHaveBeenCalledWith(['/view', object.id]) component.primaryAction(DataType.Correspondent, object) - expect(qfSpy).toHaveBeenCalledWith([ - { rule_type: FILTER_HAS_CORRESPONDENT_ANY, value: object.id.toString() }, + expect(routerSpy).toHaveBeenCalledWith([ + '/documents', + queryParamsFromFilterRules([ + { + rule_type: FILTER_HAS_CORRESPONDENT_ANY, + value: object.id.toString(), + }, + ]), ]) component.primaryAction(DataType.DocumentType, object) - expect(qfSpy).toHaveBeenCalledWith([ - { rule_type: FILTER_HAS_DOCUMENT_TYPE_ANY, value: object.id.toString() }, + expect(routerSpy).toHaveBeenCalledWith([ + '/documents', + queryParamsFromFilterRules([ + { + rule_type: FILTER_HAS_DOCUMENT_TYPE_ANY, + value: object.id.toString(), + }, + ]), ]) component.primaryAction(DataType.StoragePath, object) - expect(qfSpy).toHaveBeenCalledWith([ - { rule_type: FILTER_HAS_STORAGE_PATH_ANY, value: object.id.toString() }, + expect(routerSpy).toHaveBeenCalledWith([ + '/documents', + queryParamsFromFilterRules([ + { rule_type: FILTER_HAS_STORAGE_PATH_ANY, value: object.id.toString() }, + ]), ]) component.primaryAction(DataType.Tag, object) - expect(qfSpy).toHaveBeenCalledWith([ - { rule_type: FILTER_HAS_TAGS_ANY, value: object.id.toString() }, + expect(routerSpy).toHaveBeenCalledWith([ + '/documents', + queryParamsFromFilterRules([ + { rule_type: FILTER_HAS_TAGS_ANY, value: object.id.toString() }, + ]), ]) component.primaryAction(DataType.User, object) @@ -450,13 +472,6 @@ describe('GlobalSearchComponent', () => { expect(focusSpy).toHaveBeenCalled() }) - it('should prevent event propagation for keyboard events on buttons that are not arrows', () => { - const event = { stopImmediatePropagation: jest.fn(), key: 'Enter' } - const stopPropagationSpy = jest.spyOn(event, 'stopImmediatePropagation') - component.onButtonKeyDown(event as any) - expect(stopPropagationSpy).toHaveBeenCalled() - }) - it('should support explicit advanced search', () => { const qfSpy = jest.spyOn(documentListViewService, 'quickFilter') component.query = 'test' @@ -465,4 +480,25 @@ describe('GlobalSearchComponent', () => { { rule_type: FILTER_FULLTEXT_QUERY, value: 'test' }, ]) }) + + it('should support open in new window', () => { + const openSpy = jest.spyOn(window, 'open') + const event = new Event('click') + event['ctrlKey'] = true + component.primaryAction(DataType.Document, { id: 2 }, event as any) + expect(openSpy).toHaveBeenCalledWith('/documents/2', '_blank') + + component.searchResults = searchResults as any + component.resultsDropdown.open() + fixture.detectChanges() + + const button = component.primaryButtons.get(0).nativeElement + const keyboardEvent = new KeyboardEvent('keydown', { + key: 'Enter', + ctrlKey: true, + }) + const dispatchSpy = jest.spyOn(button, 'dispatchEvent') + button.dispatchEvent(keyboardEvent) + expect(dispatchSpy).toHaveBeenCalledTimes(2) // once for keydown, second for click + }) }) diff --git a/src-ui/src/app/components/app-frame/global-search/global-search.component.ts b/src-ui/src/app/components/app-frame/global-search/global-search.component.ts index 35973501f..2b1a078c4 100644 --- a/src-ui/src/app/components/app-frame/global-search/global-search.component.ts +++ b/src-ui/src/app/components/app-frame/global-search/global-search.component.ts @@ -41,6 +41,7 @@ import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component' import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component' import { HotKeyService } from 'src/app/services/hot-key.service' +import { queryParamsFromFilterRules } from 'src/app/utils/query-params' @Component({ selector: 'pngx-global-search', @@ -87,7 +88,7 @@ export class GlobalSearchComponent implements OnInit { }) } - ngOnInit() { + public ngOnInit() { this.hotkeyService .addShortcut({ keys: '/', description: $localize`Global search` }) .subscribe(() => { @@ -104,17 +105,22 @@ export class GlobalSearchComponent implements OnInit { }) } - public primaryAction(type: string, object: ObjectWithId) { + public primaryAction( + type: string, + object: ObjectWithId, + event: PointerEvent = null + ) { + const newWindow = event?.metaKey || event?.ctrlKey this.reset(true) let filterRuleType: number let editDialogComponent: any let size: string = 'md' switch (type) { case DataType.Document: - this.router.navigate(['/documents', object.id]) + this.navigateOrOpenInNewWindow(['/documents', object.id], newWindow) return case DataType.SavedView: - this.router.navigate(['/view', object.id]) + this.navigateOrOpenInNewWindow(['/view', object.id], newWindow) return case DataType.Correspondent: filterRuleType = FILTER_HAS_CORRESPONDENT_ANY @@ -154,9 +160,10 @@ export class GlobalSearchComponent implements OnInit { } if (filterRuleType) { - this.documentListViewService.quickFilter([ + let params = queryParamsFromFilterRules([ { rule_type: filterRuleType, value: object.id.toString() }, ]) + this.navigateOrOpenInNewWindow(['/documents', params], newWindow) } else if (editDialogComponent) { const modalRef: NgbModalRef = this.modalService.open( editDialogComponent, @@ -213,6 +220,7 @@ export class GlobalSearchComponent implements OnInit { private reset(close: boolean = false) { this.queryDebounce.next(null) + this.query = null this.searchResults = null this.currentItemIndex = -1 if (close) { @@ -233,7 +241,7 @@ export class GlobalSearchComponent implements OnInit { item.nativeElement.focus() } - onItemHover(event: MouseEvent) { + public onItemHover(event: MouseEvent) { const item: ElementRef = this.resultItems .toArray() .find((item) => item.nativeElement === event.currentTarget) @@ -241,7 +249,7 @@ export class GlobalSearchComponent implements OnInit { this.setCurrentItem() } - onButtonHover(event: MouseEvent) { + public onButtonHover(event: MouseEvent) { ;(event.currentTarget as HTMLElement).focus() } @@ -262,19 +270,14 @@ export class GlobalSearchComponent implements OnInit { event.preventDefault() this.currentItemIndex = this.searchResults.total - 1 this.setCurrentItem() - } else if ( - event.key === 'Enter' && - this.searchResults?.total === 1 && - this.resultsDropdown.isOpen() - ) { - this.primaryButtons.first.nativeElement.click() - this.searchInput.nativeElement.blur() - } else if ( - event.key === 'Enter' && - this.searchResults?.total && - !this.resultsDropdown.isOpen() - ) { - this.resultsDropdown.open() + } else if (event.key === 'Enter') { + if (this.searchResults?.total === 1 && this.resultsDropdown.isOpen()) { + this.primaryButtons.first.nativeElement.click() + this.searchInput.nativeElement.blur() + } else if (this.query?.length) { + this.runAdvanedSearch() + this.reset(true) + } } else if (event.key === 'Escape' && !this.resultsDropdown.isOpen()) { if (this.query?.length) { this.reset(true) @@ -284,7 +287,7 @@ export class GlobalSearchComponent implements OnInit { } } - dropdownKeyDown(event: KeyboardEvent) { + public dropdownKeyDown(event: KeyboardEvent) { if ( this.searchResults?.total && this.resultsDropdown.isOpen() && @@ -327,14 +330,9 @@ export class GlobalSearchComponent implements OnInit { } } - onButtonKeyDown(event: KeyboardEvent) { - // prevents ngBootstrap issue with keydown events - if ( - !['ArrowDown', 'ArrowUp', 'ArrowRight', 'ArrowLeft', 'Escape'].includes( - event.key - ) - ) { - event.stopImmediatePropagation() + public onButtonKeyDown(event: KeyboardEvent) { + if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { + event.target.dispatchEvent(new MouseEvent('click', { ctrlKey: true })) } } @@ -373,10 +371,19 @@ export class GlobalSearchComponent implements OnInit { ) } - runAdvanedSearch() { + public runAdvanedSearch() { this.documentListViewService.quickFilter([ { rule_type: FILTER_FULLTEXT_QUERY, value: this.query }, ]) this.reset(true) } + + private navigateOrOpenInNewWindow(commands: any, newWindow: boolean = false) { + if (newWindow) { + const url = this.router.serializeUrl(this.router.createUrlTree(commands)) + window.open(url, '_blank') + } else { + this.router.navigate(commands) + } + } } diff --git a/src-ui/src/styles.scss b/src-ui/src/styles.scss index 04b908720..be1262dc8 100644 --- a/src-ui/src/styles.scss +++ b/src-ui/src/styles.scss @@ -332,7 +332,8 @@ textarea, } } -.input-group .form-control-sm { +.input-group .form-control-sm, +.input-group .btn-sm { // accommodate larger font size on mobile padding-top: .15rem; padding-bottom: .15rem;