diff --git a/src-ui/setup-jest.ts b/src-ui/setup-jest.ts index 8e754589b..3486d17fc 100644 --- a/src-ui/setup-jest.ts +++ b/src-ui/setup-jest.ts @@ -85,6 +85,7 @@ const mock = () => { } } +Object.defineProperty(window, 'open', { value: jest.fn() }) Object.defineProperty(window, 'localStorage', { value: mock() }) Object.defineProperty(window, 'sessionStorage', { value: mock() }) Object.defineProperty(window, 'getComputedStyle', { 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 dc21da2f2..b8defc200 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 @@ -8,11 +8,11 @@ -
- +
+ {{item[nameProp]}}
- @if (type !== 'workflow' && type !== 'customField' && type !== 'group' && type !== 'user') { -
-
+
@if (searchResults?.total === 0) { } @else { diff --git a/src-ui/src/app/components/app-frame/global-search/global-search.component.scss b/src-ui/src/app/components/app-frame/global-search/global-search.component.scss index 7b0bcc3bf..60cb22207 100644 --- a/src-ui/src/app/components/app-frame/global-search/global-search.component.scss +++ b/src-ui/src/app/components/app-frame/global-search/global-search.component.scss @@ -31,3 +31,11 @@ form { * { --pngx-focus-alpha: 0; } + +.cursor-pointer { + cursor: pointer; +} + +.mh-75 { + max-height: 75vh; +} 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 681589cbf..342ee6975 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 @@ -19,9 +19,20 @@ import { DocumentListViewService } from 'src/app/services/document-list-view.ser import { HttpClient } from '@angular/common/http' import { HttpClientTestingModule } from '@angular/common/http/testing' import { FormsModule, ReactiveFormsModule } from '@angular/forms' -import { FILTER_HAS_CORRESPONDENT_ANY } from 'src/app/data/filter-rule-type' +import { + FILTER_HAS_ANY_TAG, + FILTER_HAS_CORRESPONDENT_ANY, + FILTER_HAS_DOCUMENT_TYPE_ANY, + FILTER_HAS_STORAGE_PATH_ANY, + FILTER_HAS_TAGS_ANY, +} from 'src/app/data/filter-rule-type' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { DocumentService } from 'src/app/services/rest/document.service' +import { MailRuleEditDialogComponent } from '../../common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component' +import { MailAccountEditDialogComponent } from '../../common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component' +import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component' +import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component' +import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component' const searchResults = { total: 11, @@ -133,13 +144,6 @@ describe('GlobalSearchComponent', () => { fixture.detectChanges() }) - it('should initialize properties', () => { - expect(component.query).toBeUndefined() - expect(component.queryDebounce).toBeInstanceOf(Subject) - expect(component.searchResults).toBeUndefined() - expect(component['currentItemIndex']).toBeUndefined() - }) - it('should handle keyboard events', () => { const focusSpy = jest.spyOn(component.searchInput.nativeElement, 'focus') component.handleKeyboardEvent( @@ -176,7 +180,20 @@ describe('GlobalSearchComponent', () => { expect(component['currentItemIndex']).toBe(0) expect(zeroItemSpy).toHaveBeenCalled() + const inputFocusSpy = jest.spyOn( + component.searchInput.nativeElement, + 'focus' + ) + component.handleKeyboardEvent( + new KeyboardEvent('keydown', { key: 'ArrowUp' }) + ) + expect(component['currentItemIndex']).toBe(-1) + expect(inputFocusSpy).toHaveBeenCalled() + const actionSpy = jest.spyOn(component, 'primaryAction') + component.handleKeyboardEvent( + new KeyboardEvent('keydown', { key: 'ArrowDown' }) + ) component.handleKeyboardEvent( new KeyboardEvent('keydown', { key: 'Enter' }) ) @@ -208,61 +225,127 @@ describe('GlobalSearchComponent', () => { { rule_type: FILTER_HAS_CORRESPONDENT_ANY, value: object.id.toString() }, ]) + component.primaryAction('documentType', object) + expect(qfSpy).toHaveBeenCalledWith([ + { rule_type: FILTER_HAS_DOCUMENT_TYPE_ANY, value: object.id.toString() }, + ]) + + component.primaryAction('storagePath', object) + expect(qfSpy).toHaveBeenCalledWith([ + { rule_type: FILTER_HAS_STORAGE_PATH_ANY, value: object.id.toString() }, + ]) + + component.primaryAction('tag', object) + expect(qfSpy).toHaveBeenCalledWith([ + { rule_type: FILTER_HAS_ANY_TAG, value: object.id.toString() }, + ]) + component.primaryAction('user', object) expect(modalSpy).toHaveBeenCalledWith(UserEditDialogComponent, { size: 'lg', }) + + component.primaryAction('group', object) + expect(modalSpy).toHaveBeenCalledWith(GroupEditDialogComponent, { + size: 'lg', + }) + + component.primaryAction('mailAccount', object) + expect(modalSpy).toHaveBeenCalledWith(MailAccountEditDialogComponent, { + size: 'xl', + }) + + component.primaryAction('mailRule', object) + expect(modalSpy).toHaveBeenCalledWith(MailRuleEditDialogComponent, { + size: 'xl', + }) + + component.primaryAction('customField', object) + expect(modalSpy).toHaveBeenCalledWith(CustomFieldEditDialogComponent, { + size: 'md', + }) + + component.primaryAction('workflow', object) + expect(modalSpy).toHaveBeenCalledWith(WorkflowEditDialogComponent, { + size: 'xl', + }) }) it('should perform secondary action', () => { const doc = searchResults.documents[0] - const routerSpy = jest.spyOn(router, 'navigate') + const openSpy = jest.spyOn(window, 'open') component.secondaryAction('document', doc) - expect(routerSpy).toHaveBeenCalledWith( - [documentService.getDownloadUrl(doc.id)], - { skipLocationChange: true } - ) + expect(openSpy).toHaveBeenCalledWith(documentService.getDownloadUrl(doc.id)) const correspondent = searchResults.correspondents[0] const modalSpy = jest.spyOn(modalService, 'open') component.secondaryAction('correspondent', correspondent) expect(modalSpy).toHaveBeenCalledWith(CorrespondentEditDialogComponent, { - size: 'lg', + size: 'md', + }) + + component.secondaryAction('documentType', searchResults.document_types[0]) + expect(modalSpy).toHaveBeenCalledWith(CorrespondentEditDialogComponent, { + size: 'md', + }) + + component.secondaryAction('storagePath', searchResults.storage_paths[0]) + expect(modalSpy).toHaveBeenCalledWith(CorrespondentEditDialogComponent, { + size: 'md', + }) + + component.secondaryAction('tag', searchResults.tags[0]) + expect(modalSpy).toHaveBeenCalledWith(CorrespondentEditDialogComponent, { + size: 'md', }) }) - // it('should reset', () => { - // jest.spyOn(component.queryDebounce, 'next'); - // jest.spyOn(component.resultsDropdown, 'close'); - // component.reset(); - // expect(component.queryDebounce.next).toHaveBeenCalledWith(''); - // expect(component.searchResults).toBeNull(); - // expect(component['currentItemIndex']).toBeUndefined(); - // expect(component.resultsDropdown.close).toHaveBeenCalled(); - // }); + it('should reset', () => { + const debounce = jest.spyOn(component.queryDebounce, 'next') + const closeSpy = jest.spyOn(component.resultsDropdown, 'close') + component['reset'](true) + expect(debounce).toHaveBeenCalledWith(null) + expect(component.searchResults).toBeNull() + expect(component['currentItemIndex']).toBe(-1) + expect(closeSpy).toHaveBeenCalled() + }) - // it('should set current item', () => { - // jest.spyOn(component.resultItems.get(0).nativeElement, 'focus'); - // component.currentItemIndex = 0; - // component.setCurrentItem(); - // expect(component.resultItems.get(0).nativeElement.focus).toHaveBeenCalled(); - // }); + it('should set current item', () => { + component.searchResults = searchResults as any + fixture.detectChanges() + const focusSpy = jest.spyOn( + component.resultItems.get(0).nativeElement, + 'focus' + ) + component['currentItemIndex'] = 0 + component['setCurrentItem']() + expect(focusSpy).toHaveBeenCalled() + }) - // it('should handle search input keydown', () => { - // jest.spyOn(component.resultItems.first.nativeElement, 'click'); - // component.searchResults = { total: 1 }; - // component.resultsDropdown = { isOpen: () => true }; - // component.searchInputKeyDown(new KeyboardEvent('keydown', { key: 'ArrowDown' })); - // expect(component.currentItemIndex).toBe(0); - // expect(component.resultItems.first.nativeElement.focus).toHaveBeenCalled(); + it('should handle search input keydown', () => { + component.searchResults = searchResults as any + component.resultsDropdown.open() + fixture.detectChanges() + component.searchInputKeyDown( + new KeyboardEvent('keydown', { key: 'ArrowDown' }) + ) + expect(component['currentItemIndex']).toBe(0) - // component.searchInputKeyDown(new KeyboardEvent('keydown', { key: 'Enter' })); - // expect(component.resultItems.first.nativeElement.click).toHaveBeenCalled(); - // }); + component.searchResults = { total: 1 } as any + const primaryActionSpy = jest.spyOn(component, 'primaryAction') + component.searchInputKeyDown(new KeyboardEvent('keydown', { key: 'Enter' })) + expect(primaryActionSpy).toHaveBeenCalled() - // it('should handle dropdown open change', () => { - // jest.spyOn(component, 'reset'); - // component.onDropdownOpenChange(false); - // expect(component.reset).toHaveBeenCalled(); - // }); + const resetSpy = jest.spyOn(GlobalSearchComponent.prototype as any, 'reset') + component.searchInputKeyDown( + new KeyboardEvent('keydown', { key: 'Escape' }) + ) + expect(resetSpy).toHaveBeenCalled() + }) + + it('should reset on dropdown close', () => { + const resetSpy = jest.spyOn(GlobalSearchComponent.prototype as any, 'reset') + component.onDropdownOpenChange(false) + expect(resetSpy).toHaveBeenCalled() + }) }) 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 5354a4f67..f08987997 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 @@ -43,7 +43,7 @@ export class GlobalSearchComponent { public query: string public queryDebounce: Subject public searchResults: GlobalSearchResult - private currentItemIndex: number + private currentItemIndex: number = -1 @ViewChild('searchInput') searchInput: ElementRef @ViewChild('resultsDropdown') resultsDropdown: NgbDropdown @@ -67,9 +67,16 @@ export class GlobalSearchComponent { this.currentItemIndex-- this.setCurrentItem() event.preventDefault() + } else { + this.searchInput.nativeElement.focus() + this.currentItemIndex = -1 } - } else if (event.key === 'Enter') { + } else if ( + event.key === 'Enter' && + document.activeElement !== this.searchInput.nativeElement + ) { this.resultItems.get(this.currentItemIndex).nativeElement.click() + event.preventDefault() } } } @@ -87,11 +94,11 @@ export class GlobalSearchComponent { .pipe( debounceTime(400), distinctUntilChanged(), - filter((query) => !query.length || query.length > 2) + filter((query) => !query?.length || query?.length > 2) ) .subscribe((text) => { this.query = text - this.search(text) + if (text) this.search(text) }) } @@ -106,6 +113,7 @@ export class GlobalSearchComponent { this.reset(true) let filterRuleType: number let editDialogComponent: any + let size: string = 'md' switch (type) { case 'document': this.router.navigate(['/documents', object.id]) @@ -124,21 +132,26 @@ export class GlobalSearchComponent { break case 'user': editDialogComponent = UserEditDialogComponent + size = 'lg' break case 'group': editDialogComponent = GroupEditDialogComponent + size = 'lg' break case 'mailAccount': editDialogComponent = MailAccountEditDialogComponent + size = 'xl' break case 'mailRule': editDialogComponent = MailRuleEditDialogComponent + size = 'xl' break case 'customField': editDialogComponent = CustomFieldEditDialogComponent break case 'workflow': editDialogComponent = WorkflowEditDialogComponent + size = 'xl' break } @@ -149,7 +162,7 @@ export class GlobalSearchComponent { } else if (editDialogComponent) { const modalRef: NgbModalRef = this.modalService.open( editDialogComponent, - { size: 'lg' } + { size } ) modalRef.componentInstance.dialogMode = EditDialogMode.EDIT modalRef.componentInstance.object = object @@ -159,11 +172,10 @@ export class GlobalSearchComponent { public secondaryAction(type: string, object: ObjectWithId) { this.reset(true) let editDialogComponent: any + let size: string = 'md' switch (type) { case 'document': - this.router.navigate([this.documentService.getDownloadUrl(object.id)], { - skipLocationChange: true, - }) + window.open(this.documentService.getDownloadUrl(object.id)) break case 'correspondent': editDialogComponent = CorrespondentEditDialogComponent @@ -182,7 +194,7 @@ export class GlobalSearchComponent { if (editDialogComponent) { const modalRef: NgbModalRef = this.modalService.open( editDialogComponent, - { size: 'lg' } + { size } ) modalRef.componentInstance.dialogMode = EditDialogMode.EDIT modalRef.componentInstance.object = object @@ -190,9 +202,9 @@ export class GlobalSearchComponent { } private reset(close: boolean = false) { - this.queryDebounce.next('') + this.queryDebounce.next(null) this.searchResults = null - this.currentItemIndex = undefined + this.currentItemIndex = -1 if (close) { this.resultsDropdown.close() } @@ -217,6 +229,8 @@ export class GlobalSearchComponent { this.resultsDropdown.isOpen() ) { this.resultItems.first.nativeElement.click() + } else if (event.key === 'Escape' && !this.resultsDropdown.isOpen()) { + this.reset(true) } }