diff --git a/src/backend/model/database/sql/SearchManager.ts b/src/backend/model/database/sql/SearchManager.ts index fff48388..dd7b1774 100644 --- a/src/backend/model/database/sql/SearchManager.ts +++ b/src/backend/model/database/sql/SearchManager.ts @@ -11,18 +11,21 @@ import {Brackets, SelectQueryBuilder, WhereExpression} from 'typeorm'; import {Config} from '../../../../common/config/private/Config'; import { ANDSearchQuery, - DateSearch, DistanceSearch, + FromDateSearch, + MaxRatingSearch, + MaxResolutionSearch, + MinRatingSearch, + MinResolutionSearch, OrientationSearch, ORSearchQuery, - RatingSearch, - ResolutionSearch, SearchListQuery, SearchQueryDTO, SearchQueryTypes, SomeOfSearchQuery, TextSearch, - TextSearchQueryMatchTypes + TextSearchQueryMatchTypes, + ToDateSearch } from '../../../../common/entities/SearchQueryDTO'; import {GalleryManager} from './GalleryManager'; import {ObjectManagers} from '../../ObjectManagers'; @@ -266,75 +269,100 @@ export class SearchManager implements ISearchManager { return q; }); - case SearchQueryTypes.date: + case SearchQueryTypes.from_date: return new Brackets(q => { - if (typeof (query).before === 'undefined' && typeof (query).after === 'undefined') { - throw new Error('Invalid search query: Date Query should contain before or after value'); + if (typeof (query).value === 'undefined') { + throw new Error('Invalid search query: Date Query should contain from value'); } const whereFN = (query).negate ? 'orWhere' : 'andWhere'; const relation = (query).negate ? '<' : '>='; const relationRev = (query).negate ? '>' : '<='; - if (typeof (query).after !== 'undefined') { - const textParam: any = {}; - textParam['after' + paramCounter.value] = (query).after; - q.where(`media.metadata.creationDate ${relation} :after${paramCounter.value}`, textParam); - } + const textParam: any = {}; + textParam['from' + paramCounter.value] = (query).value; + q.where(`media.metadata.creationDate ${relation} :from${paramCounter.value}`, textParam); - if (typeof (query).before !== 'undefined') { - const textParam: any = {}; - textParam['before' + paramCounter.value] = (query).before; - q[whereFN](`media.metadata.creationDate ${relationRev} :before${paramCounter.value}`, textParam); - } paramCounter.value++; return q; }); - case SearchQueryTypes.rating: + case SearchQueryTypes.to_date: return new Brackets(q => { - if (typeof (query).min === 'undefined' && typeof (query).max === 'undefined') { - throw new Error('Invalid search query: Rating Query should contain min or max value'); + if (typeof (query).value === 'undefined') { + throw new Error('Invalid search query: Date Query should contain to value'); + } + const relation = (query).negate ? '>' : '<='; + + const textParam: any = {}; + textParam['to' + paramCounter.value] = (query).value; + q.where(`media.metadata.creationDate ${relation} :to${paramCounter.value}`, textParam); + + paramCounter.value++; + return q; + }); + + case SearchQueryTypes.min_rating: + return new Brackets(q => { + if (typeof (query).value === 'undefined') { + throw new Error('Invalid search query: Rating Query should contain minvalue'); } - const whereFN = (query).negate ? 'orWhere' : 'andWhere'; const relation = (query).negate ? '<' : '>='; - const relationRev = (query).negate ? '>' : '<='; - if (typeof (query).min !== 'undefined') { - const textParam: any = {}; - textParam['min' + paramCounter.value] = (query).min; - q.where(`media.metadata.rating ${relation} :min${paramCounter.value}`, textParam); + + const textParam: any = {}; + textParam['min' + paramCounter.value] = (query).value; + q.where(`media.metadata.rating ${relation} :min${paramCounter.value}`, textParam); + + paramCounter.value++; + return q; + }); + case SearchQueryTypes.max_rating: + return new Brackets(q => { + if (typeof (query).value === 'undefined') { + throw new Error('Invalid search query: Rating Query should contain max value'); } - if (typeof (query).max !== 'undefined') { + const relation = (query).negate ? '>' : '<='; + + if (typeof (query).value !== 'undefined') { const textParam: any = {}; - textParam['max' + paramCounter.value] = (query).max; - q[whereFN](`media.metadata.rating ${relationRev} :max${paramCounter.value}`, textParam); + textParam['max' + paramCounter.value] = (query).value; + q.where(`media.metadata.rating ${relation} :max${paramCounter.value}`, textParam); } paramCounter.value++; return q; }); - case SearchQueryTypes.resolution: + case SearchQueryTypes.min_resolution: return new Brackets(q => { - if (typeof (query).min === 'undefined' && typeof (query).max === 'undefined') { + if (typeof (query).value === 'undefined') { + throw new Error('Invalid search query: Resolution Query should contain min value'); + } + + const relation = (query).negate ? '<' : '>='; + + const textParam: any = {}; + textParam['min' + paramCounter.value] = (query).value * 1000 * 1000; + q.where(`media.metadata.size.width * media.metadata.size.height ${relation} :min${paramCounter.value}`, textParam); + + + paramCounter.value++; + return q; + }); + + case SearchQueryTypes.max_resolution: + return new Brackets(q => { + if (typeof (query).value === 'undefined') { throw new Error('Invalid search query: Rating Query should contain min or max value'); } - const whereFN = (query).negate ? 'orWhere' : 'andWhere'; - const relation = (query).negate ? '<' : '>='; - const relationRev = (query).negate ? '>' : '<='; - if (typeof (query).min !== 'undefined') { - const textParam: any = {}; - textParam['min' + paramCounter.value] = (query).min * 1000 * 1000; - q.where(`media.metadata.size.width * media.metadata.size.height ${relation} :min${paramCounter.value}`, textParam); - } + const relation = (query).negate ? '>' : '<='; + + const textParam: any = {}; + textParam['max' + paramCounter.value] = (query).value * 1000 * 1000; + q.where(`media.metadata.size.width * media.metadata.size.height ${relation} :max${paramCounter.value}`, textParam); - if (typeof (query).max !== 'undefined') { - const textParam: any = {}; - textParam['max' + paramCounter.value] = (query).max * 1000 * 1000; - q[whereFN](`media.metadata.size.width * media.metadata.size.height ${relationRev} :max${paramCounter.value}`, textParam); - } paramCounter.value++; return q; }); diff --git a/src/common/Utils.ts b/src/common/Utils.ts index 2041de51..c63ec835 100644 --- a/src/common/Utils.ts +++ b/src/common/Utils.ts @@ -54,6 +54,11 @@ export class Utils { return ret.substr(ret.length - length); } + /** + * Checks if the two input (let them be objects or arrays or just primitives) are equal + * @param object + * @param filter + */ static equalsFilter(object: any, filter: any): boolean { if (typeof filter !== 'object' || filter == null) { return object === filter; diff --git a/src/common/entities/SearchQueryDTO.ts b/src/common/entities/SearchQueryDTO.ts index 550347f7..81a5e4a8 100644 --- a/src/common/entities/SearchQueryDTO.ts +++ b/src/common/entities/SearchQueryDTO.ts @@ -1,13 +1,19 @@ import {GPSMetadata} from './PhotoDTO'; +import {Utils} from '../Utils'; export enum SearchQueryTypes { AND = 1, OR, SOME_OF, // non-text metadata - date = 10, - rating, + // |- range types + from_date = 10, + to_date, + min_rating, + max_rating, + min_resolution, + max_resolution, + distance, - resolution, orientation, // TEXT search types @@ -34,24 +40,33 @@ export const TextSearchQueryTypes = [ SearchQueryTypes.person, SearchQueryTypes.position, ]; +export const MinRangeSearchQueryTypes = [ + SearchQueryTypes.from_date, + SearchQueryTypes.min_rating, + SearchQueryTypes.min_resolution, +]; +export const MaxRangeSearchQueryTypes = [ + SearchQueryTypes.to_date, + SearchQueryTypes.max_rating, + SearchQueryTypes.max_resolution +]; + +export const RangeSearchQueryTypes = MinRangeSearchQueryTypes.concat(MaxRangeSearchQueryTypes); export const MetadataSearchQueryTypes = [ - // non-text metadata - SearchQueryTypes.date, - SearchQueryTypes.rating, SearchQueryTypes.distance, - SearchQueryTypes.resolution, - SearchQueryTypes.orientation, + SearchQueryTypes.orientation +].concat(RangeSearchQueryTypes) + .concat(TextSearchQueryTypes); - // TEXT search types - SearchQueryTypes.any_text, - SearchQueryTypes.caption, - SearchQueryTypes.directory, - SearchQueryTypes.file_name, - SearchQueryTypes.keyword, - SearchQueryTypes.person, - SearchQueryTypes.position, -]; +export const rangedTypePairs: any = {}; +rangedTypePairs[SearchQueryTypes.from_date] = SearchQueryTypes.to_date; +rangedTypePairs[SearchQueryTypes.min_rating] = SearchQueryTypes.max_rating; +rangedTypePairs[SearchQueryTypes.min_resolution] = SearchQueryTypes.max_resolution; +// add the other direction too +for (const key of Object.keys(rangedTypePairs)) { + rangedTypePairs[rangedTypePairs[key]] = key; +} export enum TextSearchQueryMatchTypes { exact_match = 1, like = 2 @@ -59,6 +74,12 @@ export enum TextSearchQueryMatchTypes { export namespace SearchQueryDTO { + export const getRangedQueryPair = (type: SearchQueryTypes): SearchQueryTypes => { + if (rangedTypePairs[type]) { + return rangedTypePairs[type]; + } + throw new Error('Unknown ranged type'); + }; export const negate = (query: SearchQueryDTO): SearchQueryDTO => { switch (query.type) { case SearchQueryTypes.AND: @@ -74,9 +95,12 @@ export namespace SearchQueryDTO { (query).landscape = !(query).landscape; return query; - case SearchQueryTypes.date: - case SearchQueryTypes.rating: - case SearchQueryTypes.resolution: + case SearchQueryTypes.from_date: + case SearchQueryTypes.to_date: + case SearchQueryTypes.min_rating: + case SearchQueryTypes.max_rating: + case SearchQueryTypes.min_resolution: + case SearchQueryTypes.max_resolution: case SearchQueryTypes.distance: case SearchQueryTypes.any_text: case SearchQueryTypes.person: @@ -95,60 +119,211 @@ export namespace SearchQueryDTO { throw new Error('Unknown type' + query.type); } }; + + export const parse = (str: string): SearchQueryDTO => { + console.log(str); + str = str.replace(/\s\s+/g, ' ') // remove double spaces + .replace(/:\s+/g, ':').replace(/\)(?=\S)/g, ') ').trim(); + + if (str.charAt(0) === '(' && str.charAt(str.length - 1) === ')') { + str = str.slice(1, str.length - 1); + } + const fistNonBRSpace = () => { + const bracketIn = []; + for (let i = 0; i < str.length; ++i) { + if (str.charAt(i) === '(') { + bracketIn.push(i); + continue; + } + if (str.charAt(i) === ')') { + bracketIn.pop(); + continue; + } + + if (bracketIn.length === 0 && str.charAt(i) === ' ') { + return i; + } + } + return str.length - 1; + }; + + // tokenize + const tokenEnd = fistNonBRSpace(); + + if (tokenEnd !== str.length - 1) { + if (str.startsWith(' and', tokenEnd)) { + return { + type: SearchQueryTypes.AND, + list: [SearchQueryDTO.parse(str.slice(0, tokenEnd)), // trim brackets + SearchQueryDTO.parse(str.slice(tokenEnd + 4))] + }; + } else { + let padding = 0; + if (str.startsWith(' or', tokenEnd)) { + padding = 3; + } + return { + type: SearchQueryTypes.OR, + list: [SearchQueryDTO.parse(str.slice(0, tokenEnd)), // trim brackets + SearchQueryDTO.parse(str.slice(tokenEnd + padding))] + }; + } + } + if (str.startsWith('some-of:') || + new RegExp(/^\d*-of:/).test(str)) { + const prefix = str.startsWith('some-of:') ? 'some-of:' : new RegExp(/^\d*-of:/).exec(str)[0]; + let tmpList: any = SearchQueryDTO.parse(str.slice(prefix.length + 1, -1)); // trim brackets + const unfoldList = (q: SearchListQuery): SearchQueryDTO[] => { + if (q.list) { + return [].concat.apply([], q.list.map(e => unfoldList(e))); // flatten array + } + return [q]; + }; + tmpList = unfoldList(tmpList); + const ret = { + type: SearchQueryTypes.SOME_OF, + list: tmpList + }; + if (new RegExp(/^\d*-of:/).test(str)) { + ret.min = parseInt(new RegExp(/^\d*/).exec(str)[0], 10); + } + return ret; + } + + if (str.startsWith('from:')) { + return { + type: SearchQueryTypes.from_date, + value: Date.parse(str.slice('from:'.length + 1, str.length - 1)) + }; + } + if (str.startsWith('to:')) { + return { + type: SearchQueryTypes.to_date, + value: Date.parse(str.slice('to:'.length + 1, str.length - 1)) + }; + } + + if (str.startsWith('min-rating:')) { + return { + type: SearchQueryTypes.min_rating, + value: parseInt(str.slice('min-rating:'.length), 10) + }; + } + if (str.startsWith('max-rating:')) { + return { + type: SearchQueryTypes.max_rating, + value: parseInt(str.slice('max-rating:'.length), 10) + }; + } + if (str.startsWith('min-resolution:')) { + return { + type: SearchQueryTypes.min_resolution, + value: parseInt(str.slice('min-resolution:'.length), 10) + }; + } + if (str.startsWith('max-resolution:')) { + return { + type: SearchQueryTypes.max_resolution, + value: parseInt(str.slice('max-resolution:'.length), 10) + }; + } + if (new RegExp(/^\d*-km-from:/).test(str)) { + let from = str.slice(new RegExp(/^\d*-km-from:/).exec(str)[0].length); + if (from.charAt(0) === '(' && from.charAt(from.length - 1) === ')') { + from = from.slice(1, from.length - 1); + } + return { + type: SearchQueryTypes.distance, + distance: parseInt(new RegExp(/^\d*/).exec(str)[0], 10), + from: {text: from} + }; + } + + if (str.startsWith('orientation:')) { + return { + type: SearchQueryTypes.orientation, + landscape: str.slice('orientation:'.length) === 'landscape' + }; + } + + // parse text search + const tmp = TextSearchQueryTypes.map(type => ({ + key: SearchQueryTypes[type] + ':', + queryTemplate: {type: type, text: ''} + })); + for (let i = 0; i < tmp.length; ++i) { + if (str.startsWith(tmp[i].key)) { + const ret: TextSearch = Utils.clone(tmp[i].queryTemplate); + if (str.charAt(tmp[i].key.length) === '"' && str.charAt(str.length - 1) === '"') { + ret.text = str.slice(tmp[i].key.length + 1, str.length - 1); + ret.matchType = TextSearchQueryMatchTypes.exact_match; + } else if (str.charAt(tmp[i].key.length) === '(' && str.charAt(str.length - 1) === ')') { + ret.text = str.slice(tmp[i].key.length + 1, str.length - 1); + } else { + ret.text = str.slice(tmp[i].key.length); + } + return ret; + } + } + + + return {type: SearchQueryTypes.any_text, text: str}; + }; + export const stringify = (query: SearchQueryDTO): string => { if (!query || !query.type) { return ''; } switch (query.type) { case SearchQueryTypes.AND: - return '(' + (query).list.map(q => SearchQueryDTO.stringify(q)).join(' AND ') + ')'; + return '(' + (query).list.map(q => SearchQueryDTO.stringify(q)).join(' and ') + ')'; case SearchQueryTypes.OR: - return '(' + (query).list.map(q => SearchQueryDTO.stringify(q)).join(' OR ') + ')'; + return '(' + (query).list.map(q => SearchQueryDTO.stringify(q)).join(' or ') + ')'; case SearchQueryTypes.SOME_OF: if ((query).min) { - return (query).min + ' OF: (' + - (query).list.map(q => SearchQueryDTO.stringify(q)).join(', ') + ')'; + return (query).min + '-of:(' + + (query).list.map(q => SearchQueryDTO.stringify(q)).join(' ') + ')'; } - return 'SOME OF: (' + - (query).list.map(q => SearchQueryDTO.stringify(q)).join(', ') + ')'; + return 'some-of:(' + + (query).list.map(q => SearchQueryDTO.stringify(q)).join(' ') + ')'; case SearchQueryTypes.orientation: - return 'orientation: ' + (query).landscape ? 'landscape' : 'portrait'; + return 'orientation:' + ((query).landscape ? 'landscape' : 'portrait'); - case SearchQueryTypes.date: - let ret = ''; - if ((query).after) { - ret += 'from: ' + (query).after; + case SearchQueryTypes.from_date: + if (!(query).value) { + return ''; } - if ((query).before) { - ret += ' to: ' + (query).before; + return 'from:(' + new Date((query).value).toLocaleDateString() + ')'.trim(); + case SearchQueryTypes.to_date: + if (!(query).value) { + return ''; } - return ret.trim(); - case SearchQueryTypes.rating: - let rating = ''; - if ((query).min) { - rating += 'min-rating: ' + (query).min; - } - if ((query).max) { - rating += ' max-rating: ' + (query).max; - } - return rating.trim(); - case SearchQueryTypes.resolution: - let res = ''; - if ((query).min) { - res += 'min-resolution: ' + (query).min; - } - if ((query).max) { - res += ' max-resolution: ' + (query).max; - } - return res.trim(); + return 'to:(' + new Date((query).value).toLocaleDateString() + ')'.trim(); + case SearchQueryTypes.min_rating: + return 'min-rating:' + (isNaN((query).value) ? '' : (query).value); + case SearchQueryTypes.max_rating: + return 'max-rating:' + (isNaN((query).value) ? '' : (query).value); + case SearchQueryTypes.min_resolution: + return 'min-resolution:' + (isNaN((query).value) ? '' : (query).value); + case SearchQueryTypes.max_resolution: + return 'max-resolution:' + (isNaN((query).value) ? '' : (query).value); case SearchQueryTypes.distance: - return (query).distance + ' km from: ' + (query).from.text; + if ((query).from.text.indexOf(' ') !== -1) { + return (query).distance + '-km-from:(' + (query).from.text + ')'; + } + return (query).distance + '-km-from:' + (query).from.text; case SearchQueryTypes.any_text: + if ((query).matchType === TextSearchQueryMatchTypes.exact_match) { + return '"' + (query).text + '"'; + + } else if ((query).text.indexOf(' ') !== -1) { + return '(' + (query).text + ')'; + } return (query).text; case SearchQueryTypes.person: @@ -160,6 +335,12 @@ export namespace SearchQueryDTO { if (!(query).text) { return ''; } + if ((query).matchType === TextSearchQueryMatchTypes.exact_match) { + return SearchQueryTypes[query.type] + ':"' + (query).text + '"'; + + } else if ((query).text.indexOf(' ') !== -1) { + return SearchQueryTypes[query.type] + ':(' + (query).text + ')'; + } return SearchQueryTypes[query.type] + ':' + (query).text; default: @@ -177,11 +358,6 @@ export interface NegatableSearchQuery extends SearchQueryDTO { negate?: boolean; // if true negates the expression } -export interface RangeSearchQuery extends SearchQueryDTO { - min?: number; - max?: number; -} - export interface SearchListQuery extends SearchQueryDTO { list: SearchQueryDTO[]; } @@ -225,22 +401,42 @@ export interface DistanceSearch extends NegatableSearchQuery { } -export interface DateSearch extends NegatableSearchQuery { - type: SearchQueryTypes.date; - after?: number; - before?: number; +export interface RangeSearch extends NegatableSearchQuery { + value: number; } -export interface RatingSearch extends RangeSearchQuery, NegatableSearchQuery { - type: SearchQueryTypes.rating; - min?: number; - max?: number; +export interface RangeSearchGroup extends ANDSearchQuery { + list: RangeSearch[]; } -export interface ResolutionSearch extends RangeSearchQuery, NegatableSearchQuery { - type: SearchQueryTypes.resolution; - min?: number; // in megapixels - max?: number; // in megapixels +export interface FromDateSearch extends RangeSearch { + type: SearchQueryTypes.from_date; + value: number; +} + +export interface ToDateSearch extends RangeSearch { + type: SearchQueryTypes.to_date; + value: number; +} + +export interface MinRatingSearch extends RangeSearch { + type: SearchQueryTypes.min_rating; + value: number; +} + +export interface MaxRatingSearch extends RangeSearch { + type: SearchQueryTypes.max_rating; + value: number; +} + +export interface MinResolutionSearch extends RangeSearch { + type: SearchQueryTypes.min_resolution; + value: number; // in megapixels +} + +export interface MaxResolutionSearch extends RangeSearch { + type: SearchQueryTypes.max_resolution; + value: number; // in megapixels } export interface OrientationSearch { diff --git a/src/frontend/app/ui/gallery/search/query-enrty/query-entry.search.gallery.component.html b/src/frontend/app/ui/gallery/search/query-enrty/query-entry.search.gallery.component.html index b04c690b..7cdf0b52 100644 --- a/src/frontend/app/ui/gallery/search/query-enrty/query-entry.search.gallery.component.html +++ b/src/frontend/app/ui/gallery/search/query-enrty/query-entry.search.gallery.component.html @@ -1,6 +1,6 @@
-
+
-
+
-
+
-
-
- - -
-
- - + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+ Mpx
-
-
- - -
-
- - +
+ + +
+ Mpx
-
- -
- - -
- Mpx -
-
- -
- - -
- Mpx -
-
-
-
+
- - +
diff --git a/src/frontend/app/ui/gallery/search/query-enrty/query-entry.search.gallery.component.ts b/src/frontend/app/ui/gallery/search/query-enrty/query-entry.search.gallery.component.ts index 21a4202d..9dd5363c 100644 --- a/src/frontend/app/ui/gallery/search/query-enrty/query-entry.search.gallery.component.ts +++ b/src/frontend/app/ui/gallery/search/query-enrty/query-entry.search.gallery.component.ts @@ -1,11 +1,10 @@ import {Component, EventEmitter, forwardRef, OnChanges, Output} from '@angular/core'; import { - DateSearch, DistanceSearch, ListSearchQueryTypes, OrientationSearch, - RatingSearch, - ResolutionSearch, + RangeSearch, + RangeSearchQueryTypes, SearchListQuery, SearchQueryDTO, SearchQueryTypes, @@ -42,6 +41,11 @@ export class GallerySearchQueryEntryComponent implements ControlValueAccessor, V constructor() { this.SearchQueryTypesEnum = Utils.enumToArray(SearchQueryTypes); + // Range queries need to be added as AND with min and max sub entry + this.SearchQueryTypesEnum.filter(e => !RangeSearchQueryTypes.includes(e.key)); + this.SearchQueryTypesEnum.push({value: 'Date', key: SearchQueryTypes.AND}); + this.SearchQueryTypesEnum.push({value: 'Rating', key: SearchQueryTypes.AND}); + this.SearchQueryTypesEnum.push({value: 'Resolution', key: SearchQueryTypes.AND}); } @@ -49,6 +53,7 @@ export class GallerySearchQueryEntryComponent implements ControlValueAccessor, V return this.queryEntry && TextSearchQueryTypes.includes(this.queryEntry.type); } + get IsListQuery(): boolean { return this.queryEntry && ListSearchQueryTypes.includes(this.queryEntry.type); } @@ -57,13 +62,10 @@ export class GallerySearchQueryEntryComponent implements ControlValueAccessor, V return this.queryEntry; } - get AsDateQuery(): DateSearch { + get AsRangeQuery(): RangeSearch { return this.queryEntry; } - get AsResolutionQuery(): ResolutionSearch { - return this.queryEntry; - } get AsOrientationQuery(): OrientationSearch { return this.queryEntry; @@ -73,9 +75,6 @@ export class GallerySearchQueryEntryComponent implements ControlValueAccessor, V return this.queryEntry; } - get AsRatingQuery(): RatingSearch { - return this.queryEntry; - } get AsSomeOfQuery(): SomeOfSearchQuery { return this.queryEntry; @@ -132,30 +131,35 @@ export class GallerySearchQueryEntryComponent implements ControlValueAccessor, V } ngOnChanges(): void { - // console.log('ngOnChanges', this.queryEntry); + // 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); + // console.log('write value', this.queryEntry); this.ngOnChanges(); } - public registerOnChange(fn: any): void { - // console.log('registerOnChange', fn); - this.onChange = fn; + registerOnChange(fn: (_: any) => void): void { + this.propagateChange = fn; } - public registerOnTouched(fn: any): void { - this.onTouched = fn; + registerOnTouched(fn: () => void): void { + this.propagateTouch = fn; } + + public onChange(event: any) { + this.propagateChange(this.queryEntry); + } + + private propagateChange = (_: any) => { + }; + + private propagateTouch = (_: any) => { + }; } diff --git a/src/frontend/app/ui/gallery/search/search.gallery.component.html b/src/frontend/app/ui/gallery/search/search.gallery.component.html index 8d5c0c3a..fa9f693d 100644 --- a/src/frontend/app/ui/gallery/search/search.gallery.component.html +++ b/src/frontend/app/ui/gallery/search/search.gallery.component.html @@ -8,6 +8,7 @@ (blur)="onFocusLost()" (focus)="onFocus()" [(ngModel)]="rawSearchText" + (ngModelChange)="validateRawSearchText()" #name="ngModel" size="30" ngControl="search" @@ -63,8 +64,8 @@ class="form-control" i18n-placeholder placeholder="Search" - disabled - [ngModel]="rawSearchText" + [(ngModel)]="rawSearchText" + (ngModelChange)="validateRawSearchText()" size="30" name="srch-term-preview" id="srch-term-preview" @@ -72,6 +73,7 @@ diff --git a/src/frontend/app/ui/gallery/search/search.gallery.component.ts b/src/frontend/app/ui/gallery/search/search.gallery.component.ts index 4f0e7532..8e488499 100644 --- a/src/frontend/app/ui/gallery/search/search.gallery.component.ts +++ b/src/frontend/app/ui/gallery/search/search.gallery.component.ts @@ -21,6 +21,7 @@ export class GallerySearchComponent implements OnDestroy { autoCompleteItems: AutoCompleteRenderItem[] = []; public searchQueryDTO: SearchQueryDTO = {type: SearchQueryTypes.any_text, text: ''}; + public rawSearchText: string; mouseOverAutoComplete = false; readonly SearchQueryTypes: typeof SearchQueryTypes; modalRef: BsModalRef; @@ -48,12 +49,6 @@ export class GallerySearchComponent implements OnDestroy { }); } - public get rawSearchText(): string { - return SearchQueryDTO.stringify(this.searchQueryDTO); - } - - public set rawSearchText(val: string) { - } get HTMLSearchQuery() { const searchQuery: any = {}; @@ -81,7 +76,6 @@ export class GallerySearchComponent implements OnDestroy { } - public setMouseOverAutoComplete(value: boolean) { this.mouseOverAutoComplete = value; } @@ -111,6 +105,18 @@ export class GallerySearchComponent implements OnDestroy { this.searchQueryDTO = {text: '', type: SearchQueryTypes.any_text}; } + onQueryChange() { + this.rawSearchText = SearchQueryDTO.stringify(this.searchQueryDTO); + } + + validateRawSearchText() { + try { + this.searchQueryDTO = SearchQueryDTO.parse(this.rawSearchText); + console.log(this.searchQueryDTO); + } catch (e) { + console.error(e); + } + } private emptyAutoComplete() { this.autoCompleteItems = []; diff --git a/test/backend/unit/model/sql/SearchManager.ts b/test/backend/unit/model/sql/SearchManager.ts index 14bac8d8..c003220e 100644 --- a/test/backend/unit/model/sql/SearchManager.ts +++ b/test/backend/unit/model/sql/SearchManager.ts @@ -5,17 +5,20 @@ import {Utils} from '../../../../../src/common/Utils'; import {SQLTestHelper} from '../../../SQLTestHelper'; import { ANDSearchQuery, - DateSearch, DistanceSearch, + FromDateSearch, + MaxRatingSearch, + MaxResolutionSearch, + MinRatingSearch, + MinResolutionSearch, OrientationSearch, ORSearchQuery, - RatingSearch, - ResolutionSearch, SearchQueryDTO, SearchQueryTypes, SomeOfSearchQuery, TextSearch, - TextSearchQueryMatchTypes + TextSearchQueryMatchTypes, + ToDateSearch } from '../../../../../src/common/entities/SearchQueryDTO'; import {IndexingManager} from '../../../../../src/backend/model/database/sql/IndexingManager'; import {DirectoryDTO} from '../../../../../src/common/entities/DirectoryDTO'; @@ -789,7 +792,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { it('should search date', async () => { const sm = new SearchManager(); - let query = {before: 0, after: 0, type: SearchQueryTypes.date}; + let query: any = {value: 0, type: SearchQueryTypes.from_date}; expect(Utils.clone(await sm.search(query))) .to.deep.equalInAnyOrder(removeDir({ @@ -800,9 +803,8 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { resultOverflow: false })); - query = { - before: p.metadata.creationDate, - after: p.metadata.creationDate, type: SearchQueryTypes.date + query = { + value: p.metadata.creationDate, type: SearchQueryTypes.to_date }; expect(Utils.clone(await sm.search(query))) @@ -814,11 +816,10 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { resultOverflow: false })); - query = { - before: p.metadata.creationDate, - after: p.metadata.creationDate, + query = { + value: p.metadata.creationDate, negate: true, - type: SearchQueryTypes.date + type: SearchQueryTypes.from_date }; expect(Utils.clone(await sm.search(query))) @@ -830,15 +831,12 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { resultOverflow: false })); - query = { - before: p.metadata.creationDate + 1000000000, - after: 0, type: SearchQueryTypes.date + query = { + value: p.metadata.creationDate + 1000000000, + type: SearchQueryTypes.to_date }; - expect(Utils.clone(await sm.search({ - before: p.metadata.creationDate + 1000000000, - after: 0, type: SearchQueryTypes.date - }))) + expect(Utils.clone(await sm.search(query))) .to.deep.equalInAnyOrder(removeDir({ searchQuery: query, directories: [], @@ -853,7 +851,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { it('should search rating', async () => { const sm = new SearchManager(); - let query = {min: 0, max: 0, type: SearchQueryTypes.rating}; + let query: MinRatingSearch | MaxRatingSearch = {value: 0, type: SearchQueryTypes.min_rating}; expect(Utils.clone(await sm.search(query))) @@ -865,7 +863,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { resultOverflow: false })); - query = {min: 0, max: 5, type: SearchQueryTypes.rating}; + query = {value: 5, type: SearchQueryTypes.max_rating}; expect(Utils.clone(await sm.search(query))) .to.deep.equalInAnyOrder(removeDir({ searchQuery: query, @@ -875,7 +873,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { resultOverflow: false })); - query = {min: 0, max: 5, negate: true, type: SearchQueryTypes.rating}; + query = {value: 5, negate: true, type: SearchQueryTypes.max_rating}; expect(Utils.clone(await sm.search(query))) .to.deep.equalInAnyOrder(removeDir({ searchQuery: query, @@ -885,7 +883,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { resultOverflow: false })); - query = {min: 2, max: 2, type: SearchQueryTypes.rating}; + query = {value: 2, type: SearchQueryTypes.min_rating}; expect(Utils.clone(await sm.search(query))) .to.deep.equalInAnyOrder(removeDir({ searchQuery: query, @@ -895,7 +893,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { resultOverflow: false })); - query = {min: 2, max: 2, negate: true, type: SearchQueryTypes.rating}; + query = {value: 2, negate: true, type: SearchQueryTypes.min_rating}; expect(Utils.clone(await sm.search(query))) .to.deep.equalInAnyOrder(removeDir({ searchQuery: query, @@ -910,7 +908,9 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { it('should search resolution', async () => { const sm = new SearchManager(); - let query = {min: 0, max: 0, type: SearchQueryTypes.resolution}; + let query: MinResolutionSearch | MaxResolutionSearch = + {value: 0, type: SearchQueryTypes.min_resolution}; + expect(Utils.clone(await sm.search(query))) .to.deep.equalInAnyOrder(removeDir({ searchQuery: query, @@ -920,7 +920,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { resultOverflow: false })); - query = {max: 1, type: SearchQueryTypes.resolution}; + query = {value: 1, type: SearchQueryTypes.max_resolution}; expect(Utils.clone(await sm.search(query))) .to.deep.equalInAnyOrder(removeDir({ searchQuery: query, @@ -930,7 +930,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { resultOverflow: false })); - query = {min: 2, max: 3, type: SearchQueryTypes.resolution}; + query = {value: 2, type: SearchQueryTypes.min_resolution}; expect(Utils.clone(await sm.search(query))) .to.deep.equalInAnyOrder(removeDir({ searchQuery: query, @@ -940,7 +940,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { resultOverflow: false })); - query = {min: 2, max: 3, negate: true, type: SearchQueryTypes.resolution}; + query = {value: 3, negate: true, type: SearchQueryTypes.max_resolution}; expect(Utils.clone(await sm.search(query))) .to.deep.equalInAnyOrder(removeDir({ searchQuery: query, @@ -950,7 +950,7 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { resultOverflow: false })); - query = {min: 3, type: SearchQueryTypes.resolution}; + query = {value: 3, negate: true, type: SearchQueryTypes.min_resolution}; expect(Utils.clone(await sm.search(query))) .to.deep.equalInAnyOrder(removeDir({ searchQuery: query, @@ -1094,6 +1094,8 @@ describe('SearchManager', (sqlHelper: SQLTestHelper) => { text: 'xyz', type: SearchQueryTypes.keyword }; + + // tslint:disable-next-line expect(await sm.getRandomPhoto(query)).to.not.exist; query = { diff --git a/test/common/unit/SearchQueryDTO.ts b/test/common/unit/SearchQueryDTO.ts new file mode 100644 index 00000000..25d2f9f6 --- /dev/null +++ b/test/common/unit/SearchQueryDTO.ts @@ -0,0 +1,137 @@ +import {expect} from 'chai'; +import { + ANDSearchQuery, + DistanceSearch, + FromDateSearch, + MaxRatingSearch, + MaxResolutionSearch, + MinRatingSearch, + MinResolutionSearch, + OrientationSearch, + ORSearchQuery, + SearchQueryDTO, + SearchQueryTypes, + SomeOfSearchQuery, + TextSearch, + ToDateSearch +} from '../../../src/common/entities/SearchQueryDTO'; + +describe('SearchQueryDTO', () => { + + + const check = (query: SearchQueryDTO) => { + expect(SearchQueryDTO.parse(SearchQueryDTO.stringify(query))).to.deep.equals(query, SearchQueryDTO.stringify(query)); + + }; + + describe('should serialize and deserialize', () => { + it('Text search', () => { + check({type: SearchQueryTypes.any_text, text: 'test'}); + check({type: SearchQueryTypes.person, text: 'person_test'}); + check({type: SearchQueryTypes.directory, text: 'directory'}); + check({type: SearchQueryTypes.keyword, text: 'big boom'}); + check({type: SearchQueryTypes.caption, text: 'caption'}); + check({type: SearchQueryTypes.file_name, text: 'filename'}); + check({type: SearchQueryTypes.position, text: 'New York'}); + }); + + it('Date search', () => { + check({type: SearchQueryTypes.from_date, value: (new Date(2020, 1, 1)).getTime()}); + check({type: SearchQueryTypes.to_date, value: (new Date(2020, 1, 2)).getTime()}); + }); + it('Rating search', () => { + check({type: SearchQueryTypes.min_rating, value: 10}); + check({type: SearchQueryTypes.max_rating, value: 1}); + }); + it('Resolution search', () => { + check({type: SearchQueryTypes.min_resolution, value: 10}); + check({type: SearchQueryTypes.max_resolution, value: 5}); + }); + it('Distance search', () => { + check({type: SearchQueryTypes.distance, distance: 10, from: {text: 'New York'}}); + }); + it('OrientationSearch search', () => { + check({type: SearchQueryTypes.orientation, landscape: true}); + check({type: SearchQueryTypes.orientation, landscape: false}); + }); + it('And search', () => { + check({ + type: SearchQueryTypes.AND, + list: [ + {type: SearchQueryTypes.keyword, text: 'big boom'}, + {type: SearchQueryTypes.position, text: 'New York'} + ] + }); + check({ + type: SearchQueryTypes.AND, + list: [ + {type: SearchQueryTypes.keyword, text: 'big boom'}, + { + type: SearchQueryTypes.AND, + list: [ + {type: SearchQueryTypes.caption, text: 'caption'}, + {type: SearchQueryTypes.position, text: 'New York'} + ] + } + ] + }); + }); + it('Or search', () => { + check({ + type: SearchQueryTypes.OR, + list: [ + {type: SearchQueryTypes.keyword, text: 'big boom'}, + {type: SearchQueryTypes.position, text: 'New York'} + ] + }); + check({ + type: SearchQueryTypes.OR, + list: [ + { + type: SearchQueryTypes.OR, + list: [ + {type: SearchQueryTypes.keyword, text: 'big boom'}, + {type: SearchQueryTypes.person, text: 'person_test'} + ] + }, + {type: SearchQueryTypes.position, text: 'New York'} + ] + }); + }); + it('Some of search', () => { + check({ + type: SearchQueryTypes.SOME_OF, + list: [ + {type: SearchQueryTypes.keyword, text: 'big boom'}, + {type: SearchQueryTypes.position, text: 'New York'} + ] + }); + check({ + type: SearchQueryTypes.SOME_OF, + min: 2, + list: [ + {type: SearchQueryTypes.keyword, text: 'big boom'}, + {type: SearchQueryTypes.position, text: 'New York'}, + {type: SearchQueryTypes.caption, text: 'caption test'} + ] + }); + check({ + type: SearchQueryTypes.SOME_OF, + min: 2, + list: [ + {type: SearchQueryTypes.keyword, text: 'big boom'}, + {type: SearchQueryTypes.person, text: 'person_test'}, + { + type: SearchQueryTypes.AND, + list: [ + {type: SearchQueryTypes.caption, text: 'caption'}, + {type: SearchQueryTypes.position, text: 'New York'} + ] + } + ] + }); + }); + }); + + +});