From d89800b69abfa7f101a6d4deb33768301a290581 Mon Sep 17 00:00:00 2001 From: Braun Patrik Date: Mon, 16 May 2016 11:03:11 +0200 Subject: [PATCH] implementing typed search --- backend/middlewares/GalleryMWs.ts | 13 ++- backend/model/ISearchManager.ts | 8 +- backend/model/memory/SearchManager.ts | 2 +- backend/model/mongoose/MongoSearchManager.ts | 82 ++++++++++++------- backend/routes/GalleryRouter.ts | 4 +- backend/routes/PublicRouter.ts | 2 +- common/entities/AutoCompleteItem.ts | 4 +- common/entities/SearchResult.ts | 10 +-- frontend/app/app.component.ts | 5 ++ frontend/app/gallery/gallery.component.html | 13 ++- frontend/app/gallery/gallery.component.ts | 30 +++++-- frontend/app/gallery/gallery.service.ts | 11 ++- .../photo/photo.grid.gallery.component.html | 8 +- .../photo/photo.grid.gallery.component.ts | 17 +++- .../search/search.gallery.component.html | 7 +- .../search/search.gallery.component.ts | 32 ++++++-- frontend/app/model/network/network.service.ts | 2 +- 17 files changed, 177 insertions(+), 73 deletions(-) diff --git a/backend/middlewares/GalleryMWs.ts b/backend/middlewares/GalleryMWs.ts index 9ec1b2a0..ad4ee213 100644 --- a/backend/middlewares/GalleryMWs.ts +++ b/backend/middlewares/GalleryMWs.ts @@ -4,7 +4,7 @@ import {NextFunction, Request, Response} from "express"; import {Error, ErrorCodes} from "../../common/entities/Error"; import {Directory} from "../../common/entities/Directory"; import {ObjectManagerRepository} from "../model/ObjectManagerRepository"; -import {AutoCompleteItem} from "../../common/entities/AutoCompleteItem"; +import {AutoCompleteItem, SearchTypes} from "../../common/entities/AutoCompleteItem"; import {ContentWrapper} from "../../common/entities/ConentWrapper"; import {SearchResult} from "../../common/entities/SearchResult"; import {Photo} from "../../common/entities/Photo"; @@ -70,8 +70,14 @@ export class GalleryMWs { if (!(req.params.text)) { return next(); } - - ObjectManagerRepository.getInstance().getSearchManager().search(req.params.text, (err, result:SearchResult) => { + + let type:SearchTypes; + console.log() + if (req.query.type) { + type = parseInt(req.query.type); + } + + ObjectManagerRepository.getInstance().getSearchManager().search(req.params.text, type, (err, result:SearchResult) => { if (err || !result) { return next(new Error(ErrorCodes.GENERAL_ERROR, err)); } @@ -90,6 +96,7 @@ export class GalleryMWs { return next(); } + ObjectManagerRepository.getInstance().getSearchManager().instantSearch(req.params.text, (err, result:SearchResult) => { if (err || !result) { return next(new Error(ErrorCodes.GENERAL_ERROR, err)); diff --git a/backend/model/ISearchManager.ts b/backend/model/ISearchManager.ts index 0ff7ab7b..017c6dc3 100644 --- a/backend/model/ISearchManager.ts +++ b/backend/model/ISearchManager.ts @@ -1,7 +1,7 @@ -import {AutoCompleteItem} from "../../common/entities/AutoCompleteItem"; +import {AutoCompleteItem, SearchTypes} from "../../common/entities/AutoCompleteItem"; import {SearchResult} from "../../common/entities/SearchResult"; export interface ISearchManager { - autocomplete(text, cb:(error:any, result:Array) => void); - search(text, cb:(error:any, result:SearchResult) => void); - instantSearch(text, cb:(error:any, result:SearchResult) => void); + autocomplete(text:string, cb:(error:any, result:Array) => void); + search(text:string, searchType:SearchTypes, cb:(error:any, result:SearchResult) => void); + instantSearch(text:string, cb:(error:any, result:SearchResult) => void); } \ No newline at end of file diff --git a/backend/model/memory/SearchManager.ts b/backend/model/memory/SearchManager.ts index 792d66a6..5d20d367 100644 --- a/backend/model/memory/SearchManager.ts +++ b/backend/model/memory/SearchManager.ts @@ -9,7 +9,7 @@ export class SearchManager implements ISearchManager { throw new Error("not implemented"); } - search(text, cb:(error:any, result:SearchResult) => void) { + search(text, searchType:SearchTypes, cb:(error:any, result:SearchResult) => void) { throw new Error("not implemented"); } diff --git a/backend/model/mongoose/MongoSearchManager.ts b/backend/model/mongoose/MongoSearchManager.ts index 0b973524..376afe6a 100644 --- a/backend/model/mongoose/MongoSearchManager.ts +++ b/backend/model/mongoose/MongoSearchManager.ts @@ -1,4 +1,4 @@ -import {AutoCompleteItem, AutoCompeleteTypes} from "../../../common/entities/AutoCompleteItem"; +import {AutoCompleteItem, SearchTypes} from "../../../common/entities/AutoCompleteItem"; import {ISearchManager} from "../ISearchManager"; import {DirectoryModel} from "./entities/DirectoryModel"; import {PhotoModel} from "./entities/PhotoModel"; @@ -18,25 +18,25 @@ export class MongoSearchManager implements ISearchManager { promises.push( PhotoModel.find({name: {$regex: text, $options: "i"}}) .limit(10).select('name').exec().then((res:Array)=> { - items = items.concat(this.encapsulateAutoComplete(res.map(r => r.name), AutoCompeleteTypes.image)); + items = items.concat(this.encapsulateAutoComplete(res.map(r => r.name), SearchTypes.image)); })); promises.push( PhotoModel.find({"metadata.positionData.city": {$regex: text, $options: "i"}}) .limit(10).select('metadata.positionData.city').exec().then((res:Array)=> { - items = items.concat(this.encapsulateAutoComplete(res.map(r => r.metadata.positionData.city), AutoCompeleteTypes.position)); + items = items.concat(this.encapsulateAutoComplete(res.map(r => r.metadata.positionData.city), SearchTypes.position)); })); promises.push( PhotoModel.find({"metadata.positionData.state": {$regex: text, $options: "i"}}) .limit(10).select('metadata.positionData.state').exec().then((res:Array)=> { - items = items.concat(this.encapsulateAutoComplete(res.map(r => r.metadata.positionData.state), AutoCompeleteTypes.position)); + items = items.concat(this.encapsulateAutoComplete(res.map(r => r.metadata.positionData.state), SearchTypes.position)); })); promises.push( PhotoModel.find({"metadata.positionData.country": {$regex: text, $options: "i"}}) .limit(10).select('metadata.positionData.country').exec().then((res:Array)=> { - items = items.concat(this.encapsulateAutoComplete(res.map(r => r.metadata.positionData.country), AutoCompeleteTypes.position)); + items = items.concat(this.encapsulateAutoComplete(res.map(r => r.metadata.positionData.country), SearchTypes.position)); })); //TODO: fix caseinsensitivity @@ -44,7 +44,7 @@ export class MongoSearchManager implements ISearchManager { PhotoModel.find({"metadata.keywords": {$regex: text, $options: "i"}}) .limit(10).select('metadata.keywords').exec().then((res:Array)=> { res.forEach((photo)=> { - items = items.concat(this.encapsulateAutoComplete(photo.metadata.keywords.filter(k => k.indexOf(text) != -1), AutoCompeleteTypes.keyword)); + items = items.concat(this.encapsulateAutoComplete(photo.metadata.keywords.filter(k => k.indexOf(text) != -1), SearchTypes.keyword)); }); })); @@ -52,7 +52,7 @@ export class MongoSearchManager implements ISearchManager { DirectoryModel.find({ name: {$regex: text, $options: "i"} }).limit(10).select('name').exec().then((res:Array)=> { - items = items.concat(this.encapsulateAutoComplete(res.map(r => r.name), AutoCompeleteTypes.directory)); + items = items.concat(this.encapsulateAutoComplete(res.map(r => r.name), SearchTypes.directory)); })); @@ -66,39 +66,59 @@ export class MongoSearchManager implements ISearchManager { } - search(text, cb:(error:any, result:SearchResult) => void) { - console.log("instantSearch: " + text); + search(text:string, searchType:SearchTypes, cb:(error:any, result:SearchResult) => void) { + console.log("search: " + text + ", type:" + searchType); let result:SearchResult = new SearchResult(); + + let promises = []; + let photoFilterOR = []; + result.searchText = text; - PhotoModel.find({ - $or: [ - {name: {$regex: text, $options: "i"}}, - {"metadata.positionData.city": {$regex: text, $options: "i"}}, - {"metadata.positionData.state": {$regex: text, $options: "i"}}, - {"metadata.positionData.country": {$regex: text, $options: "i"}}, - {"metadata.keywords": {$regex: text, $options: "i"}} - ] + result.searchType = searchType; - }).populate('directory', 'name path').exec((err, res:Array) => { - if (err || !res) { - return cb(err, null); - } - result.photos = res; - DirectoryModel.find({ + if (!searchType || searchType === SearchTypes.image) { + photoFilterOR.push({name: {$regex: text, $options: "i"}}); + } + if (!searchType || searchType === SearchTypes.position) { + photoFilterOR.push({"metadata.positionData.city": {$regex: text, $options: "i"}}); + photoFilterOR.push({"metadata.positionData.state": {$regex: text, $options: "i"}}); + photoFilterOR.push({"metadata.positionData.country": {$regex: text, $options: "i"}}); + } + if (!searchType || searchType === SearchTypes.keyword) { + photoFilterOR.push({"metadata.keywords": {$regex: text, $options: "i"}}); + } + + let photoFilter = {}; + if (photoFilterOR.length == 1) { + photoFilter = photoFilterOR[0]; + } else { + photoFilter = {$or: photoFilterOR}; + } + + if (!searchType || photoFilterOR.length > 0) { + promises.push(PhotoModel.find(photoFilter).populate('directory', 'name path').exec().then((res:Array) => { + result.photos = res; + })); + } + + + if (!searchType || searchType === SearchTypes.directory) { + promises.push(DirectoryModel.find({ name: { $regex: text, $options: "i" } - }).select('name').exec((err, res:Array) => { - if (err || !res) { - return cb(err, null); - } + }).exec().then((res:Array) => { result.directories = res; - return cb(null, result); - }); - + })); + } + Promise.all(promises).then(()=> { + return cb(null, result); + }).catch((err)=> { + console.error(err); + return cb(err, null); }); } @@ -138,7 +158,7 @@ export class MongoSearchManager implements ISearchManager { }); } - private encapsulateAutoComplete(values:Array, type:AutoCompeleteTypes) { + private encapsulateAutoComplete(values:Array, type:SearchTypes) { let res = []; values.forEach((value)=> { res.push(new AutoCompleteItem(value, type)); diff --git a/backend/routes/GalleryRouter.ts b/backend/routes/GalleryRouter.ts index f36ae89d..5aa578d6 100644 --- a/backend/routes/GalleryRouter.ts +++ b/backend/routes/GalleryRouter.ts @@ -45,7 +45,7 @@ export class GalleryRouter { private addSearch() { this.app.get("/api/gallery/search/:text", - AuthenticationMWs.authenticate, + // AuthenticationMWs.authenticate, GalleryMWs.search, RenderingMWs.renderResult ); @@ -61,7 +61,7 @@ export class GalleryRouter { private addAutoComplete() { this.app.get("/api/gallery/autocomplete/:text", - // AuthenticationMWs.authenticate, + AuthenticationMWs.authenticate, GalleryMWs.autocomplete, RenderingMWs.renderResult ); diff --git a/backend/routes/PublicRouter.ts b/backend/routes/PublicRouter.ts index aeb5cb22..406e0941 100644 --- a/backend/routes/PublicRouter.ts +++ b/backend/routes/PublicRouter.ts @@ -29,7 +29,7 @@ export class PublicRouter { res.render(_path.resolve(__dirname, './../../frontend/index.ejs'), res.tpl); }; - this.app.get(['/', '/login', "/gallery*", "/admin"], renderIndex); + this.app.get(['/', '/login', "/gallery*", "/admin", "/search*"], renderIndex); } diff --git a/common/entities/AutoCompleteItem.ts b/common/entities/AutoCompleteItem.ts index 0b36f9ca..91ee2200 100644 --- a/common/entities/AutoCompleteItem.ts +++ b/common/entities/AutoCompleteItem.ts @@ -1,4 +1,4 @@ -export enum AutoCompeleteTypes { +export enum SearchTypes { image, directory, keyword, @@ -6,7 +6,7 @@ export enum AutoCompeleteTypes { } export class AutoCompleteItem { - constructor(public text:string, public type:AutoCompeleteTypes) { + constructor(public text:string, public type:SearchTypes) { } equals(other:AutoCompleteItem) { diff --git a/common/entities/SearchResult.ts b/common/entities/SearchResult.ts index 1bdf4378..764f75b1 100644 --- a/common/entities/SearchResult.ts +++ b/common/entities/SearchResult.ts @@ -1,9 +1,9 @@ import {Directory} from "./Directory"; import {Photo} from "./Photo"; +import {SearchTypes} from "./AutoCompleteItem"; export class SearchResult { - - public searchText:string; - public directories:Array; - public photos:Array; - + public searchText:string = ""; + public searchType:SearchTypes; + public directories:Array = []; + public photos:Array = []; } \ No newline at end of file diff --git a/frontend/app/app.component.ts b/frontend/app/app.component.ts index f93a1d40..4f464b33 100644 --- a/frontend/app/app.component.ts +++ b/frontend/app/app.component.ts @@ -50,6 +50,11 @@ import {NetworkService} from "./model/network/network.service"; name: 'Gallery', component: GalleryComponent }, + { + path: '/search/:searchText', + name: 'Search', + component: GalleryComponent + }, ]) export class AppComponent implements OnInit { diff --git a/frontend/app/gallery/gallery.component.html b/frontend/app/gallery/gallery.component.html index 65bf85a9..1e058207 100644 --- a/frontend/app/gallery/gallery.component.html +++ b/frontend/app/gallery/gallery.component.html @@ -1,7 +1,7 @@
- +
diff --git a/frontend/app/gallery/gallery.component.ts b/frontend/app/gallery/gallery.component.ts index 0f5e9e5b..bced5bfb 100644 --- a/frontend/app/gallery/gallery.component.ts +++ b/frontend/app/gallery/gallery.component.ts @@ -1,6 +1,6 @@ /// -import {Component, OnInit} from "@angular/core"; +import {Component, OnInit, ViewChild} from "@angular/core"; import {AuthenticationService} from "../model/network/authentication.service.ts"; import {Router, RouteParams} from "@angular/router-deprecated"; import {GalleryService} from "./gallery.service"; @@ -10,6 +10,7 @@ import {FrameComponent} from "../frame/frame.component"; import {GalleryLightboxComponent} from "./lightbox/lightbox.gallery.component"; import {GallerySearchComponent} from "./search/search.gallery.component"; import {Config} from "../config/Config"; +import {SearchTypes} from "../../../common/entities/AutoCompleteItem"; @Component({ selector: 'gallery', @@ -23,9 +24,10 @@ import {Config} from "../config/Config"; }) export class GalleryComponent implements OnInit { + @ViewChild(GallerySearchComponent) search:GallerySearchComponent; public showSearchBar:boolean = true; - + constructor(private _galleryService:GalleryService, private _params:RouteParams, private _authService:AuthenticationService, @@ -40,12 +42,30 @@ export class GalleryComponent implements OnInit { return; } - let directoryName = this._params.get('directory'); console.log(this._params); - console.log(directoryName); + + let searchText = this._params.get('searchText'); + if (searchText && searchText != "") { + console.log("searching"); + let typeString = this._params.get('type'); + + if (typeString && typeString != "") { + console.log("with type"); + let type:SearchTypes = SearchTypes[typeString]; + this._galleryService.search(searchText, type); + return; + } + + this._galleryService.search(searchText); + return; + } + + + let directoryName = this._params.get('directory'); directoryName = directoryName ? directoryName : ""; - this._galleryService.getDirectory(directoryName); + + } diff --git a/frontend/app/gallery/gallery.service.ts b/frontend/app/gallery/gallery.service.ts index 7c1240a3..7c546b6a 100644 --- a/frontend/app/gallery/gallery.service.ts +++ b/frontend/app/gallery/gallery.service.ts @@ -6,6 +6,7 @@ import {Message} from "../../../common/entities/Message"; import {ContentWrapper} from "../../../common/entities/ConentWrapper"; import {Photo} from "../../../common/entities/Photo"; import {Directory} from "../../../common/entities/Directory"; +import {SearchTypes} from "../../../common/entities/AutoCompleteItem"; @Injectable() export class GalleryService { @@ -33,12 +34,18 @@ export class GalleryService { } //TODO: cache - public search(text:string):Promise> { + public search(text:string, type?:SearchTypes):Promise> { clearTimeout(this.searchId); if (text === null || text === '') { return Promise.resolve(new Message(null, null)); } - return this._networkService.getJson("/gallery/search/" + text).then( + + let queryString = "/gallery/search/" + text; + if (type) { + queryString += "?type=" + type; + } + + return this._networkService.getJson(queryString).then( (message:Message) => { if (!message.error && message.result) { this.content = message.result; diff --git a/frontend/app/gallery/grid/photo/photo.grid.gallery.component.html b/frontend/app/gallery/grid/photo/photo.grid.gallery.component.html index 5d1be512..570120f1 100644 --- a/frontend/app/gallery/grid/photo/photo.grid.gallery.component.html +++ b/frontend/app/gallery/grid/photo/photo.grid.gallery.component.html @@ -5,16 +5,16 @@
diff --git a/frontend/app/gallery/grid/photo/photo.grid.gallery.component.ts b/frontend/app/gallery/grid/photo/photo.grid.gallery.component.ts index 0e09a02d..53b8e428 100644 --- a/frontend/app/gallery/grid/photo/photo.grid.gallery.component.ts +++ b/frontend/app/gallery/grid/photo/photo.grid.gallery.component.ts @@ -3,11 +3,14 @@ import {Component, Input, ElementRef, ViewChild} from "@angular/core"; import {IRenderable, Dimension} from "../../../model/IRenderable"; import {GridPhoto} from "../GridPhoto"; +import {SearchTypes} from "../../../../../common/entities/AutoCompleteItem"; +import {RouterLink} from "@angular/router-deprecated"; @Component({ selector: 'gallery-grid-photo', templateUrl: 'app/gallery/grid/photo/photo.grid.gallery.component.html', styleUrls: ['app/gallery/grid/photo/photo.grid.gallery.component.css'], + directives: [RouterLink], }) export class GalleryPhotoComponent implements IRenderable { @Input() gridPhoto:GridPhoto; @@ -18,15 +21,25 @@ export class GalleryPhotoComponent implements IRenderable { height: 0, background: "" }; + SearchTypes:any = []; constructor() { + this.SearchTypes = SearchTypes; } + getPositionText():string { + if (!this.gridPhoto) { + return "" + } + return this.gridPhoto.photo.metadata.positionData.city || + this.gridPhoto.photo.metadata.positionData.state || + this.gridPhoto.photo.metadata.positionData.country; + } hover() { this.infoStyle.height = this.infoDiv.nativeElement.clientHeight; this.infoStyle.background = "rgba(0,0,0,0.8)"; - + } mouseOut() { @@ -34,7 +47,7 @@ export class GalleryPhotoComponent implements IRenderable { this.infoStyle.background = "rgba(0,0,0,0.0)"; } - + public getDimension():Dimension { return new Dimension(this.imageRef.nativeElement.offsetTop, this.imageRef.nativeElement.offsetLeft, diff --git a/frontend/app/gallery/search/search.gallery.component.html b/frontend/app/gallery/search/search.gallery.component.html index 37e856d9..a34899cb 100644 --- a/frontend/app/gallery/search/search.gallery.component.html +++ b/frontend/app/gallery/search/search.gallery.component.html @@ -6,8 +6,10 @@ ngControl="search" name="srch-term" id="srch-term" autocomplete="off"> -
-
+ diff --git a/frontend/app/gallery/search/search.gallery.component.ts b/frontend/app/gallery/search/search.gallery.component.ts index 6d7cae23..e99e2286 100644 --- a/frontend/app/gallery/search/search.gallery.component.ts +++ b/frontend/app/gallery/search/search.gallery.component.ts @@ -2,7 +2,8 @@ import {Component} from "@angular/core"; import {AutoCompleteService} from "./autocomplete.service"; -import {AutoCompleteItem, AutoCompeleteTypes} from "../../../../common/entities/AutoCompleteItem"; +import {AutoCompleteItem, SearchTypes} from "../../../../common/entities/AutoCompleteItem"; +import {RouteParams, RouterLink} from "@angular/router-deprecated"; import {Message} from "../../../../common/entities/Message"; import {GalleryService} from "../gallery.service"; import {FORM_DIRECTIVES} from "@angular/common"; @@ -13,14 +14,22 @@ import {Config} from "../../config/Config"; templateUrl: 'app/gallery/search/search.gallery.component.html', styleUrls: ['app/gallery/search/search.gallery.component.css'], providers: [AutoCompleteService], - directives: [FORM_DIRECTIVES] + directives: [FORM_DIRECTIVES, RouterLink] }) export class GallerySearchComponent { autoCompleteItems:Array = []; private searchText:string = ""; - constructor(private _autoCompleteService:AutoCompleteService, private _galleryService:GalleryService) { + SearchTypes:any = []; + + constructor(private _autoCompleteService:AutoCompleteService, private _galleryService:GalleryService, private _params:RouteParams) { + + this.SearchTypes = SearchTypes; + let searchText = this._params.get('searchText'); + if (searchText && searchText != "") { + this.searchText = searchText; + } } onSearchChange(event:KeyboardEvent) { @@ -49,9 +58,16 @@ export class GallerySearchComponent { } + mouseOverAutoComplete:boolean = false; + public setMouseOverAutoComplete(value) { + this.mouseOverAutoComplete = value; + } + public onFocusLost(event) { - this.autoCompleteItems = []; + if (this.mouseOverAutoComplete == false) { + this.autoCompleteItems = []; + } } public onFocus(event) { @@ -88,15 +104,19 @@ export class GallerySearchComponent { }); } + public setSearchText(searchText:string) { + this.searchText = searchText; + } + } class AutoCompleteRenderItem { public preText:string = ""; public highLightText:string = ""; public postText:string = ""; - public type:AutoCompeleteTypes; + public type:SearchTypes; - constructor(public text:string, searchText:string, type:AutoCompeleteTypes) { + constructor(public text:string, searchText:string, type:SearchTypes) { let preIndex = text.toLowerCase().indexOf(searchText.toLowerCase()); if (preIndex > -1) { this.preText = text.substring(0, preIndex); diff --git a/frontend/app/model/network/network.service.ts b/frontend/app/model/network/network.service.ts index e98c3e01..3d7eb706 100644 --- a/frontend/app/model/network/network.service.ts +++ b/frontend/app/model/network/network.service.ts @@ -23,7 +23,7 @@ export class NetworkService { .toPromise() .then(res => > res.json()) .catch(NetworkService.handleError); - } + } return this._http[method](this._baseUrl + url, body, options) .toPromise()