Merge pull request #4841 from GabrielInTheWorld/globalSearch
Refactores the 'global search'
This commit is contained in:
commit
e3f3108f8c
@ -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,
|
||||||
|
@ -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>
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user