Refactores the 'global search'
This commit is contained in:
parent
1c3d60fe39
commit
5e922f66d2
@ -136,9 +136,11 @@ export class SearchService {
|
||||
*
|
||||
* @param query The search query
|
||||
* @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();
|
||||
return this.searchModels
|
||||
.filter(s => inCollectionStrings.includes(s.collectionString))
|
||||
@ -148,7 +150,12 @@ export class SearchService {
|
||||
.map(x => x as (BaseViewModel & Searchable))
|
||||
.filter(model => model.formatForSearch().some(text => text.toLowerCase().includes(query)))
|
||||
.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 {
|
||||
collectionString: searchModel.collectionString,
|
||||
|
@ -1,6 +1,7 @@
|
||||
<os-head-bar [nav]="false" [goBack]="true">
|
||||
<!-- Title -->
|
||||
<div class="title-slot"><h2 translate>Search results</h2></div>
|
||||
|
||||
<!-- Menu -->
|
||||
<div class="menu-slot">
|
||||
<button type="button" mat-icon-button [matMenuTriggerFor]="menu"><mat-icon>more_vert</mat-icon></button>
|
||||
@ -8,51 +9,64 @@
|
||||
</os-head-bar>
|
||||
|
||||
<!-- search-field -->
|
||||
<div class="search-field">
|
||||
<form [formGroup]="quickSearchform">
|
||||
<mat-form-field>
|
||||
<input matInput osAutofocus formControlName="query" (keyup)="quickSearch()" />
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<div class="search-container">
|
||||
<div class="search-field">
|
||||
<mat-form-field appearance="outline" class="search-component">
|
||||
<mat-label>{{ 'Search' | translate }}</mat-label>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<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 {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
max-width: 50%;
|
||||
margin: 15px auto;
|
||||
|
||||
form {
|
||||
width: 80%;
|
||||
margin: 8px auto;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
mat-form-field.search-component {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.noSearchResults {
|
||||
text-align: center;
|
||||
@media screen and (max-width: 400px) {
|
||||
.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 {
|
||||
|
@ -1,16 +1,14 @@
|
||||
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 { MatSnackBar } from '@angular/material/snack-bar';
|
||||
|
||||
import { Subject } from 'rxjs';
|
||||
import { auditTime, debounceTime } from 'rxjs/operators';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { DataStoreService } from 'app/core/core-services/data-store.service';
|
||||
import { SearchService, SearchModel, SearchResult } from 'app/core/ui-services/search.service';
|
||||
import { BaseViewComponent } from '../../../base/base-view';
|
||||
import { FormControl } from '@angular/forms';
|
||||
|
||||
type SearchModelEnabled = SearchModel & { enabled: boolean };
|
||||
|
||||
@ -24,14 +22,14 @@ type SearchModelEnabled = SearchModel & { enabled: boolean };
|
||||
})
|
||||
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.
|
||||
@ -41,7 +39,7 @@ export class SearchComponent extends BaseViewComponent implements OnInit {
|
||||
/**
|
||||
* The search results for the ui
|
||||
*/
|
||||
public searchResults: SearchResult[];
|
||||
public searchResults: SearchResult[] = [];
|
||||
|
||||
/**
|
||||
* 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)[];
|
||||
|
||||
/**
|
||||
* 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
|
||||
@ -62,8 +65,6 @@ export class SearchComponent extends BaseViewComponent implements OnInit {
|
||||
* @param translate
|
||||
* @param matSnackBar
|
||||
* @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
|
||||
*/
|
||||
public constructor(
|
||||
@ -71,17 +72,17 @@ export class SearchComponent extends BaseViewComponent implements OnInit {
|
||||
translate: TranslateService,
|
||||
matSnackBar: MatSnackBar,
|
||||
private DS: DataStoreService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private router: Router,
|
||||
private searchService: SearchService
|
||||
) {
|
||||
super(title, translate, matSnackBar);
|
||||
this.quickSearchform = new FormGroup({ query: new FormControl([]) });
|
||||
|
||||
this.registeredModels = this.searchService.getRegisteredModels().map(rm => ({ ...rm, enabled: true }));
|
||||
|
||||
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 {
|
||||
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.
|
||||
*
|
||||
* @param query optional, if given, `this.query` will be set to this value
|
||||
*/
|
||||
public search(query?: string): void {
|
||||
if (query) {
|
||||
this.query = query;
|
||||
public search(): void {
|
||||
if (!this.query || this.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
|
||||
*/
|
||||
public toggleModel(registeredModel: SearchModelEnabled): void {
|
||||
public toggleModel(event: MouseEvent, registeredModel: SearchModelEnabled): void {
|
||||
event.stopPropagation();
|
||||
registeredModel.enabled = !registeredModel.enabled;
|
||||
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