Global search UI improvements, coverage
This commit is contained in:
parent
4c976bf070
commit
eaecac0407
@ -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', {
|
||||
|
@ -8,11 +8,11 @@
|
||||
</form>
|
||||
|
||||
<ng-template #resultItemTemplate let-item="item" let-nameProp="nameProp" let-type="type" let-icon="icon">
|
||||
<div #resultItem ngbDropdownItem class="py-2 d-flex align-items-center focus-ring border-0" (click)="primaryAction(type, item)">
|
||||
<i-bs width="1em" height="1em" name="{{icon}}" class="me-2"></i-bs>
|
||||
<div #resultItem ngbDropdownItem class="py-2 d-flex align-items-center focus-ring border-0 cursor-pointer" (click)="primaryAction(type, item)">
|
||||
<i-bs width="1.2em" height="1.2em" name="{{icon}}" class="me-2 text-muted"></i-bs>
|
||||
<span>{{item[nameProp]}}</span>
|
||||
<div class="btn-group ms-auto">
|
||||
<button class="btn btn-sm btn-outline-primary" (click)="primaryAction(type, item); $event.stopPropagation()">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="primaryAction(type, item); $event.stopPropagation()">
|
||||
@if (type === 'document' || type === 'workflow' || type === 'customField' || type === 'group' || type === 'user') {
|
||||
<i-bs width="1em" height="1em" name="pencil"></i-bs>
|
||||
} @else {
|
||||
@ -20,7 +20,7 @@
|
||||
}
|
||||
</button>
|
||||
@if (type !== 'workflow' && type !== 'customField' && type !== 'group' && type !== 'user') {
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="secondaryAction(type, item); $event.stopPropagation()">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" (click)="secondaryAction(type, item); $event.stopPropagation()">
|
||||
@if (type === 'document') {
|
||||
<i-bs width="1em" height="1em" name="download"></i-bs>
|
||||
} @else {
|
||||
@ -32,7 +32,7 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<div ngbDropdownMenu class="col-6">
|
||||
<div ngbDropdownMenu class="col-6 mh-75 overflow-y-scroll">
|
||||
@if (searchResults?.total === 0) {
|
||||
<h6 class="dropdown-header" i18n="@@searchResults.noResults">No results</h6>
|
||||
} @else {
|
||||
|
@ -31,3 +31,11 @@ form {
|
||||
* {
|
||||
--pngx-focus-alpha: 0;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mh-75 {
|
||||
max-height: 75vh;
|
||||
}
|
||||
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
@ -43,7 +43,7 @@ export class GlobalSearchComponent {
|
||||
public query: string
|
||||
public queryDebounce: Subject<string>
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user