1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2024-11-28 08:58:49 +02:00

Implementing adv. search query builder #58

This commit is contained in:
Patrik J. Braun 2021-01-31 12:22:56 +01:00
parent de9c58fd90
commit 88015cc33e
24 changed files with 603 additions and 210 deletions

View File

@ -4,7 +4,6 @@ import {DiskMangerWorker} from '../src/backend/model/threading/DiskMangerWorker'
import {IndexingManager} from '../src/backend/model/database/sql/IndexingManager';
import * as path from 'path';
import * as fs from 'fs';
import {SearchTypes} from '../src/common/entities/AutoCompleteItem';
import {Utils} from '../src/common/Utils';
import {DirectoryDTO} from '../src/common/entities/DirectoryDTO';
import {ServerConfig} from '../src/common/config/private/PrivateConfig';
@ -21,6 +20,7 @@ import {GalleryRouter} from '../src/backend/routes/GalleryRouter';
import {Express} from 'express';
import {PersonRouter} from '../src/backend/routes/PersonRouter';
import {QueryParams} from '../src/common/QueryParams';
import {SearchQueryTypes, TextSearch} from '../src/common/entities/SearchQueryDTO';
export interface BenchmarkResult {
@ -48,10 +48,6 @@ class BMGalleryRouter extends GalleryRouter {
GalleryRouter.addSearch(app);
}
public static addInstantSearch(app: Express) {
GalleryRouter.addInstantSearch(app);
}
public static addAutoComplete(app: Express) {
GalleryRouter.addAutoComplete(app);
}
@ -128,16 +124,15 @@ export class BenchmarkRunner {
return await bm.run(this.RUNS);
}
async bmAllSearch(text: string): Promise<{ result: BenchmarkResult, searchType: SearchTypes }[]> {
async bmAllSearch(text: string): Promise<{ result: BenchmarkResult, searchType: SearchQueryTypes }[]> {
await this.setupDB();
const types = Utils.enumToArray(SearchTypes).map(a => a.key).concat([null]);
const results: { result: BenchmarkResult, searchType: SearchTypes }[] = [];
const types = Utils.enumToArray(SearchQueryTypes).map(a => a.key).concat([null]);
const results: { result: BenchmarkResult, searchType: SearchQueryTypes }[] = [];
for (let i = 0; i < types.length; i++) {
const req = Utils.clone(this.requestTemplate);
req.params.text = text;
req.query[QueryParams.gallery.search.type] = types[i];
const bm = new Benchmark('Searching for `' + text + '` as `' + (types[i] ? SearchTypes[types[i]] : 'any') + '`', req);
req.query[QueryParams.gallery.search.query] = <TextSearch>{type: types[i], text: text};
const bm = new Benchmark('Searching for `' + text + '` as `' + (types[i] ? SearchQueryTypes[types[i]] : 'any') + '`', req);
BMGalleryRouter.addSearch(bm.BmExpressApp);
results.push({result: await bm.run(this.RUNS), searchType: types[i]});
@ -145,14 +140,6 @@ export class BenchmarkRunner {
return results;
}
async bmInstantSearch(text: string): Promise<BenchmarkResult> {
await this.setupDB();
const req = Utils.clone(this.requestTemplate);
req.params.text = text;
const bm = new Benchmark('Instant search for `' + text + '`', req);
BMGalleryRouter.addInstantSearch(bm.BmExpressApp);
return await bm.run(this.RUNS);
}
async bmAutocomplete(text: string): Promise<BenchmarkResult> {
await this.setupDB();

View File

@ -71,7 +71,6 @@ const run = async () => {
printResult(await bm.bmListDirectory());
printResult(await bm.bmListPersons());
(await bm.bmAllSearch('a')).forEach(res => printResult(res.result));
printResult(await bm.bmInstantSearch('a'));
printResult(await bm.bmAutocomplete('a'));
printLine('*Measurements run ' + RUNS + ' times and an average was calculated.');
console.log(resultsText);

View File

@ -162,7 +162,7 @@ export class GalleryMWs {
return next();
}
const query: SearchQueryDTO = <any>req.query[QueryParams.gallery.search.type];
const query: SearchQueryDTO = <any>req.query[QueryParams.gallery.search.query];
try {
const result = await ObjectManagers.getInstance().SearchManager.search(query);
@ -203,7 +203,7 @@ export class GalleryMWs {
return next();
}
try {
const query: SearchQueryDTO = <any>req.query[QueryParams.gallery.search.type];
const query: SearchQueryDTO = <any>req.query[QueryParams.gallery.search.query];
const photo = await ObjectManagers.getInstance()
.SearchManager.getRandomPhoto(query);

View File

@ -2,8 +2,13 @@ import {AutoCompleteItem} from '../../../../common/entities/AutoCompleteItem';
import {ISearchManager} from '../interfaces/ISearchManager';
import {SearchResultDTO} from '../../../../common/entities/SearchResultDTO';
import {SearchQueryDTO, SearchQueryTypes} from '../../../../common/entities/SearchQueryDTO';
import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
export class SearchManager implements ISearchManager {
getRandomPhoto(queryFilter: SearchQueryDTO): Promise<PhotoDTO> {
throw new Error('Method not implemented.');
}
autocomplete(text: string, type: SearchQueryTypes): Promise<AutoCompleteItem[]> {
throw new Error('Method not implemented.');
}

View File

@ -22,7 +22,7 @@ import {
SearchQueryTypes,
SomeOfSearchQuery,
TextSearch,
TextSearchQueryTypes
TextSearchQueryMatchTypes
} from '../../../../common/entities/SearchQueryDTO';
import {GalleryManager} from './GalleryManager';
import {ObjectManagers} from '../../ObjectManagers';
@ -359,7 +359,7 @@ export class SearchManager implements ISearchManager {
return new Brackets((q: WhereExpression) => {
const createMatchString = (str: string) => {
return (<TextSearch>query).matchType === TextSearchQueryTypes.exact_match ? str : `%${str}%`;
return (<TextSearch>query).matchType === TextSearchQueryMatchTypes.exact_match ? str : `%${str}%`;
};
const LIKE = (<TextSearch>query).negate ? 'NOT LIKE' : 'LIKE';
@ -416,7 +416,7 @@ export class SearchManager implements ISearchManager {
// Matching for array type fields
const matchArrayField = (fieldName: string) => {
q[whereFN](new Brackets(qbr => {
if ((<TextSearch>query).matchType !== TextSearchQueryTypes.exact_match) {
if ((<TextSearch>query).matchType !== TextSearchQueryMatchTypes.exact_match) {
qbr[whereFN](`${fieldName} ${LIKE} :text${paramCounter.value} COLLATE utf8_general_ci`,
textParam);
} else {

View File

@ -16,7 +16,6 @@ export const QueryParams = {
photo: 'p',
sharingKey_query: 'sk',
sharingKey_params: 'sharingKey',
searchText: 'searchText',
directory: 'directory',
knownLastModified: 'klm',
knownLastScanned: 'kls'

View File

@ -12,15 +12,48 @@ export enum SearchQueryTypes {
// TEXT search types
any_text = 100,
person,
keyword,
position,
caption,
file_name,
directory,
file_name,
keyword,
person,
position,
}
export enum TextSearchQueryTypes {
export const ListSearchQueryTypes = [
SearchQueryTypes.AND,
SearchQueryTypes.OR,
SearchQueryTypes.SOME_OF,
];
export const TextSearchQueryTypes = [
SearchQueryTypes.any_text,
SearchQueryTypes.caption,
SearchQueryTypes.directory,
SearchQueryTypes.file_name,
SearchQueryTypes.keyword,
SearchQueryTypes.person,
SearchQueryTypes.position,
];
export const MetadataSearchQueryTypes = [
// non-text metadata
SearchQueryTypes.date,
SearchQueryTypes.rating,
SearchQueryTypes.distance,
SearchQueryTypes.resolution,
SearchQueryTypes.orientation,
// TEXT search types
SearchQueryTypes.any_text,
SearchQueryTypes.caption,
SearchQueryTypes.directory,
SearchQueryTypes.file_name,
SearchQueryTypes.keyword,
SearchQueryTypes.person,
SearchQueryTypes.position,
];
export enum TextSearchQueryMatchTypes {
exact_match = 1, like = 2
}
@ -104,7 +137,7 @@ export interface TextSearch extends NegatableSearchQuery {
SearchQueryTypes.caption |
SearchQueryTypes.file_name |
SearchQueryTypes.directory;
matchType: TextSearchQueryTypes;
matchType: TextSearchQueryMatchTypes;
text: string;
}

View File

@ -93,6 +93,7 @@ import {JobButtonComponent} from './ui/settings/jobs/button/job-button.settings.
import {ErrorInterceptor} from './model/network/helper/error.interceptor';
import {CSRFInterceptor} from './model/network/helper/csrf.interceptor';
import {SettingsEntryComponent} from './ui/settings/_abstract/settings-entry/settings-entry.component';
import {GallerySearchQueryEntryComponent} from './ui/gallery/search/query-enrty/query-entry.search.gallery.component';
@Injectable()
@ -174,6 +175,7 @@ export function translationsFactory(locale: string) {
GalleryMapLightboxComponent,
FrameComponent,
GallerySearchComponent,
GallerySearchQueryEntryComponent,
GalleryShareComponent,
GalleryNavigatorComponent,
GalleryPhotoComponent,

View File

@ -27,7 +27,7 @@ export function galleryMatcherFunction(
}
if (path === 'search') {
if (segments.length > 1) {
posParams[QueryParams.gallery.searchText] = segments[1];
posParams[QueryParams.gallery.search.query] = segments[1];
}
return {consumed: segments.slice(0, Math.min(segments.length, 2)), posParams};
}

View File

@ -1,4 +1,4 @@
<a [routerLink]="['/search', person.name, {type: SearchTypes[SearchTypes.person]}]"
<a [routerLink]="['/search', searchQuery]"
style="display: inline-block;">

View File

@ -1,12 +1,13 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {RouterLink} from '@angular/router';
import {PersonDTO} from '../../../../../common/entities/PersonDTO';
import {SearchTypes} from '../../../../../common/entities/AutoCompleteItem';
import {DomSanitizer} from '@angular/platform-browser';
import {PersonThumbnail, ThumbnailManagerService} from '../../gallery/thumbnailManager.service';
import {FacesService} from '../faces.service';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {Config} from '../../../../../common/config/public/Config';
import {SearchQueryTypes, TextSearch, TextSearchQueryMatchTypes} from '../../../../../common/entities/SearchQueryDTO';
import {QueryParams} from '../../../../../common/QueryParams';
@Component({
selector: 'app-face',
@ -19,7 +20,7 @@ export class FaceComponent implements OnInit, OnDestroy {
@Input() size: number;
thumbnail: PersonThumbnail = null;
SearchTypes = SearchTypes;
public searchQuery: any;
constructor(private thumbnailService: ThumbnailManagerService,
private _sanitizer: DomSanitizer,
@ -34,6 +35,12 @@ export class FaceComponent implements OnInit, OnDestroy {
ngOnInit() {
this.thumbnail = this.thumbnailService.getPersonThumbnail(this.person);
this.searchQuery = {};
this.searchQuery[QueryParams.gallery.search.query] = <TextSearch>{
type: SearchQueryTypes.person,
text: this.person.name,
matchType: TextSearchQueryMatchTypes.exact_match
};
}

View File

@ -2,11 +2,12 @@ import {Injectable} from '@angular/core';
import {DirectoryDTO} from '../../../../common/entities/DirectoryDTO';
import {Utils} from '../../../../common/Utils';
import {Config} from '../../../../common/config/public/Config';
import {AutoCompleteItem, SearchTypes} from '../../../../common/entities/AutoCompleteItem';
import {AutoCompleteItem} from '../../../../common/entities/AutoCompleteItem';
import {SearchResultDTO} from '../../../../common/entities/SearchResultDTO';
import {MediaDTO} from '../../../../common/entities/MediaDTO';
import {SortingMethods} from '../../../../common/entities/SortingMethods';
import {VersionService} from '../../model/version.service';
import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO';
interface CacheItem<T> {
timestamp: number;
@ -21,7 +22,6 @@ export class GalleryCacheService {
private static readonly INSTANT_SEARCH_PREFIX = 'instant_search:';
private static readonly SEARCH_PREFIX = 'search:';
private static readonly SORTING_PREFIX = 'sorting:';
private static readonly SEARCH_TYPE_PREFIX = ':type:';
private static readonly VERSION = 'version';
constructor(private versionService: VersionService) {
@ -40,7 +40,7 @@ export class GalleryCacheService {
const tmp = localStorage.getItem(key);
if (tmp != null) {
const value: CacheItem<SearchResultDTO> = JSON.parse(tmp);
if (value.timestamp < Date.now() - Config.Client.Search.instantSearchCacheTimeout) {
if (value.timestamp < Date.now() - Config.Client.Search.searchCacheTimeout) {
localStorage.removeItem(key);
return null;
}
@ -158,19 +158,15 @@ export class GalleryCacheService {
}
}
public getSearch(text: string, type?: SearchTypes): SearchResultDTO {
public getSearch(query: SearchQueryDTO): SearchResultDTO {
if (Config.Client.Other.enableCache === false) {
return null;
}
let key = GalleryCacheService.SEARCH_PREFIX + text;
if (typeof type !== 'undefined' && type !== null) {
key += GalleryCacheService.SEARCH_TYPE_PREFIX + type;
}
const key = GalleryCacheService.SEARCH_PREFIX + JSON.stringify(query);
return GalleryCacheService.loadCacheItem(key);
}
public setSearch(text: string, type: SearchTypes, searchResult: SearchResultDTO): void {
public setSearch(query: SearchQueryDTO, searchResult: SearchResultDTO): void {
if (Config.Client.Other.enableCache === false) {
return;
}
@ -178,10 +174,7 @@ export class GalleryCacheService {
timestamp: Date.now(),
item: searchResult
};
let key = GalleryCacheService.SEARCH_PREFIX + text;
if (typeof type !== 'undefined' && type !== null) {
key += GalleryCacheService.SEARCH_TYPE_PREFIX + type;
}
const key = GalleryCacheService.SEARCH_PREFIX + JSON.stringify(query);
try {
localStorage.setItem(key, JSON.stringify(tmp));
} catch (e) {

View File

@ -3,7 +3,6 @@ import {AuthenticationService} from '../../model/network/authentication.service'
import {ActivatedRoute, Params, Router} from '@angular/router';
import {GalleryService} from './gallery.service';
import {GalleryGridComponent} from './grid/grid.gallery.component';
import {SearchTypes} from '../../../../common/entities/AutoCompleteItem';
import {Config} from '../../../../common/config/public/Config';
import {DirectoryDTO} from '../../../../common/entities/DirectoryDTO';
import {SearchResultDTO} from '../../../../common/entities/SearchResultDTO';
@ -18,7 +17,7 @@ import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
import {QueryParams} from '../../../../common/QueryParams';
import {SeededRandomService} from '../../model/seededRandom.service';
import {take} from 'rxjs/operators';
import {FileDTO} from '../../../../common/entities/FileDTO';
import {SearchQueryTypes} from '../../../../common/entities/SearchQueryDTO';
@Component({
selector: 'app-gallery',
@ -37,7 +36,7 @@ export class GalleryComponent implements OnInit, OnDestroy {
public isPhotoWithLocation = false;
public countDown: { day: number, hour: number, minute: number, second: number } = null;
public readonly mapEnabled: boolean;
readonly SearchTypes: typeof SearchTypes;
readonly SearchTypes: typeof SearchQueryTypes;
private $counter: Observable<number>;
private subscription: { [key: string]: Subscription } = {
content: null,
@ -54,7 +53,7 @@ export class GalleryComponent implements OnInit, OnDestroy {
private _navigation: NavigationService,
private rndService: SeededRandomService) {
this.mapEnabled = Config.Client.Map.enabled;
this.SearchTypes = SearchTypes;
this.SearchTypes = SearchQueryTypes;
PageHelper.showScrollY();
}
@ -115,15 +114,10 @@ export class GalleryComponent implements OnInit, OnDestroy {
}
private onRoute = async (params: Params) => {
const searchText = params[QueryParams.gallery.searchText];
if (searchText && searchText !== '') {
const typeString: string = params[QueryParams.gallery.search.type];
let type: SearchTypes = null;
if (typeString && typeString !== '') {
type = <any>SearchTypes[<any>typeString];
}
const searchQuery = params[QueryParams.gallery.search.query];
if (searchQuery) {
this._galleryService.search(searchText, type).catch(console.error);
this._galleryService.search(searchQuery).catch(console.error);
return;
}

View File

@ -2,7 +2,6 @@ import {Injectable} from '@angular/core';
import {NetworkService} from '../../model/network/network.service';
import {ContentWrapper} from '../../../../common/entities/ConentWrapper';
import {DirectoryDTO} from '../../../../common/entities/DirectoryDTO';
import {SearchTypes} from '../../../../common/entities/AutoCompleteItem';
import {GalleryCacheService} from './cache.gallery.service';
import {BehaviorSubject} from 'rxjs';
import {Config} from '../../../../common/config/public/Config';
@ -11,6 +10,7 @@ import {NavigationService} from '../../model/navigation.service';
import {SortingMethods} from '../../../../common/entities/SortingMethods';
import {QueryParams} from '../../../../common/QueryParams';
import {PG2ConfMap} from '../../../../common/PG2ConfMap';
import {SearchQueryDTO} from '../../../../common/entities/SearchQueryDTO';
@Injectable()
@ -23,15 +23,7 @@ export class GalleryService {
};
private lastDirectory: DirectoryDTO;
private searchId: any;
private ongoingSearch: {
text: string,
type: SearchTypes
} = null;
private ongoingInstantSearch: {
text: string,
type: SearchTypes
} = null;
private runInstantSearchFor: string;
private ongoingSearch: SearchQueryDTO = null;
constructor(private networkService: NetworkService,
private galleryCacheService: GalleryCacheService,
@ -124,99 +116,35 @@ export class GalleryService {
}
}
public async search(text: string, type?: SearchTypes): Promise<void> {
public async search(query: SearchQueryDTO): Promise<void> {
if (this.searchId != null) {
clearTimeout(this.searchId);
}
if (text === null || text === '' || text.trim() === '.') {
return null;
}
this.ongoingSearch = {text: text, type: type};
this.ongoingSearch = query;
this.setContent(new ContentWrapper());
const cw = new ContentWrapper();
cw.searchResult = this.galleryCacheService.getSearch(text, type);
cw.searchResult = this.galleryCacheService.getSearch(query);
if (cw.searchResult == null) {
if (this.runInstantSearchFor === text && !type) {
await this.instantSearch(text, type);
return;
}
const params: { [key: string]: any } = {};
if (typeof type !== 'undefined' && type !== null) {
params[QueryParams.gallery.search.type] = type;
params[QueryParams.gallery.search.query] = query;
cw.searchResult = (await this.networkService.getJson<ContentWrapper>('/search', params)).searchResult;
this.galleryCacheService.setSearch(query, cw.searchResult);
}
cw.searchResult = (await this.networkService.getJson<ContentWrapper>('/search/' + text, params)).searchResult;
if (this.ongoingSearch &&
(this.ongoingSearch.text !== text || this.ongoingSearch.type !== type)) {
if (this.ongoingSearch !== query) {
return;
}
this.galleryCacheService.setSearch(text, type, cw.searchResult);
}
this.setContent(cw);
}
public async instantSearch(text: string, type?: SearchTypes): Promise<ContentWrapper> {
if (text === null || text === '' || text.trim() === '.') {
const content = new ContentWrapper(this.lastDirectory);
this.setContent(content);
if (this.searchId != null) {
clearTimeout(this.searchId);
}
if (!this.lastDirectory) {
this.loadDirectory('/').catch(console.error);
}
return null;
}
if (this.searchId != null) {
clearTimeout(this.searchId);
}
this.runInstantSearchFor = null;
this.ongoingInstantSearch = {text: text, type: type};
const cw = new ContentWrapper();
cw.directory = null;
cw.searchResult = this.galleryCacheService.getSearch(text);
if (cw.searchResult == null) {
// If result is not search cache, try to load more
this.searchId = setTimeout(() => {
this.search(text, type).catch(console.error);
this.searchId = null;
}, Config.Client.Search.InstantSearchTimeout);
cw.searchResult = this.galleryCacheService.getInstantSearch(text);
if (cw.searchResult == null) {
cw.searchResult = (await this.networkService.getJson<ContentWrapper>('/instant-search/' + text)).searchResult;
if (this.ongoingInstantSearch &&
(this.ongoingInstantSearch.text !== text || this.ongoingInstantSearch.type !== type)) {
return;
}
this.galleryCacheService.setInstantSearch(text, cw.searchResult);
}
}
this.setContent(cw);
// if instant search do not have a result, do not do a search
if (cw.searchResult.media.length === 0 && cw.searchResult.directories.length === 0) {
if (this.searchId != null) {
clearTimeout(this.searchId);
}
}
return cw;
}
isSearchResult(): boolean {
return !!this.content.value.searchResult;
}
runInstantSearch(searchText: string) {
this.runInstantSearchFor = searchText;
}
}

View File

@ -1,12 +1,12 @@
import {Component, ElementRef, Input, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {Dimension, IRenderable} from '../../../../model/IRenderable';
import {GridMedia} from '../GridMedia';
import {SearchTypes} from '../../../../../../common/entities/AutoCompleteItem';
import {RouterLink} from '@angular/router';
import {Thumbnail, ThumbnailManagerService} from '../../thumbnailManager.service';
import {Config} from '../../../../../../common/config/public/Config';
import {PageHelper} from '../../../../model/page.helper';
import {PhotoDTO, PhotoMetadata} from '../../../../../../common/entities/PhotoDTO';
import {SearchQueryTypes} from '../../../../../../common/entities/SearchQueryDTO';
@Component({
selector: 'app-gallery-grid-photo',
@ -20,11 +20,11 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy {
@ViewChild('photoContainer', {static: true}) container: ElementRef;
thumbnail: Thumbnail;
keywords: { value: string, type: SearchTypes }[] = null;
keywords: { value: string, type: SearchQueryTypes }[] = null;
infoBarVisible = false;
animationTimer: number = null;
readonly SearchTypes: typeof SearchTypes = SearchTypes;
readonly SearchQueryTypes: typeof SearchQueryTypes = SearchQueryTypes;
searchEnabled = true;
wasInView: boolean = null;
@ -60,9 +60,9 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy {
if (Config.Client.Faces.enabled) {
const names: string[] = (metadata.faces || []).map(f => f.name);
this.keywords = names.filter((name, index) => names.indexOf(name) === index)
.map(n => ({value: n, type: SearchTypes.person}));
.map(n => ({value: n, type: SearchQueryTypes.person}));
}
this.keywords = this.keywords.concat((metadata.keywords || []).map(k => ({value: k, type: SearchTypes.keyword})));
this.keywords = this.keywords.concat((metadata.keywords || []).map(k => ({value: k, type: SearchQueryTypes.keyword})));
}
}

View File

@ -6,8 +6,8 @@ import {Observable, Subscription, timer} from 'rxjs';
import {filter} from 'rxjs/operators';
import {PhotoDTO} from '../../../../../../common/entities/PhotoDTO';
import {GalleryLightboxMediaComponent} from '../media/media.lightbox.gallery.component';
import {SearchTypes} from '../../../../../../common/entities/AutoCompleteItem';
import {Config} from '../../../../../../common/config/public/Config';
import {SearchQueryTypes} from '../../../../../../common/entities/SearchQueryDTO';
export enum PlayBackStates {
Paused = 1,
@ -46,13 +46,13 @@ export class ControlsLightboxComponent implements OnDestroy, OnInit, OnChanges {
public controllersAlwaysOn = false;
public controllersVisible = true;
public drag = {x: 0, y: 0};
public SearchTypes = SearchTypes;
public SearchQueryTypes = SearchQueryTypes;
private visibilityTimer: number = null;
private timer: Observable<number>;
private timerSub: Subscription;
private prevDrag = {x: 0, y: 0};
private prevZoom = 1;
private faceContainerDim = {width: 0, height: 0};
public faceContainerDim = {width: 0, height: 0};
constructor(public fullScreenService: FullScreenService) {
}

View File

@ -6,7 +6,7 @@ import {VideoDTO, VideoMetadata} from '../../../../../../common/entities/VideoDT
import {Utils} from '../../../../../../common/Utils';
import {QueryService} from '../../../../model/query.service';
import {MapService} from '../../map/map.service';
import {SearchTypes} from '../../../../../../common/entities/AutoCompleteItem';
import {SearchQueryTypes} from '../../../../../../common/entities/SearchQueryDTO';
@Component({
selector: 'app-info-panel',
@ -19,8 +19,8 @@ export class InfoPanelLightboxComponent implements OnInit {
public readonly mapEnabled: boolean;
public readonly searchEnabled: boolean;
keywords: { value: string, type: SearchTypes }[] = null;
readonly SearchTypes: typeof SearchTypes = SearchTypes;
keywords: { value: string, type: SearchQueryTypes }[] = null;
readonly SearchQueryTypes: typeof SearchQueryTypes = SearchQueryTypes;
constructor(public queryService: QueryService,
public mapService: MapService) {
@ -59,9 +59,9 @@ export class InfoPanelLightboxComponent implements OnInit {
if (Config.Client.Faces.enabled) {
const names: string[] = (metadata.faces || []).map(f => f.name);
this.keywords = names.filter((name, index) => names.indexOf(name) === index)
.map(n => ({value: n, type: SearchTypes.person}));
.map(n => ({value: n, type: SearchQueryTypes.person}));
}
this.keywords = this.keywords.concat((metadata.keywords || []).map(k => ({value: k, type: SearchTypes.keyword})));
this.keywords = this.keywords.concat((metadata.keywords || []).map(k => ({value: k, type: SearchQueryTypes.keyword})));
}
}

View File

@ -10,7 +10,7 @@ import {Utils} from '../../../../../common/Utils';
import {SortingMethods} from '../../../../../common/entities/SortingMethods';
import {Config} from '../../../../../common/config/public/Config';
import {SearchResultDTO} from '../../../../../common/entities/SearchResultDTO';
import {SearchTypes} from '../../../../../common/entities/AutoCompleteItem';
import {SearchQueryTypes} from '../../../../../common/entities/SearchQueryDTO';
@Component({
selector: 'app-gallery-navbar',
@ -27,7 +27,7 @@ export class GalleryNavigatorComponent implements OnChanges {
sortingMethodsType: { key: number; value: string }[] = [];
config = Config;
DefaultSorting = Config.Client.Other.defaultPhotoSortingMethod;
readonly SearchTypes = SearchTypes;
readonly SearchQueryTypes = SearchQueryTypes;
private readonly RootFolderName: string;
constructor(private _authService: AuthenticationService,

View File

@ -0,0 +1,6 @@
.query-list{
padding-left: 25px;
}
label{
margin-top: 0.3rem;
}

View File

@ -0,0 +1,216 @@
<div class="row mt-1 mb-1" *ngIf="queryEntry">
<ng-container *ngIf="IsListQuery">
<div class="input-group col-md-2">
<select
id="listSearchType"
name="listSearchType"
class="form-control"
[(ngModel)]="queryEntry.type"
(ngModelChange)="onChangeType($event)">
<option *ngFor="let opt of SearchQueryTypesEnum" [ngValue]="opt.key">{{opt.value}}
</option>
</select>
</div>
<ng-container *ngIf="queryEntry.type == SearchQueryTypes.SOME_OF">
<label class="col-md-4 control-label" for="someOfMinValue">
<ng-container i18n>At least this many</ng-container>
(1-{{AsListQuery.list.length}}):</label>
<input
type="number" min="1" [max]="AsListQuery.list.length" class="form-control col-md-2" placeholder="1"
title="At least this many"
i18n-title
[(ngModel)]="AsSomeOfQuery.min"
(ngModelChange)="onChange(queryEntry)"
name="someOfMinValue"
id="someOfMinValue"
required="required">
<div class="col-md-3"></div>
</ng-container>
<ng-container *ngIf="queryEntry.type != SearchQueryTypes.SOME_OF">
<div class="col-md-9"></div>
</ng-container>
<button [ngClass]="true? 'btn-danger':'btn-secondary'"
class="btn float-right col-md-1">
<span class="oi oi-trash" aria-hidden="true" aria-label="Delete"></span>
</button>
<div class="container query-list">
<app-gallery-search-query-entry *ngFor="let sq of AsListQuery.list; index as i"
[(ngModel)]="sq"
(delete)="itemDeleted(i)">
</app-gallery-search-query-entry>
</div>
<button class="btn btn-primary mx-auto" (click)="addQuery()">
<span class="oi oi-plus" aria-hidden="true" aria-label="Add"> Add</span>
</button>
</ng-container>
<ng-container *ngIf="!IsListQuery">
<div class="input-group col-md-2">
<select
id="searchType"
name="searchType"
class="form-control"
[(ngModel)]="queryEntry.type"
(ngModelChange)="onChangeType(queryEntry)">
<option *ngFor="let opt of SearchQueryTypesEnum" [ngValue]="opt.key">{{opt.value}}
</option>
</select>
</div>
<div class="input-group col-md-9" *ngIf="IsTextQuery">
<input
id="searchField"
name="searchField"
placeholder="link"
class="form-control input-md"
[(ngModel)]="AsTextQuery.text"
(ngModelChange)="onChange(queryEntry)"
type="text"/>
</div>
<ng-container [ngSwitch]="queryEntry.type">
<div *ngSwitchCase="SearchQueryTypes.distance" class="col-md-9 d-flex">
<div class="input-group col-md-4">
<input type="number" class="form-control" placeholder="1"
id="distance"
min="0"
step="0.1"
[(ngModel)]="AsDistanceQuery.distance"
(ngModelChange)="onChange(queryEntry)"
name="distance" required>
<div class="input-group-append">
<span class="input-group-text">km</span>
</div>
</div>
<div class="input-group col-md-8">
<label class="col-md-4 control-label" for="maxResolution">From</label>
<input id="from"
name="from"
title="From"
placeholder="New York"
i18n-title
class="form-control input-md"
[(ngModel)]="AsDistanceQuery.from.text"
(ngModelChange)="onChange(queryEntry)"
type="text">
</div>
</div>
<div *ngSwitchCase="SearchQueryTypes.date" class="col-md-9 d-flex">
<div class="input-group col-md-6">
<label class="col-md-4 control-label" for="maxResolution">From:</label>
<input id="afterDate"
name="afterDate"
title="After"
i18n-title
class="form-control input-md"
[(ngModel)]="AsDateQuery.after"
(ngModelChange)="onChange(queryEntry)"
type="date">
</div>
<div class="input-group col-md-6">
<label class="col-md-4 control-label" for="maxResolution">To:</label>
<input id="beforeDate"
name="beforeDate"
title="Before"
i18n-title
[(ngModel)]="AsDateQuery.before"
(ngModelChange)="onChange(queryEntry)"
class="form-control input-md"
type="date">
</div>
</div>
<div *ngSwitchCase="SearchQueryTypes.rating" class="col-md-9 d-flex">
<div class="input-group col-md-6">
<label class="col-md-4 control-label" for="maxResolution">Min:</label>
<input id="minRating"
name="minRating"
title="Minimum Rating"
placeholder="0"
i18n-title
min="0"
[max]="AsRatingQuery.max || 5"
class="form-control input-md"
[(ngModel)]="AsRatingQuery.min"
(ngModelChange)="onChange(queryEntry)"
type="number">
</div>
<div class="input-group col-md-6">
<label class="col-md-4 control-label" for="maxResolution">Max:</label>
<input id="maxRating"
name="maxRating"
title="Maximum Rating"
placeholder="5"
i18n-title
[min]="AsRatingQuery.min || 0"
max="5"
class="form-control input-md"
[(ngModel)]="AsRatingQuery.max"
(ngModelChange)="onChange(queryEntry)"
type="number">
</div>
</div>
<div *ngSwitchCase="SearchQueryTypes.resolution" class="col-md-9 d-flex">
<div class="input-group col-md-6">
<label class="col-md-4 control-label" for="maxResolution">Min:</label>
<input id="minResolution"
name="minResolution"
title="Minimum Rating"
placeholder="0"
i18n-title
min="0"
class="form-control input-md"
[(ngModel)]="AsResolutionQuery.min"
(ngModelChange)="onChange(queryEntry)"
type="number">
<div class="input-group-append">
<span class="input-group-text">Mpx</span>
</div>
</div>
<div class="input-group col-md-6">
<label class="col-md-4 control-label" for="maxResolution">Max:</label>
<input id="maxResolution"
name="maxResolution"
title="Maximum Rating"
placeholder="5"
i18n-title
[min]="AsResolutionQuery.min || 0"
class="form-control input-md"
[(ngModel)]="AsResolutionQuery.max"
(ngModelChange)="onChange(queryEntry)"
type="number">
<div class="input-group-append">
<span class="input-group-text">Mpx</span>
</div>
</div>
</div>
<div *ngSwitchCase="SearchQueryTypes.orientation" class="col-md-9 d-flex">
<div class="input-group col-md-6">
<bSwitch
class="switch"
id="orientation"
name="orientation"
title="Orientation"
switch-on-color="primary"
switch-off-color="primary"
switch-inverse="true"
switch-off-text="Portrait"
switch-on-text="Landscape"
i18n-switch-off-text
i18n-switch-on-text
switch-handle-width="100"
switch-label-width="20"
(ngModelChange)="onChange(queryEntry)"
[(ngModel)]="AsOrientationQuery.landscape">
</bSwitch>
</div>
</div>
</ng-container>
<button [ngClass]="true? 'btn-danger':'btn-secondary'"
class="btn float-right col-md-1"
(click)="deleteItem()">
<span class="oi oi-trash" aria-hidden="true" aria-label="Delete"></span>
</button>
</ng-container>
</div>

View File

@ -0,0 +1,161 @@
import {Component, EventEmitter, forwardRef, OnChanges, Output} from '@angular/core';
import {
DateSearch,
DistanceSearch,
ListSearchQueryTypes,
OrientationSearch,
RatingSearch,
ResolutionSearch,
SearchListQuery,
SearchQueryDTO,
SearchQueryTypes,
SomeOfSearchQuery,
TextSearch,
TextSearchQueryTypes
} from '../../../../../../common/entities/SearchQueryDTO';
import {Utils} from '../../../../../../common/Utils';
import {ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator} from '@angular/forms';
@Component({
selector: 'app-gallery-search-query-entry',
templateUrl: './query-entry.search.gallery.component.html',
styleUrls: ['./query-entry.search.gallery.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GallerySearchQueryEntryComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => GallerySearchQueryEntryComponent),
multi: true
}
]
})
export class GallerySearchQueryEntryComponent implements ControlValueAccessor, Validator, OnChanges {
public queryEntry: SearchQueryDTO;
public SearchQueryTypesEnum: { value: string; key: SearchQueryTypes }[];
public SearchQueryTypes = SearchQueryTypes;
@Output() delete = new EventEmitter<void>();
constructor() {
this.SearchQueryTypesEnum = Utils.enumToArray(SearchQueryTypes);
}
get IsTextQuery(): boolean {
return this.queryEntry && TextSearchQueryTypes.includes(this.queryEntry.type);
}
get IsListQuery(): boolean {
return this.queryEntry && ListSearchQueryTypes.includes(this.queryEntry.type);
}
get AsListQuery(): SearchListQuery {
return <any>this.queryEntry;
}
get AsDateQuery(): DateSearch {
return <any>this.queryEntry;
}
get AsResolutionQuery(): ResolutionSearch {
return <any>this.queryEntry;
}
get AsOrientationQuery(): OrientationSearch {
return <any>this.queryEntry;
}
get AsDistanceQuery(): DistanceSearch {
return <any>this.queryEntry;
}
get AsRatingQuery(): RatingSearch {
return <any>this.queryEntry;
}
get AsSomeOfQuery(): SomeOfSearchQuery {
return <any>this.queryEntry;
}
get AsTextQuery(): TextSearch {
return <any>this.queryEntry;
}
validate(control: FormControl): ValidationErrors {
return {required: true};
}
addQuery(): void {
console.log('clicked', this.IsListQuery);
if (!this.IsListQuery) {
return;
}
this.AsListQuery.list.push(<TextSearch>{type: SearchQueryTypes.any_text, text: ''});
}
onChangeType($event: any) {
if (this.IsListQuery) {
delete this.AsTextQuery.text;
this.AsListQuery.list = this.AsListQuery.list || [
<TextSearch>{type: SearchQueryTypes.any_text, text: ''},
<TextSearch>{type: SearchQueryTypes.any_text, text: ''}
];
} else {
delete this.AsListQuery.list;
}
if (this.queryEntry.type === SearchQueryTypes.distance) {
this.AsDistanceQuery.from = {text: ''};
this.AsDistanceQuery.distance = 1;
} else {
delete this.AsDistanceQuery.from;
delete this.AsDistanceQuery.distance;
}
if (this.queryEntry.type === SearchQueryTypes.orientation) {
this.AsOrientationQuery.landscape = true;
} else {
delete this.AsOrientationQuery.landscape;
}
this.onChange(this.queryEntry);
}
deleteItem() {
this.delete.emit();
}
itemDeleted(i: number) {
this.AsListQuery.list.splice(i, 1);
}
ngOnChanges(): void {
// console.log('ngOnChanges', this.queryEntry);
}
public onChange(value: any): void {
// console.log('onChange', this.queryEntry);
}
public onTouched(): void {
}
public writeValue(obj: any): void {
this.queryEntry = obj;
// console.log('write value', this.queryEntry);
this.ngOnChanges();
}
public registerOnChange(fn: any): void {
// console.log('registerOnChange', fn);
this.onChange = fn;
}
public registerOnTouched(fn: any): void {
this.onTouched = fn;
}
}

View File

@ -7,7 +7,7 @@
(keyup)="onSearchChange($event)"
(blur)="onFocusLost()"
(focus)="onFocus()"
[(ngModel)]="searchText"
[(ngModel)]="rawSearchText"
#name="ngModel"
size="30"
ngControl="search"
@ -19,26 +19,63 @@
<div class="autocomplete-list" *ngIf="autoCompleteItems.length > 0"
(mouseover)="setMouseOverAutoComplete(true)" (mouseout)="setMouseOverAutoComplete(false)">
<div class="autocomplete-item" *ngFor="let item of autoCompleteItems">
<a [routerLink]="['/search', item.text, {type: SearchTypes[item.type]}]">
<div>
<span [ngSwitch]="item.type">
<span *ngSwitchCase="SearchTypes.photo" class="oi oi-image"></span>
<span *ngSwitchCase="SearchTypes.person" class="oi oi-person"></span>
<span *ngSwitchCase="SearchTypes.video" class="oi oi-video"></span>
<span *ngSwitchCase="SearchTypes.directory" class="oi oi-folder"></span>
<span *ngSwitchCase="SearchTypes.keyword" class="oi oi-tag"></span>
<span *ngSwitchCase="SearchTypes.position" class="oi oi-map-marker"></span>
<span *ngSwitchCase="SearchQueryTypes.caption" class="oi oi-image"></span>
<span *ngSwitchCase="SearchQueryTypes.directory" class="oi oi-folder"></span>
<span *ngSwitchCase="SearchQueryTypes.file_name" class="oi oi-image"></span>
<span *ngSwitchCase="SearchQueryTypes.keyword" class="oi oi-tag"></span>
<span *ngSwitchCase="SearchQueryTypes.person" class="oi oi-person"></span>
<span *ngSwitchCase="SearchQueryTypes.position" class="oi oi-map-marker"></span>
</span>
{{item.preText}}<strong>{{item.highLightText}}</strong>{{item.postText}}
</a>
</div>
</div>
</div>
<div class="input-group-btn" style="display: block">
<button class="btn btn-light" type="button"
[routerLink]="['/search', searchText]">
[routerLink]="['/search', HTMLSearchQuery]">
<span class="oi oi-magnifying-glass"></span>
</button>
</div>
<div class="input-group-btn" style="display: block">
<button class="btn btn-light" type="button" (click)="openModal(searchModal)">
<span class="oi oi-chevron-bottom"></span>
</button>
</div>
</div>
</form>
<ng-template #searchModal>
<!-- sharing Modal-->
<div class="modal-header">
<h5 class="modal-title" i18n>Search</h5>
<button type="button" class="close" (click)="hideModal()" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form #searchPanelForm="ngForm" class="form-horizontal">
<input type="text"
class="form-control"
i18n-placeholder
placeholder="Search"
disabled
[ngModel]="rawSearchText"
size="30"
name="srch-term-preview"
id="srch-term-preview"
autocomplete="off">
<app-gallery-search-query-entry
[(ngModel)]="searchQueryDTO"
name="search-root"
(delete)="resetQuery()">
</app-gallery-search-query-entry>
</form>
</div>
</ng-template>

View File

@ -1,12 +1,15 @@
import {Component, OnDestroy} from '@angular/core';
import {Component, OnDestroy, TemplateRef} from '@angular/core';
import {AutoCompleteService} from './autocomplete.service';
import {AutoCompleteItem, SearchTypes} from '../../../../../common/entities/AutoCompleteItem';
import {AutoCompleteItem} from '../../../../../common/entities/AutoCompleteItem';
import {ActivatedRoute, Params, RouterLink} from '@angular/router';
import {GalleryService} from '../gallery.service';
import {Subscription} from 'rxjs';
import {Config} from '../../../../../common/config/public/Config';
import {NavigationService} from '../../../model/navigation.service';
import {QueryParams} from '../../../../../common/QueryParams';
import {MetadataSearchQueryTypes, SearchQueryDTO, SearchQueryTypes, TextSearch} from '../../../../../common/entities/SearchQueryDTO';
import {BsModalService} from 'ngx-bootstrap/modal';
import {BsModalRef} from 'ngx-bootstrap/modal/bs-modal-ref.service';
@Component({
selector: 'app-gallery-search',
@ -17,31 +20,48 @@ import {QueryParams} from '../../../../../common/QueryParams';
export class GallerySearchComponent implements OnDestroy {
autoCompleteItems: AutoCompleteRenderItem[] = [];
public searchText = '';
public searchQueryDTO: SearchQueryDTO = <TextSearch>{type: SearchQueryTypes.any_text, text: ''};
mouseOverAutoComplete = false;
readonly SearchQueryTypes: typeof SearchQueryTypes;
modalRef: BsModalRef;
public readonly MetadataSearchQueryTypes: { value: string; key: SearchQueryTypes }[];
private cache = {
lastAutocomplete: '',
lastInstantSearch: ''
};
mouseOverAutoComplete = false;
readonly SearchTypes: typeof SearchTypes;
private readonly subscription: Subscription = null;
constructor(private _autoCompleteService: AutoCompleteService,
private _galleryService: GalleryService,
private navigationService: NavigationService,
private _route: ActivatedRoute) {
private _route: ActivatedRoute,
private modalService: BsModalService) {
this.SearchTypes = SearchTypes;
this.SearchQueryTypes = SearchQueryTypes;
this.MetadataSearchQueryTypes = MetadataSearchQueryTypes.map(v => ({key: v, value: SearchQueryTypes[v]}));
this.subscription = this._route.params.subscribe((params: Params) => {
const searchText = params[QueryParams.gallery.searchText];
if (searchText && searchText !== '') {
this.searchText = searchText;
const searchQuery = params[QueryParams.gallery.search.query];
if (searchQuery) {
this.searchQueryDTO = searchQuery;
}
});
}
public get rawSearchText() {
return JSON.stringify(this.searchQueryDTO);
}
public set rawSearchText(val: any) {
}
get HTMLSearchQuery() {
const searchQuery: any = {};
searchQuery[QueryParams.gallery.search.query] = this.searchQueryDTO;
return searchQuery;
}
ngOnDestroy() {
if (this.subscription !== null) {
@ -57,19 +77,9 @@ export class GallerySearchComponent implements OnDestroy {
if (Config.Client.Search.AutoComplete.enabled &&
this.cache.lastAutocomplete !== searchText) {
this.cache.lastAutocomplete = searchText;
this.autocomplete(searchText).catch(console.error);
// this.autocomplete(searchText).catch(console.error);
}
if (Config.Client.Search.instantSearchEnabled &&
this.cache.lastInstantSearch !== searchText) {
this.cache.lastInstantSearch = searchText;
if (searchText === '') {
return this.navigationService.toGallery().catch(console.error);
}
this._galleryService.runInstantSearch(searchText);
this.navigationService.search(searchText).catch(console.error);
}
}
@ -84,9 +94,25 @@ export class GallerySearchComponent implements OnDestroy {
}
public onFocus() {
this.autocomplete(this.searchText).catch(console.error);
// TODO: implement autocomplete
// this.autocomplete(this.searchText).catch(console.error);
}
public async openModal(template: TemplateRef<any>) {
this.modalRef = this.modalService.show(template, {class: 'modal-lg'});
document.body.style.paddingRight = '0px';
}
public hideModal() {
this.modalRef.hide();
this.modalRef = null;
}
resetQuery() {
this.searchQueryDTO = <TextSearch>{text: '', type: SearchQueryTypes.any_text};
}
private emptyAutoComplete() {
this.autoCompleteItems = [];
}
@ -126,9 +152,9 @@ class AutoCompleteRenderItem {
public preText = '';
public highLightText = '';
public postText = '';
public type: SearchTypes;
public type: SearchQueryTypes;
constructor(public text: string, searchText: string, type: SearchTypes) {
constructor(public text: string, searchText: string, type: SearchQueryTypes) {
const preIndex = text.toLowerCase().indexOf(searchText.toLowerCase());
if (preIndex > -1) {
this.preText = text.substring(0, preIndex);

View File

@ -15,7 +15,7 @@ import {
SearchQueryTypes,
SomeOfSearchQuery,
TextSearch,
TextSearchQueryTypes
TextSearchQueryMatchTypes
} from '../../../../../src/common/entities/SearchQueryDTO';
import {IndexingManager} from '../../../../../src/backend/model/database/sql/IndexingManager';
import {DirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO';
@ -482,7 +482,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
query = <TextSearch>{
text: 'Boba',
type: SearchQueryTypes.any_text,
matchType: TextSearchQueryTypes.exact_match
matchType: TextSearchQueryMatchTypes.exact_match
};
expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
@ -496,7 +496,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
query = <TextSearch>{
text: 'Boba Fett',
type: SearchQueryTypes.any_text,
matchType: TextSearchQueryTypes.exact_match
matchType: TextSearchQueryMatchTypes.exact_match
};
expect(Utils.clone(await sm.search(query)))
@ -571,7 +571,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
query = <TextSearch>{
text: 'star wars',
matchType: TextSearchQueryTypes.exact_match,
matchType: TextSearchQueryMatchTypes.exact_match,
type: SearchQueryTypes.keyword
};
@ -585,7 +585,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
query = <TextSearch>{
text: 'wookiees',
matchType: TextSearchQueryTypes.exact_match,
matchType: TextSearchQueryMatchTypes.exact_match,
type: SearchQueryTypes.keyword
};
@ -684,14 +684,14 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
query = <TextSearch>{
text: '/wars dir',
matchType: TextSearchQueryTypes.exact_match,
matchType: TextSearchQueryMatchTypes.exact_match,
type: SearchQueryTypes.directory
};
expect(Utils.clone(await sm.search(<TextSearch>{
text: '/wars dir',
matchType: TextSearchQueryTypes.exact_match,
matchType: TextSearchQueryMatchTypes.exact_match,
type: SearchQueryTypes.directory
}))).to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query,
@ -704,7 +704,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
query = <TextSearch>{
text: '/wars dir/Return of the Jedi',
matchType: TextSearchQueryTypes.exact_match,
matchType: TextSearchQueryMatchTypes.exact_match,
type: SearchQueryTypes.directory
};
@ -718,7 +718,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
query = <TextSearch>{
text: '/wars dir/Return of the Jedi',
matchType: TextSearchQueryTypes.exact_match,
matchType: TextSearchQueryMatchTypes.exact_match,
type: SearchQueryTypes.directory
};
@ -752,7 +752,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
query = <TextSearch>{
text: 'Boba',
type: SearchQueryTypes.person,
matchType: TextSearchQueryTypes.exact_match
matchType: TextSearchQueryMatchTypes.exact_match
};
expect(Utils.clone(await sm.search(query))).to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
@ -766,13 +766,13 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
query = <TextSearch>{
text: 'Boba Fett',
type: SearchQueryTypes.person,
matchType: TextSearchQueryTypes.exact_match
matchType: TextSearchQueryMatchTypes.exact_match
};
expect(Utils.clone(await sm.search(<TextSearch>{
text: 'Boba Fett',
type: SearchQueryTypes.person,
matchType: TextSearchQueryTypes.exact_match
matchType: TextSearchQueryMatchTypes.exact_match
}))).to.deep.equalInAnyOrder(removeDir(<SearchResultDTO>{
searchQuery: query,
directories: [],
@ -1098,7 +1098,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => {
query = <TextSearch>{
text: 'wookiees',
matchType: TextSearchQueryTypes.exact_match,
matchType: TextSearchQueryMatchTypes.exact_match,
type: SearchQueryTypes.keyword
};
expect(Utils.clone(await sm.getRandomPhoto(query))).to.deep.equalInAnyOrder(searchifyMedia(p_faceLess));