1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2024-12-25 02:04:15 +02:00

implementing typed search

This commit is contained in:
Braun Patrik 2016-05-16 11:03:11 +02:00
parent 60dab77cf4
commit d32914bc02
17 changed files with 178 additions and 74 deletions

View File

@ -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));

View File

@ -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<AutoCompleteItem>) => 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<AutoCompleteItem>) => void);
search(text:string, searchType:SearchTypes, cb:(error:any, result:SearchResult) => void);
instantSearch(text:string, cb:(error:any, result:SearchResult) => void);
}

View File

@ -1,4 +1,4 @@
import {AutoCompleteItem} from "../../../common/entities/AutoCompleteItem";
import {AutoCompleteItem, SearchTypes} from "../../../common/entities/AutoCompleteItem";
import {ISearchManager} from "../ISearchManager";
import {SearchResult} from "../../../common/entities/SearchResult";
@ -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");
}

View File

@ -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<any>)=> {
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<any>)=> {
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<any>)=> {
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<any>)=> {
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<any>)=> {
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<any>)=> {
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<any>) => {
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<any>) => {
result.photos = res;
}));
}
if (!searchType || searchType === SearchTypes.directory) {
promises.push(DirectoryModel.find({
name: {
$regex: text,
$options: "i"
}
}).select('name').exec((err, res:Array<any>) => {
if (err || !res) {
return cb(err, null);
}
}).exec().then((res:Array<any>) => {
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<string>, type:AutoCompeleteTypes) {
private encapsulateAutoComplete(values:Array<string>, type:SearchTypes) {
let res = [];
values.forEach((value)=> {
res.push(new AutoCompleteItem(value, type));

View File

@ -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
);

View File

@ -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);
}

View File

@ -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) {

View File

@ -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<Directory>;
public photos:Array<Photo>;
public searchText:string = "";
public searchType:SearchTypes;
public directories:Array<Directory> = [];
public photos:Array<Photo> = [];
}

View File

@ -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 {

View File

@ -1,7 +1,7 @@
<gallery-lightbox #lightbox></gallery-lightbox>
<app-frame>
<div navbar>
<gallery-search *ngIf="showSearchBar"></gallery-search>
<gallery-search #search *ngIf="showSearchBar"></gallery-search>
</div>
<div body class="container" style="width: 100%; padding:0" *ngIf="_galleryService.content.directory">
<gallery-directory *ngFor="let directory of _galleryService.content.directory.directories"
@ -11,7 +11,16 @@
<div body class="container" style="width: 100%; padding:0" *ngIf="_galleryService.content.searchResult">
<ol class="breadcrumb">
<li class="active"> Searching for: <strong>{{_galleryService.content.searchResult.searchText}}</strong></li>
<li class="active">
Searching for:
<span [ngSwitch]="_galleryService.content.searchResult.searchType">
<span *ngSwitchWhen="0" class="glyphicon glyphicon-picture"></span>
<span *ngSwitchWhen="1" class="glyphicon glyphicon-folder-open"></span>
<span *ngSwitchWhen="2" class="glyphicon glyphicon-tag"></span>
<span *ngSwitchWhen="3" class="glyphicon glyphicon-map-marker"></span>
</span>
<strong>{{_galleryService.content.searchResult.searchText}}</strong>
</li>
</ol>
<div *ngFor="let directory of _galleryService.content.searchResult.directories">
<gallery-directory *ngIf="directory" [directory]="directory"></gallery-directory>

View File

@ -1,6 +1,6 @@
///<reference path="../../browser.d.ts"/>
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);
}

View File

@ -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<Message<ContentWrapper>> {
public search(text:string, type?:SearchTypes):Promise<Message<ContentWrapper>> {
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<ContentWrapper>) => {
if (!message.error && message.result) {
this.content = message.result;

View File

@ -5,16 +5,16 @@
<div class="photo-position" *ngIf="gridPhoto.photo.metadata.positionData">
<span class="glyphicon glyphicon-map-marker"></span>
<a *ngIf="gridPhoto.photo.metadata.positionData.city || gridPhoto.photo.metadata.positionData.state || gridPhoto.photo.metadata.positionData.country">
{{gridPhoto.photo.metadata.positionData.city || gridPhoto.photo.metadata.positionData.state ||
gridPhoto.photo.metadata.positionData.country}}
<a [routerLink]="['Search',{searchText: getPositionText(), type:SearchTypes[SearchTypes.position]}]"
*ngIf="getPositionText()">
{{getPositionText()}}
</a>
</div>
<div class="photo-keywords">
<template ngFor let-keyword [ngForOf]="gridPhoto.photo.metadata.keywords" let-last="last">
#<a>{{keyword}}</a>
<a [routerLink]="['Search',{searchText: keyword, type: SearchTypes[SearchTypes.keyword]}]">#{{keyword}}</a>
<template [ngIf]="!last">,</template>
</template>

View File

@ -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,

View File

@ -6,8 +6,10 @@
ngControl="search"
name="srch-term" id="srch-term" autocomplete="off">
<div class="autocomplete-list" *ngIf="autoCompleteItems.length > 0">
<div class="autocomplete-item" *ngFor="let item of autoCompleteItems" (mousedown)="search(item)">
<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',{searchText: item.text, type: SearchTypes[item.type]}]">
<span [ngSwitch]="item.type">
<span *ngSwitchWhen="0" class="glyphicon glyphicon-picture"></span>
<span *ngSwitchWhen="1" class="glyphicon glyphicon-folder-open"></span>
@ -15,6 +17,7 @@
<span *ngSwitchWhen="3" class="glyphicon glyphicon-map-marker"></span>
</span>
{{item.preText}}<strong>{{item.highLightText}}</strong>{{item.postText}}
</a>
</div>
</div>

View File

@ -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<AutoCompleteRenderItem> = [];
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);

View File

@ -23,7 +23,7 @@ export class NetworkService {
.toPromise()
.then(res => <Message<any>> res.json())
.catch(NetworkService.handleError);
}
}
return this._http[method](this._baseUrl + url, body, options)
.toPromise()