Merge pull request #4841 from GabrielInTheWorld/globalSearch

Refactores the 'global search'
This commit is contained in:
Sean 2019-07-19 15:24:54 +02:00 committed by GitHub
commit e3f3108f8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 146 additions and 111 deletions

View File

@ -136,9 +136,11 @@ export class SearchService {
* *
* @param query The search query * @param query The search query
* @param inCollectionStrings All connection strings which should be used for searching. * @param inCollectionStrings All connection strings which should be used for searching.
* @returns All search results sorted by the model's title (via `getTItle()`). * @param sortingProperty Sorting by `id` or `title`.
*
* @returns All search results sorted by the model's title (via `getTitle()`).
*/ */
public search(query: string, inCollectionStrings: string[]): SearchResult[] { public search(query: string, inCollectionStrings: string[], sortingProperty: 'id' | 'title'): SearchResult[] {
query = query.toLowerCase(); query = query.toLowerCase();
return this.searchModels return this.searchModels
.filter(s => inCollectionStrings.includes(s.collectionString)) .filter(s => inCollectionStrings.includes(s.collectionString))
@ -148,7 +150,12 @@ export class SearchService {
.map(x => x as (BaseViewModel & Searchable)) .map(x => x as (BaseViewModel & Searchable))
.filter(model => model.formatForSearch().some(text => text.toLowerCase().includes(query))) .filter(model => model.formatForSearch().some(text => text.toLowerCase().includes(query)))
.sort((a, b) => { .sort((a, b) => {
return this.languageCollator.compare(a.getTitle(), b.getTitle()); switch (sortingProperty) {
case 'id':
return a.id - b.id;
case 'title':
return this.languageCollator.compare(a.getTitle(), b.getTitle());
}
}); });
return { return {
collectionString: searchModel.collectionString, collectionString: searchModel.collectionString,

View File

@ -1,6 +1,7 @@
<os-head-bar [nav]="false" [goBack]="true"> <os-head-bar [nav]="false" [goBack]="true">
<!-- Title --> <!-- Title -->
<div class="title-slot"><h2 translate>Search results</h2></div> <div class="title-slot"><h2 translate>Search results</h2></div>
<!-- Menu --> <!-- Menu -->
<div class="menu-slot"> <div class="menu-slot">
<button type="button" mat-icon-button [matMenuTriggerFor]="menu"><mat-icon>more_vert</mat-icon></button> <button type="button" mat-icon-button [matMenuTriggerFor]="menu"><mat-icon>more_vert</mat-icon></button>
@ -8,51 +9,64 @@
</os-head-bar> </os-head-bar>
<!-- search-field --> <!-- search-field -->
<div class="search-field"> <div class="search-container">
<form [formGroup]="quickSearchform"> <div class="search-field">
<mat-form-field> <mat-form-field appearance="outline" class="search-component">
<input matInput osAutofocus formControlName="query" (keyup)="quickSearch()" /> <mat-label>{{ 'Search' | translate }}</mat-label>
<mat-icon matSuffix>search</mat-icon> <input matInput [formControl]="searchForm" />
<mat-icon matPrefix>search</mat-icon>
<button *ngIf="searchForm.value !== ''" mat-icon-button matSuffix (click)="searchForm.setValue('')">
<mat-icon>close</mat-icon>
</button>
</mat-form-field> </mat-form-field>
</form> </div>
<mat-menu #menu="matMenu">
<button mat-menu-item *ngFor="let registeredModel of registeredModels" (click)="toggleModel($event, registeredModel)">
<mat-checkbox [checked]="registeredModel.enabled" (click)="$event.preventDefault()">
<span>{{ registeredModel.verboseNamePlural | translate }}</span>
</mat-checkbox>
</button>
<mat-divider></mat-divider>
<h4 matSubheader>{{ 'Sort' | translate }}</h4>
<mat-list-item *ngFor="let option of sortingOptionsList" (click)="toggleSorting($event, option.option)">
<button mat-menu-item>
<mat-icon matListIcon>{{ sortingProperty === option.option ? 'check' : '' }}</mat-icon>
<span translate>{{ option.label }}</span>
</button>
</mat-list-item>
</mat-menu>
<div *ngIf="searchResults.length > 0">
<div class="os-card">
{{ searchResultCount }}
<span *ngIf="searchResultCount === 1" translate>result</span>
<span *ngIf="searchResultCount !== 1" translate>results</span>
</div>
<div class="os-card">
<div class="noSearchResults" *ngIf="searchResultCount === 0 && query !== ''">
<span translate>No search result found for</span> "{{ query }}"
</div>
<ng-container *ngFor="let searchResult of searchResults">
<mat-card *ngIf="searchResult.models.length > 0">
<mat-card-title>
{{ searchResult.models.length }} {{ searchResult.verboseName | translate }}
</mat-card-title>
<mat-card-content>
<mat-list>
<mat-list-item *ngFor="let model of searchResult.models">
<a *ngIf="!searchResult.openInNewTab" [routerLink]="model.getDetailStateURL()">
{{ model.getTitle() }}
</a>
<a *ngIf="searchResult.openInNewTab" [routerLink]="model.getDetailStateURL()" target="_blank" >
{{ model.getTitle() }}
</a>
</mat-list-item>
</mat-list>
</mat-card-content>
</mat-card>
</ng-container>
</div>
</div>
</div> </div>
<mat-menu #menu="matMenu">
<button mat-menu-item *ngFor="let registeredModel of registeredModels" (click)="toggleModel(registeredModel)">
<mat-checkbox [checked]="registeredModel.enabled" (click)="$event.preventDefault()">
<span>{{ registeredModel.verboseNamePlural | translate }}</span>
</mat-checkbox>
</button>
</mat-menu>
<div class="noSearchResults" *ngIf="searchResultCount === 0">
<span translate>No search result found for</span> "{{ query }}"
</div>
<ng-container *ngIf="searchResultCount > 0">
<h3>
{{ searchResultCount }}
<span *ngIf="searchResultCount === 1" translate>result</span>
<span *ngIf="searchResultCount > 1" translate>results</span>
</h3>
<ng-container *ngFor="let searchResult of searchResults">
<mat-card *ngIf="searchResult.models.length > 0">
<mat-card-title>
{{ searchResult.models.length }} {{ searchResult.verboseName | translate }}
</mat-card-title>
<mat-card-content>
<mat-list>
<mat-list-item *ngFor="let model of searchResult.models">
<a *ngIf="!searchResult.openInNewTab" [routerLink]="model.getDetailStateURL()">
{{ model.getTitle() }}
</a>
<a *ngIf="searchResult.openInNewTab" [routerLink]="model.getDetailStateURL()" target="_blank" >
{{ model.getTitle() }}
</a>
</mat-list-item>
</mat-list>
</mat-card-content>
</mat-card>
</ng-container>
</ng-container>

View File

@ -1,18 +1,40 @@
// Variables
$border: 1px solid rgba(0, 0, 0, 0.125);
// Definitions
.search-field { .search-field {
width: 100%; display: flex;
max-width: 50%;
margin: 15px auto;
form { mat-form-field.search-component {
width: 80%;
margin: 8px auto;
}
mat-form-field {
width: 100%; width: 100%;
} }
} }
.noSearchResults { @media screen and (max-width: 400px) {
text-align: center; .search-container {
padding: 0 8px;
}
.search-field {
display: block;
mat-form-field.search-sort {
width: 100%;
margin: 0;
}
}
}
mat-list-item {
height: auto !important;
&:not(:first-child) {
margin-top: 8px;
}
&:not(:last-child) {
margin-bottom: 8px;
}
} }
mat-card { mat-card {

View File

@ -1,16 +1,14 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { FormGroup, FormControl } from '@angular/forms';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { Subject } from 'rxjs';
import { auditTime, debounceTime } from 'rxjs/operators'; import { auditTime, debounceTime } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DataStoreService } from 'app/core/core-services/data-store.service'; import { DataStoreService } from 'app/core/core-services/data-store.service';
import { SearchService, SearchModel, SearchResult } from 'app/core/ui-services/search.service'; import { SearchService, SearchModel, SearchResult } from 'app/core/ui-services/search.service';
import { BaseViewComponent } from '../../../base/base-view'; import { BaseViewComponent } from '../../../base/base-view';
import { FormControl } from '@angular/forms';
type SearchModelEnabled = SearchModel & { enabled: boolean }; type SearchModelEnabled = SearchModel & { enabled: boolean };
@ -24,14 +22,14 @@ type SearchModelEnabled = SearchModel & { enabled: boolean };
}) })
export class SearchComponent extends BaseViewComponent implements OnInit { export class SearchComponent extends BaseViewComponent implements OnInit {
/** /**
* the search term * List with all options for sorting by.
*/ */
public query: string; public sortingOptionsList = [{ option: 'title', label: 'Title' }, { option: 'id', label: 'ID' }];
/** /**
* Holds the typed search query. * the search term
*/ */
public quickSearchform: FormGroup; public query = '';
/** /**
* The amout of search results. * The amout of search results.
@ -41,7 +39,7 @@ export class SearchComponent extends BaseViewComponent implements OnInit {
/** /**
* The search results for the ui * The search results for the ui
*/ */
public searchResults: SearchResult[]; public searchResults: SearchResult[] = [];
/** /**
* A list of models, that are registered to be searched. Used for * A list of models, that are registered to be searched. Used for
@ -50,9 +48,14 @@ export class SearchComponent extends BaseViewComponent implements OnInit {
public registeredModels: (SearchModelEnabled)[]; public registeredModels: (SearchModelEnabled)[];
/** /**
* This subject is used for the quicksearch input. It is used to debounce the input. * Property to decide what to sort by.
*/ */
private quickSearchSubject = new Subject<string>(); public sortingProperty: 'id' | 'title' = 'title';
/**
* Form-control for the input-field.
*/
public searchForm = new FormControl('');
/** /**
* Inits the quickSearchForm, gets the registered models from the search service * Inits the quickSearchForm, gets the registered models from the search service
@ -62,8 +65,6 @@ export class SearchComponent extends BaseViewComponent implements OnInit {
* @param translate * @param translate
* @param matSnackBar * @param matSnackBar
* @param DS DataStorService * @param DS DataStorService
* @param activatedRoute determine the search term from the URL
* @param router To change the query in the url
* @param searchService For searching in the models * @param searchService For searching in the models
*/ */
public constructor( public constructor(
@ -71,17 +72,17 @@ export class SearchComponent extends BaseViewComponent implements OnInit {
translate: TranslateService, translate: TranslateService,
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
private DS: DataStoreService, private DS: DataStoreService,
private activatedRoute: ActivatedRoute,
private router: Router,
private searchService: SearchService private searchService: SearchService
) { ) {
super(title, translate, matSnackBar); super(title, translate, matSnackBar);
this.quickSearchform = new FormGroup({ query: new FormControl([]) });
this.registeredModels = this.searchService.getRegisteredModels().map(rm => ({ ...rm, enabled: true })); this.registeredModels = this.searchService.getRegisteredModels().map(rm => ({ ...rm, enabled: true }));
this.DS.modifiedObservable.pipe(auditTime(100)).subscribe(() => this.search()); this.DS.modifiedObservable.pipe(auditTime(100)).subscribe(() => this.search());
this.quickSearchSubject.pipe(debounceTime(250)).subscribe(query => this.search(query)); this.searchForm.valueChanges.pipe(debounceTime(250)).subscribe(query => {
this.query = query;
this.search();
});
} }
/** /**
@ -89,48 +90,26 @@ export class SearchComponent extends BaseViewComponent implements OnInit {
*/ */
public ngOnInit(): void { public ngOnInit(): void {
super.setTitle('Search'); super.setTitle('Search');
this.query = this.activatedRoute.snapshot.queryParams.query;
this.quickSearchform.get('query').setValue(this.query);
this.search();
} }
/** /**
* Searches for the query in `this.query` or the query given. * Searches for the query in `this.query` or the query given.
*
* @param query optional, if given, `this.query` will be set to this value
*/ */
public search(query?: string): void { public search(): void {
if (query) { if (!this.query || this.query === '') {
this.query = query; this.searchResults = [];
} else {
// Just search for enabled models.
const collectionStrings = this.registeredModels.filter(rm => rm.enabled).map(rm => rm.collectionString);
// Get all results
this.searchResults = this.searchService.search(this.query, collectionStrings, this.sortingProperty);
// Because the results are per model, we need to accumulate the total number of all search results.
this.searchResultCount = this.searchResults
.map(sr => sr.models.length)
.reduce((acc, current) => acc + current, 0);
} }
if (!this.query) {
return;
}
// Just search for enabled models.
const collectionStrings = this.registeredModels.filter(rm => rm.enabled).map(rm => rm.collectionString);
// Get all results
this.searchResults = this.searchService.search(this.query, collectionStrings);
// Because the results are per model, we need to accumulate the total number of all search results.
this.searchResultCount = this.searchResults
.map(sr => sr.models.length)
.reduce((acc, current) => acc + current, 0);
// Update the URL.
this.router.navigate([], {
relativeTo: this.activatedRoute,
queryParams: { query: this.query },
replaceUrl: true
});
}
/**
* Handler for the quick search input. Emits the typed value to the `quickSearchSubject`.
*/
public quickSearch(): void {
this.quickSearchSubject.next(this.quickSearchform.get('query').value);
} }
/** /**
@ -138,8 +117,21 @@ export class SearchComponent extends BaseViewComponent implements OnInit {
* *
* @param registeredModel The model to toggle * @param registeredModel The model to toggle
*/ */
public toggleModel(registeredModel: SearchModelEnabled): void { public toggleModel(event: MouseEvent, registeredModel: SearchModelEnabled): void {
event.stopPropagation();
registeredModel.enabled = !registeredModel.enabled; registeredModel.enabled = !registeredModel.enabled;
this.search(); this.search();
} }
/**
* Function to switch between sorting-options.
*
* @param event The `MouseEvent`
* @param option The sorting-option
*/
public toggleSorting(event: MouseEvent, option: 'id' | 'title'): void {
event.stopPropagation();
this.sortingProperty = option;
this.search();
}
} }