Improves the global search to find IDs
- Considers the 'id' of objects for search.
This commit is contained in:
parent
fffbf95a63
commit
1d5b1b4fed
@ -192,16 +192,16 @@ export class SearchService {
|
||||
*
|
||||
* @param query The search query
|
||||
* @param inCollectionStrings All connection strings which should be used for searching.
|
||||
* @param sortingProperty Sorting by `id` or `title`.
|
||||
* @param dedicatedId Optional parameter. Useful to look for a specific id in the given collectionStrings.
|
||||
* @param searchOnlyById Optional parameter. Decides, whether all models should only be filtered by their id.
|
||||
*
|
||||
* @returns All search results sorted by the model's title (via `getTitle()`).
|
||||
*/
|
||||
public search(
|
||||
query: string,
|
||||
inCollectionStrings: string[],
|
||||
sortingProperty: 'title' | 'id' = 'title',
|
||||
dedicatedId?: number
|
||||
dedicatedId?: number,
|
||||
searchOnlyById: boolean = false
|
||||
): SearchResult[] {
|
||||
query = query.toLowerCase();
|
||||
return this.searchModels
|
||||
@ -211,20 +211,15 @@ export class SearchService {
|
||||
.getAll(searchModel.collectionString)
|
||||
.map(x => x as (BaseViewModel & Searchable))
|
||||
.filter(model =>
|
||||
dedicatedId
|
||||
? model.id === dedicatedId
|
||||
: model
|
||||
!searchOnlyById
|
||||
? model.id === dedicatedId ||
|
||||
model
|
||||
.formatForSearch()
|
||||
.searchValue.some(text => text && text.toLowerCase().indexOf(query) !== -1)
|
||||
: model.id === dedicatedId
|
||||
)
|
||||
.sort((a, b) => {
|
||||
switch (sortingProperty) {
|
||||
case 'id':
|
||||
return a.id - b.id;
|
||||
case 'title':
|
||||
return this.languageCollator.compare(a.getTitle(), b.getTitle());
|
||||
}
|
||||
});
|
||||
.sort((a, b) => this.languageCollator.compare(a.getTitle(), b.getTitle()));
|
||||
|
||||
return {
|
||||
collectionString: searchModel.collectionString,
|
||||
verboseName: results.length === 1 ? searchModel.verboseNameSingular : searchModel.verboseNamePlural,
|
||||
|
@ -1,7 +1,6 @@
|
||||
.filter-menu-content-wrapper {
|
||||
overflow-y: scroll;
|
||||
height: 100%;
|
||||
|
||||
div.indent {
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
@ -38,6 +38,7 @@
|
||||
|
||||
<!-- Search bar -->
|
||||
<os-rounded-input
|
||||
#searchField
|
||||
[model]="searchFieldInput"
|
||||
[size]="'small'"
|
||||
[fullWidth]="false"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, Input, Output, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { Component, EventEmitter, HostListener, Input, Output, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { MatBottomSheet } from '@angular/material/bottom-sheet';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
@ -9,6 +9,7 @@ import { BaseSortListService } from 'app/core/ui-services/base-sort-list.service
|
||||
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||
import { BaseViewModel } from 'app/site/base/base-view-model';
|
||||
import { FilterMenuComponent } from './filter-menu/filter-menu.component';
|
||||
import { RoundedInputComponent } from '../rounded-input/rounded-input.component';
|
||||
import { SortBottomSheetComponent } from './sort-bottom-sheet/sort-bottom-sheet.component';
|
||||
|
||||
/**
|
||||
@ -32,6 +33,9 @@ import { SortBottomSheetComponent } from './sort-bottom-sheet/sort-bottom-sheet.
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class SortFilterBarComponent<V extends BaseViewModel> {
|
||||
@ViewChild('searchField', { static: true })
|
||||
public searchField: RoundedInputComponent;
|
||||
|
||||
/**
|
||||
* The currently active sorting service for the list view
|
||||
*/
|
||||
@ -224,4 +228,12 @@ export class SortFilterBarComponent<V extends BaseViewModel> {
|
||||
const itemProperty = option.property as string;
|
||||
return itemProperty.charAt(0).toUpperCase() + itemProperty.slice(1);
|
||||
}
|
||||
|
||||
@HostListener('document:keydown', ['$event']) public onKeyDown(event: KeyboardEvent): void {
|
||||
if (event.ctrlKey && event.key === 'f') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.searchField.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,18 +9,23 @@
|
||||
></os-rounded-input>
|
||||
<button mat-icon-button [matMenuTriggerFor]="filterMenu"><mat-icon>filter_list</mat-icon></button>
|
||||
<mat-menu #filterMenu="matMenu">
|
||||
<button mat-menu-item (click)="setSearchStringForID()">
|
||||
<mat-icon>{{ !!searchStringForID ? 'checked' : '' }}</mat-icon>
|
||||
ID
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button
|
||||
mat-menu-item
|
||||
*ngFor="let model of registeredModels"
|
||||
(click)="setCollection(model.verboseNamePlural)"
|
||||
>
|
||||
<mat-icon>{{ model.verboseNamePlural === searchCollection ? 'checked' : '' }}</mat-icon>
|
||||
<mat-icon>{{ model.collectionString === specificCollectionString ? 'checked' : '' }}</mat-icon>
|
||||
{{ model.verboseNamePlural | translate }}
|
||||
</button>
|
||||
</mat-menu>
|
||||
</div>
|
||||
<h4 *ngIf="searchResultCount > 0" class="result-count">
|
||||
{{ searchResultCount }} {{ searchResultCount === 1 ? ( 'result' | translate ) : ( 'results' | translate ) }}
|
||||
{{ searchResultCount }} {{ searchResultCount === 1 ? ('result' | translate) : ('results' | translate) }}
|
||||
</h4>
|
||||
<mat-divider></mat-divider>
|
||||
<div class="result-view" *ngIf="searchResults.length > 0">
|
||||
@ -37,7 +42,9 @@
|
||||
<mat-basic-chip
|
||||
class="lightblue filter-count"
|
||||
disableRipple
|
||||
matTooltip="{{ result.models.length === 1 ? ( 'result' | translate ) : ( 'results' | translate ) }}"
|
||||
matTooltip="{{
|
||||
result.models.length === 1 ? ('result' | translate) : ('results' | translate)
|
||||
}}"
|
||||
>{{ result.models.length }}</mat-basic-chip
|
||||
>
|
||||
</mat-panel-title>
|
||||
@ -84,7 +91,9 @@
|
||||
</mat-accordion>
|
||||
<div class="no-results" *ngIf="!selectedModel && searchString.length > 0">
|
||||
<span translate>No search result found</span>
|
||||
<span *ngIf="searchCollection"> ({{ 'with filter' | translate }} "{{ searchCollection | translate }}")</span>.
|
||||
<span *ngIf="searchCollection"
|
||||
> ({{ 'with filter' | translate }} "{{ searchCollection | translate }}")</span
|
||||
>.
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider [vertical]="true"></mat-divider>
|
||||
|
@ -36,8 +36,20 @@ export class SuperSearchComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* Holds the collection-string of the specific collection.
|
||||
*
|
||||
* Is set, if the user has entered a collection.
|
||||
*/
|
||||
private specificCollectionString: string = null;
|
||||
public specificCollectionString: string = null;
|
||||
|
||||
/**
|
||||
* Holds the input text the user entered to search for a specific id.
|
||||
*/
|
||||
public searchStringForID: string = null;
|
||||
|
||||
/**
|
||||
* The specific id the user searches for.
|
||||
*/
|
||||
private specificID: number = null;
|
||||
|
||||
/**
|
||||
* The results for the given query.
|
||||
@ -119,11 +131,11 @@ export class SuperSearchComponent implements OnInit {
|
||||
this.collectionStrings = this.registeredModels.map(rm => rm.collectionString);
|
||||
this.translatedCollectionStrings = this.searchService.getTranslatedCollectionStrings();
|
||||
|
||||
this.searchForm.valueChanges.pipe(debounceTime(250)).subscribe(value => {
|
||||
this.searchForm.valueChanges.pipe(debounceTime(250)).subscribe((value: string) => {
|
||||
if (value.trim() === '') {
|
||||
this.clearResults();
|
||||
} else {
|
||||
this.specificCollectionString = this.searchSpecificCollection(value.trim());
|
||||
this.prepareForSearch(value.trim());
|
||||
}
|
||||
this.search();
|
||||
});
|
||||
@ -136,22 +148,11 @@ export class SuperSearchComponent implements OnInit {
|
||||
*/
|
||||
private search(): void {
|
||||
if (this.searchString !== '') {
|
||||
// Local variable to check, if the user searches for a specific id.
|
||||
let dedicatedId: number;
|
||||
|
||||
const query = this.searchString;
|
||||
// Looks, if the query matches variations of 'nr.' followed by at least one digit.
|
||||
// If so, the user searches for a specific id in some collections.
|
||||
// Everything not case-sensitive.
|
||||
if (query.match(/n\w*r\.?\:?\s*\d+/gi)) {
|
||||
// If so, this expression finds the number.
|
||||
dedicatedId = +query.match(/\d+/g);
|
||||
}
|
||||
this.searchResults = this.searchService.search(
|
||||
query,
|
||||
this.searchString,
|
||||
this.specificCollectionString ? [this.specificCollectionString] : this.collectionStrings,
|
||||
'title',
|
||||
dedicatedId
|
||||
this.specificID,
|
||||
!!this.searchStringForID
|
||||
);
|
||||
this.selectFirstResult();
|
||||
} else {
|
||||
@ -162,33 +163,71 @@ export class SuperSearchComponent implements OnInit {
|
||||
.reduce((acc, current) => acc + current, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to check several things.
|
||||
*
|
||||
* First the query is splitted and the first part is tested
|
||||
* for a specific collection.
|
||||
*
|
||||
* Second the next part is tested for a specific id.
|
||||
* It's looking for the word `id` or any kind of `nr.`
|
||||
* and a number.
|
||||
*
|
||||
* @param query The user's input, he searches for.
|
||||
*/
|
||||
private prepareForSearch(query: string): void {
|
||||
// The query is splitted by the first whitespace or the first ':'.
|
||||
const splittedQuery = query.split(/\s*(?::|\s+)\s*/);
|
||||
|
||||
this.specificCollectionString = this.searchSpecificCollection(splittedQuery[0]);
|
||||
if (this.specificCollectionString) {
|
||||
this.searchCollection = splittedQuery.shift();
|
||||
}
|
||||
|
||||
this.searchStringForID = this.searchSpecificId(splittedQuery[0]) ? splittedQuery.shift() : null;
|
||||
|
||||
// This test, whether the query includes an number --> Then get this number.
|
||||
if (/\b\d+\b/g.test(splittedQuery[0])) {
|
||||
this.specificID = +query.match(/\d+/g);
|
||||
}
|
||||
|
||||
// The rest will be joined to one string.
|
||||
this.searchString = splittedQuery.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* This function test, if the query matches some of the `collectionStrings`.
|
||||
*
|
||||
* That indicates, that the user looks for items in a specific collection.
|
||||
*
|
||||
* @returns { { collection: string, query: string[] } | null } Either an object containing the found collection and the query
|
||||
* @returns { string | null } Either an object containing the found collection and the query
|
||||
* or null, if there exists none.
|
||||
*/
|
||||
private searchSpecificCollection(query: string): string | null {
|
||||
// The query is splitted by the first whitespace or the first ':'.
|
||||
const splittedQuery = query.split(/\s*(?::|\s+)\s*/);
|
||||
const nextCollection = this.translatedCollectionStrings.find(item =>
|
||||
// The value of the item should match the query plus any further
|
||||
// characters (useful for splitted words in the query).
|
||||
// This will look, if the user searches in a specific collection.
|
||||
// Flag 'i' tells, that cases are ignored.
|
||||
new RegExp(item.value, 'i').test(splittedQuery[0])
|
||||
// new RegExp(item.value, 'i').test(splittedQuery[0])
|
||||
new RegExp(item.value, 'i').test(query)
|
||||
);
|
||||
if (!!nextCollection) {
|
||||
this.searchString = splittedQuery.slice(1).join(' ');
|
||||
this.searchCollection = splittedQuery[0];
|
||||
return nextCollection.collection;
|
||||
} else {
|
||||
this.searchString = query;
|
||||
this.searchCollection = '';
|
||||
return null;
|
||||
}
|
||||
return !!nextCollection ? nextCollection.collection : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to see, whether a string matches the word `id` or any kind of `nr`.
|
||||
*
|
||||
* @param query The query, which is tested for the word `id` or `nr`.
|
||||
*
|
||||
* @returns {boolean} If the given string matches any kind of the test-string.
|
||||
*/
|
||||
private searchSpecificId(query: string = ''): boolean {
|
||||
// Looks, if the query matches variations of 'nr.' or 'id'
|
||||
// If so, the user searches for a specific id in some collections.
|
||||
// Everything not case-sensitive.
|
||||
return !!(query.match(/\bn\w*r\.?\:?\b/gi) || query.match(/\bid\.?\:?\b/gi));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -215,6 +254,19 @@ export class SuperSearchComponent implements OnInit {
|
||||
private selectNextResult(up: boolean): void {
|
||||
const tmp = this.searchResults.flatMap((result: SearchResult) => result.models);
|
||||
this.changeModel(tmp[(tmp.indexOf(this.selectedModel) + (up ? -1 : 1)).modulo(tmp.length)]);
|
||||
|
||||
this.scrollToSelected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to scroll with the current selected model, if the user uses the keyboard to navigate.
|
||||
*/
|
||||
private scrollToSelected(): void {
|
||||
const selectedElement = document.getElementsByClassName('selected')[0];
|
||||
selectedElement.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -223,12 +275,27 @@ export class SuperSearchComponent implements OnInit {
|
||||
* @param collectionName The `verboseName` of the selected collection.
|
||||
*/
|
||||
public setCollection(collectionName: string): void {
|
||||
this.searchCollection = this.searchCollection === collectionName ? '' : collectionName;
|
||||
if (this.searchCollection !== '') {
|
||||
this.searchForm.setValue(this.searchCollection + ': ' + this.searchString);
|
||||
} else {
|
||||
this.searchForm.setValue(this.searchString);
|
||||
}
|
||||
this.searchCollection =
|
||||
this.searchCollection.toLowerCase() === collectionName.toLowerCase() ? '' : collectionName;
|
||||
this.setSearch();
|
||||
}
|
||||
|
||||
/**
|
||||
* This function sets the string for id or clears the variable, if already existing.
|
||||
*/
|
||||
public setSearchStringForID(): void {
|
||||
this.searchStringForID = !!this.searchStringForID ? null : 'id';
|
||||
this.setSearch();
|
||||
}
|
||||
|
||||
/**
|
||||
* This function puts the various strings together.
|
||||
*/
|
||||
private setSearch(): void {
|
||||
this.searchForm.setValue(
|
||||
[this.searchCollection, this.searchStringForID].map(value => (value ? value + ': ' : '')).join('') +
|
||||
this.searchString
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -274,6 +341,7 @@ export class SuperSearchComponent implements OnInit {
|
||||
this.selectedModel = null;
|
||||
this.searchCollection = '';
|
||||
this.searchString = '';
|
||||
this.searchStringForID = null;
|
||||
this.saveQueryToStorage(null);
|
||||
}
|
||||
|
||||
@ -304,6 +372,10 @@ export class SuperSearchComponent implements OnInit {
|
||||
* @param event KeyboardEvent to listen to keyboard-inputs.
|
||||
*/
|
||||
@HostListener('document:keydown', ['$event']) public onKeyNavigation(event: KeyboardEvent): void {
|
||||
if (event.ctrlKey && event.key === 'f') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
if (!!this.selectedModel) {
|
||||
if (event.key === 'Enter') {
|
||||
this.viewResult(this.selectedModel);
|
||||
|
@ -114,7 +114,7 @@ export class ViewCategory extends BaseViewModel<Category> implements CategoryTit
|
||||
}
|
||||
|
||||
public getDetailStateURL(): string {
|
||||
return '/motions/category';
|
||||
return `motions/category/${this.id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -315,6 +315,8 @@ export class SiteComponent extends BaseComponent implements OnInit {
|
||||
*/
|
||||
@HostListener('document:keydown', ['$event']) public onKeyNavigation(event: KeyboardEvent): void {
|
||||
if (event.altKey && event.shiftKey && event.code === 'KeyF') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.overlayService.showSearch();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user