Enhancement: global search tweaks (#6674)

This commit is contained in:
shamoon 2024-05-13 09:12:02 -07:00 committed by GitHub
parent 7983487430
commit a1e4365ff2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 130 additions and 82 deletions

View File

@ -1013,7 +1013,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context>
<context context-type="linenumber">92</context> <context context-type="linenumber">93</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1926290004382723170" datatype="html"> <trans-unit id="1926290004382723170" datatype="html">
@ -2113,11 +2113,11 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">51</context> <context context-type="linenumber">55</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">68</context> <context context-type="linenumber">72</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/input/permissions/permissions-form/permissions-form.component.html</context> <context context-type="sourcefile">src/app/components/common/input/permissions/permissions-form/permissions-form.component.html</context>
@ -2676,7 +2676,7 @@
<source>Advanced search</source> <source>Advanced search</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">19</context> <context context-type="linenumber">23</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
@ -2687,25 +2687,25 @@
<source>Open</source> <source>Open</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">45</context> <context context-type="linenumber">49</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">48</context> <context context-type="linenumber">52</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6329940072345709724" datatype="html"> <trans-unit id="6329940072345709724" datatype="html">
<source>Filter documents</source> <source>Filter documents</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">54</context> <context context-type="linenumber">58</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3099741642167775297" datatype="html"> <trans-unit id="3099741642167775297" datatype="html">
<source>Download</source> <source>Download</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">65</context> <context context-type="linenumber">69</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html</context> <context context-type="sourcefile">src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html</context>
@ -2732,113 +2732,113 @@
<source>No results</source> <source>No results</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">79</context> <context context-type="linenumber">83</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="searchResults.documents" datatype="html"> <trans-unit id="searchResults.documents" datatype="html">
<source>Documents</source> <source>Documents</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">82</context> <context context-type="linenumber">86</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="searchResults.saved_views" datatype="html"> <trans-unit id="searchResults.saved_views" datatype="html">
<source>Saved Views</source> <source>Saved Views</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">88</context> <context context-type="linenumber">92</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="searchResults.tags" datatype="html"> <trans-unit id="searchResults.tags" datatype="html">
<source>Tags</source> <source>Tags</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">95</context> <context context-type="linenumber">99</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="searchResults.correspondents" datatype="html"> <trans-unit id="searchResults.correspondents" datatype="html">
<source>Correspondents</source> <source>Correspondents</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">102</context> <context context-type="linenumber">106</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="searchResults.documentTypes" datatype="html"> <trans-unit id="searchResults.documentTypes" datatype="html">
<source>Document types</source> <source>Document types</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">109</context> <context context-type="linenumber">113</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="searchResults.storagePaths" datatype="html"> <trans-unit id="searchResults.storagePaths" datatype="html">
<source>Storage paths</source> <source>Storage paths</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">116</context> <context context-type="linenumber">120</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="searchResults.users" datatype="html"> <trans-unit id="searchResults.users" datatype="html">
<source>Users</source> <source>Users</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">123</context> <context context-type="linenumber">127</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="searchResults.groups" datatype="html"> <trans-unit id="searchResults.groups" datatype="html">
<source>Groups</source> <source>Groups</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">130</context> <context context-type="linenumber">134</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="searchResults.customFields" datatype="html"> <trans-unit id="searchResults.customFields" datatype="html">
<source>Custom fields</source> <source>Custom fields</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">137</context> <context context-type="linenumber">141</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="searchResults.mailAccounts" datatype="html"> <trans-unit id="searchResults.mailAccounts" datatype="html">
<source>Mail accounts</source> <source>Mail accounts</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">144</context> <context context-type="linenumber">148</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="searchResults.mailRules" datatype="html"> <trans-unit id="searchResults.mailRules" datatype="html">
<source>Mail rules</source> <source>Mail rules</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">151</context> <context context-type="linenumber">155</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="searchResults.workflows" datatype="html"> <trans-unit id="searchResults.workflows" datatype="html">
<source>Workflows</source> <source>Workflows</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">158</context> <context context-type="linenumber">162</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="83507137894716798" datatype="html"> <trans-unit id="83507137894716798" datatype="html">
<source>Successfully updated object.</source> <source>Successfully updated object.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context>
<context context-type="linenumber">168</context> <context context-type="linenumber">175</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context>
<context context-type="linenumber">206</context> <context context-type="linenumber">213</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1801333259018423190" datatype="html"> <trans-unit id="1801333259018423190" datatype="html">
<source>Error occurred saving object.</source> <source>Error occurred saving object.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context>
<context context-type="linenumber">171</context> <context context-type="linenumber">178</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context>
<context context-type="linenumber">209</context> <context context-type="linenumber">216</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8700121026680200191" datatype="html"> <trans-unit id="8700121026680200191" datatype="html">

View File

@ -6,15 +6,19 @@
<div class="form-control form-control-sm"> <div class="form-control form-control-sm">
<input class="bg-transparent border-0 w-100 h-100" #searchInput type="text" name="query" <input class="bg-transparent border-0 w-100 h-100" #searchInput type="text" name="query"
placeholder="Search" aria-label="Search" i18n-placeholder placeholder="Search" aria-label="Search" i18n-placeholder
autocomplete="off" spellcheck="false" autocomplete="off"
[(ngModel)]="query" (ngModelChange)="this.queryDebounce.next($event)" (keydown)="searchInputKeyDown($event)"> spellcheck="false"
[(ngModel)]="query"
(ngModelChange)="this.queryDebounce.next($event)"
(keydown)="searchInputKeyDown($event)"
ngbDropdownAnchor>
<div class="position-absolute top-50 end-0 translate-middle"> <div class="position-absolute top-50 end-0 translate-middle">
@if (loading) { @if (loading) {
<div class="spinner-border spinner-border-sm text-muted mt-1"></div> <div class="spinner-border spinner-border-sm text-muted mt-1"></div>
} }
</div> </div>
</div> </div>
@if (query && (searchResults?.documents.length === searchService.searchResultObjectLimit || searchService.searchDbOnly)) { @if (query) {
<button class="btn btn-sm btn-outline-secondary" type="button" (click)="runAdvanedSearch()"> <button class="btn btn-sm btn-outline-secondary" type="button" (click)="runAdvanedSearch()">
<ng-container i18n>Advanced search</ng-container> <ng-container i18n>Advanced search</ng-container>
<i-bs width="1em" height="1em" name="arrow-right-short"></i-bs> <i-bs width="1em" height="1em" name="arrow-right-short"></i-bs>
@ -25,7 +29,7 @@
<ng-template #resultItemTemplate let-item="item" let-nameProp="nameProp" let-type="type" let-icon="icon" let-date="date"> <ng-template #resultItemTemplate let-item="item" let-nameProp="nameProp" let-type="type" let-icon="icon" let-date="date">
<div #resultItem ngbDropdownItem class="py-2 d-flex align-items-center focus-ring border-0 cursor-pointer" tabindex="-1" <div #resultItem ngbDropdownItem class="py-2 d-flex align-items-center focus-ring border-0 cursor-pointer" tabindex="-1"
(click)="primaryAction(type, item)" (click)="primaryAction(type, item, $event)"
(mouseenter)="onItemHover($event)"> (mouseenter)="onItemHover($event)">
<i-bs width="1.2em" height="1.2em" name="{{icon}}" class="me-2 text-muted"></i-bs> <i-bs width="1.2em" height="1.2em" name="{{icon}}" class="me-2 text-muted"></i-bs>
<div class="text-truncate"> <div class="text-truncate">
@ -36,7 +40,7 @@
</div> </div>
<div class="btn-group ms-auto"> <div class="btn-group ms-auto">
<button #primaryButton type="button" class="btn btn-sm btn-outline-primary d-flex" <button #primaryButton type="button" class="btn btn-sm btn-outline-primary d-flex"
(click)="primaryAction(type, item); $event.stopImmediatePropagation()" (click)="primaryAction(type, item, $event); $event.stopImmediatePropagation()"
(keydown)="onButtonKeyDown($event)" (keydown)="onButtonKeyDown($event)"
[disabled]="disablePrimaryButton(type, item)" [disabled]="disablePrimaryButton(type, item)"
(mouseenter)="onButtonHover($event)"> (mouseenter)="onButtonHover($event)">
@ -56,7 +60,7 @@
</button> </button>
@if (type !== DataType.SavedView && type !== DataType.Workflow && type !== DataType.CustomField && type !== DataType.Group && type !== DataType.User && type !== DataType.MailAccount && type !== DataType.MailRule) { @if (type !== DataType.SavedView && type !== DataType.Workflow && type !== DataType.CustomField && type !== DataType.Group && type !== DataType.User && type !== DataType.MailAccount && type !== DataType.MailRule) {
<button #secondaryButton type="button" class="btn btn-sm btn-outline-primary d-flex" <button #secondaryButton type="button" class="btn btn-sm btn-outline-primary d-flex"
(click)="secondaryAction(type, item); $event.stopImmediatePropagation()" (click)="secondaryAction(type, item, $event); $event.stopImmediatePropagation()"
(keydown)="onButtonKeyDown($event)" (keydown)="onButtonKeyDown($event)"
[disabled]="disableSecondaryButton(type, item)" [disabled]="disableSecondaryButton(type, item)"
(mouseenter)="onButtonHover($event)"> (mouseenter)="onButtonHover($event)">

View File

@ -36,6 +36,7 @@ import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-e
import { ElementRef } from '@angular/core' import { ElementRef } from '@angular/core'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { DataType } from 'src/app/data/datatype' import { DataType } from 'src/app/data/datatype'
import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
const searchResults = { const searchResults = {
total: 11, total: 11,
@ -248,10 +249,7 @@ describe('GlobalSearchComponent', () => {
expect(blurSpy).toHaveBeenCalled() expect(blurSpy).toHaveBeenCalled()
component.searchResults = { total: 1 } as any component.searchResults = { total: 1 } as any
component.resultsDropdown.close() component.resultsDropdown.open()
const openSpy = jest.spyOn(component.resultsDropdown, 'open')
component.searchInputKeyDown(new KeyboardEvent('keydown', { key: 'Enter' }))
expect(openSpy).toHaveBeenCalled()
component.searchInputKeyDown( component.searchInputKeyDown(
new KeyboardEvent('keydown', { key: 'ArrowDown' }) new KeyboardEvent('keydown', { key: 'ArrowDown' })
@ -260,6 +258,13 @@ describe('GlobalSearchComponent', () => {
const closeSpy = jest.spyOn(component.resultsDropdown, 'close') const closeSpy = jest.spyOn(component.resultsDropdown, 'close')
component.dropdownKeyDown(new KeyboardEvent('keydown', { key: 'Escape' })) component.dropdownKeyDown(new KeyboardEvent('keydown', { key: 'Escape' }))
expect(closeSpy).toHaveBeenCalled() 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(() => { it('should search on query debounce', fakeAsync(() => {
@ -276,7 +281,6 @@ describe('GlobalSearchComponent', () => {
it('should support primary action', () => { it('should support primary action', () => {
const object = { id: 1 } const object = { id: 1 }
const routerSpy = jest.spyOn(router, 'navigate') const routerSpy = jest.spyOn(router, 'navigate')
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
const modalSpy = jest.spyOn(modalService, 'open') const modalSpy = jest.spyOn(modalService, 'open')
let modal: NgbModalRef let modal: NgbModalRef
@ -289,23 +293,41 @@ describe('GlobalSearchComponent', () => {
expect(routerSpy).toHaveBeenCalledWith(['/view', object.id]) expect(routerSpy).toHaveBeenCalledWith(['/view', object.id])
component.primaryAction(DataType.Correspondent, object) component.primaryAction(DataType.Correspondent, object)
expect(qfSpy).toHaveBeenCalledWith([ expect(routerSpy).toHaveBeenCalledWith([
{ rule_type: FILTER_HAS_CORRESPONDENT_ANY, value: object.id.toString() }, '/documents',
queryParamsFromFilterRules([
{
rule_type: FILTER_HAS_CORRESPONDENT_ANY,
value: object.id.toString(),
},
]),
]) ])
component.primaryAction(DataType.DocumentType, object) component.primaryAction(DataType.DocumentType, object)
expect(qfSpy).toHaveBeenCalledWith([ expect(routerSpy).toHaveBeenCalledWith([
{ rule_type: FILTER_HAS_DOCUMENT_TYPE_ANY, value: object.id.toString() }, '/documents',
queryParamsFromFilterRules([
{
rule_type: FILTER_HAS_DOCUMENT_TYPE_ANY,
value: object.id.toString(),
},
]),
]) ])
component.primaryAction(DataType.StoragePath, object) component.primaryAction(DataType.StoragePath, object)
expect(qfSpy).toHaveBeenCalledWith([ expect(routerSpy).toHaveBeenCalledWith([
{ rule_type: FILTER_HAS_STORAGE_PATH_ANY, value: object.id.toString() }, '/documents',
queryParamsFromFilterRules([
{ rule_type: FILTER_HAS_STORAGE_PATH_ANY, value: object.id.toString() },
]),
]) ])
component.primaryAction(DataType.Tag, object) component.primaryAction(DataType.Tag, object)
expect(qfSpy).toHaveBeenCalledWith([ expect(routerSpy).toHaveBeenCalledWith([
{ rule_type: FILTER_HAS_TAGS_ANY, value: object.id.toString() }, '/documents',
queryParamsFromFilterRules([
{ rule_type: FILTER_HAS_TAGS_ANY, value: object.id.toString() },
]),
]) ])
component.primaryAction(DataType.User, object) component.primaryAction(DataType.User, object)
@ -450,13 +472,6 @@ describe('GlobalSearchComponent', () => {
expect(focusSpy).toHaveBeenCalled() 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', () => { it('should support explicit advanced search', () => {
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter') const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
component.query = 'test' component.query = 'test'
@ -465,4 +480,25 @@ describe('GlobalSearchComponent', () => {
{ rule_type: FILTER_FULLTEXT_QUERY, value: 'test' }, { 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
})
}) })

View File

@ -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 { 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 { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component'
import { HotKeyService } from 'src/app/services/hot-key.service' import { HotKeyService } from 'src/app/services/hot-key.service'
import { queryParamsFromFilterRules } from 'src/app/utils/query-params'
@Component({ @Component({
selector: 'pngx-global-search', selector: 'pngx-global-search',
@ -87,7 +88,7 @@ export class GlobalSearchComponent implements OnInit {
}) })
} }
ngOnInit() { public ngOnInit() {
this.hotkeyService this.hotkeyService
.addShortcut({ keys: '/', description: $localize`Global search` }) .addShortcut({ keys: '/', description: $localize`Global search` })
.subscribe(() => { .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) this.reset(true)
let filterRuleType: number let filterRuleType: number
let editDialogComponent: any let editDialogComponent: any
let size: string = 'md' let size: string = 'md'
switch (type) { switch (type) {
case DataType.Document: case DataType.Document:
this.router.navigate(['/documents', object.id]) this.navigateOrOpenInNewWindow(['/documents', object.id], newWindow)
return return
case DataType.SavedView: case DataType.SavedView:
this.router.navigate(['/view', object.id]) this.navigateOrOpenInNewWindow(['/view', object.id], newWindow)
return return
case DataType.Correspondent: case DataType.Correspondent:
filterRuleType = FILTER_HAS_CORRESPONDENT_ANY filterRuleType = FILTER_HAS_CORRESPONDENT_ANY
@ -154,9 +160,10 @@ export class GlobalSearchComponent implements OnInit {
} }
if (filterRuleType) { if (filterRuleType) {
this.documentListViewService.quickFilter([ let params = queryParamsFromFilterRules([
{ rule_type: filterRuleType, value: object.id.toString() }, { rule_type: filterRuleType, value: object.id.toString() },
]) ])
this.navigateOrOpenInNewWindow(['/documents', params], newWindow)
} else if (editDialogComponent) { } else if (editDialogComponent) {
const modalRef: NgbModalRef = this.modalService.open( const modalRef: NgbModalRef = this.modalService.open(
editDialogComponent, editDialogComponent,
@ -213,6 +220,7 @@ export class GlobalSearchComponent implements OnInit {
private reset(close: boolean = false) { private reset(close: boolean = false) {
this.queryDebounce.next(null) this.queryDebounce.next(null)
this.query = null
this.searchResults = null this.searchResults = null
this.currentItemIndex = -1 this.currentItemIndex = -1
if (close) { if (close) {
@ -233,7 +241,7 @@ export class GlobalSearchComponent implements OnInit {
item.nativeElement.focus() item.nativeElement.focus()
} }
onItemHover(event: MouseEvent) { public onItemHover(event: MouseEvent) {
const item: ElementRef = this.resultItems const item: ElementRef = this.resultItems
.toArray() .toArray()
.find((item) => item.nativeElement === event.currentTarget) .find((item) => item.nativeElement === event.currentTarget)
@ -241,7 +249,7 @@ export class GlobalSearchComponent implements OnInit {
this.setCurrentItem() this.setCurrentItem()
} }
onButtonHover(event: MouseEvent) { public onButtonHover(event: MouseEvent) {
;(event.currentTarget as HTMLElement).focus() ;(event.currentTarget as HTMLElement).focus()
} }
@ -262,19 +270,14 @@ export class GlobalSearchComponent implements OnInit {
event.preventDefault() event.preventDefault()
this.currentItemIndex = this.searchResults.total - 1 this.currentItemIndex = this.searchResults.total - 1
this.setCurrentItem() this.setCurrentItem()
} else if ( } else if (event.key === 'Enter') {
event.key === 'Enter' && if (this.searchResults?.total === 1 && this.resultsDropdown.isOpen()) {
this.searchResults?.total === 1 && this.primaryButtons.first.nativeElement.click()
this.resultsDropdown.isOpen() this.searchInput.nativeElement.blur()
) { } else if (this.query?.length) {
this.primaryButtons.first.nativeElement.click() this.runAdvanedSearch()
this.searchInput.nativeElement.blur() this.reset(true)
} else if ( }
event.key === 'Enter' &&
this.searchResults?.total &&
!this.resultsDropdown.isOpen()
) {
this.resultsDropdown.open()
} else if (event.key === 'Escape' && !this.resultsDropdown.isOpen()) { } else if (event.key === 'Escape' && !this.resultsDropdown.isOpen()) {
if (this.query?.length) { if (this.query?.length) {
this.reset(true) this.reset(true)
@ -284,7 +287,7 @@ export class GlobalSearchComponent implements OnInit {
} }
} }
dropdownKeyDown(event: KeyboardEvent) { public dropdownKeyDown(event: KeyboardEvent) {
if ( if (
this.searchResults?.total && this.searchResults?.total &&
this.resultsDropdown.isOpen() && this.resultsDropdown.isOpen() &&
@ -327,14 +330,9 @@ export class GlobalSearchComponent implements OnInit {
} }
} }
onButtonKeyDown(event: KeyboardEvent) { public onButtonKeyDown(event: KeyboardEvent) {
// prevents ngBootstrap issue with keydown events if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
if ( event.target.dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
!['ArrowDown', 'ArrowUp', 'ArrowRight', 'ArrowLeft', 'Escape'].includes(
event.key
)
) {
event.stopImmediatePropagation()
} }
} }
@ -373,10 +371,19 @@ export class GlobalSearchComponent implements OnInit {
) )
} }
runAdvanedSearch() { public runAdvanedSearch() {
this.documentListViewService.quickFilter([ this.documentListViewService.quickFilter([
{ rule_type: FILTER_FULLTEXT_QUERY, value: this.query }, { rule_type: FILTER_FULLTEXT_QUERY, value: this.query },
]) ])
this.reset(true) 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)
}
}
} }

View File

@ -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 // accommodate larger font size on mobile
padding-top: .15rem; padding-top: .15rem;
padding-bottom: .15rem; padding-bottom: .15rem;