Quick-Search: Added Quick-search in the topbar
Changed how and what we store in elastic to unify it with how we store things in mongodb so we can have more generic javascript code to render the data. Elastic changes: Added: Node.project.url Altered to store id instead of url Node.picture Made Post searchable ./manage.py elastic reset_index ./manage.py elastic reindex Thanks to Pablo and Sybren
This commit is contained in:
58
src/scripts/js/es6/common/quicksearch/MultiSearch.js
Normal file
58
src/scripts/js/es6/common/quicksearch/MultiSearch.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import {SearchParams} from './SearchParams';
|
||||
|
||||
export class MultiSearch {
|
||||
constructor(kwargs) {
|
||||
this.uiUrl = kwargs['uiUrl']; // Url for advanced search
|
||||
this.apiUrl = kwargs['apiUrl']; // Url for api calls
|
||||
this.searchParams = MultiSearch.createMultiSearchParams(kwargs['searchParams']);
|
||||
this.q = '';
|
||||
}
|
||||
|
||||
setSearchWord(q) {
|
||||
this.q = q;
|
||||
this.searchParams.forEach((qsParam) => {
|
||||
qsParam.setSearchWord(q);
|
||||
});
|
||||
}
|
||||
|
||||
getSearchUrl() {
|
||||
return this.uiUrl + '?q=' + this.q;
|
||||
}
|
||||
|
||||
getAllParams() {
|
||||
let retval = $.map(this.searchParams, (msParams) => {
|
||||
return msParams.params;
|
||||
});
|
||||
return retval;
|
||||
}
|
||||
|
||||
parseResult(rawResult) {
|
||||
return $.map(rawResult, (subResult, index) => {
|
||||
let name = this.searchParams[index].name;
|
||||
let pStr = this.searchParams[index].getParamStr();
|
||||
let result = $.map(subResult.hits.hits, (hit) => {
|
||||
return hit._source;
|
||||
});
|
||||
return {
|
||||
name: name,
|
||||
url: this.uiUrl + '?' + pStr,
|
||||
result: result,
|
||||
hasResults: !!result.length
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
thenExecute() {
|
||||
let data = JSON.stringify(this.getAllParams());
|
||||
let rawAjax = $.getJSON(this.apiUrl, data);
|
||||
let prettyPromise = rawAjax.then(this.parseResult.bind(this));
|
||||
prettyPromise['abort'] = rawAjax.abort.bind(rawAjax); // Hack to be able to abort the promise down the road
|
||||
return prettyPromise;
|
||||
}
|
||||
|
||||
static createMultiSearchParams(argsList) {
|
||||
return $.map(argsList, (args) => {
|
||||
return new SearchParams(args);
|
||||
});
|
||||
}
|
||||
}
|
204
src/scripts/js/es6/common/quicksearch/QuickSearch.js
Normal file
204
src/scripts/js/es6/common/quicksearch/QuickSearch.js
Normal file
@@ -0,0 +1,204 @@
|
||||
import { create$noHits, create$results, create$input } from './templates'
|
||||
import {SearchFacade} from './SearchFacade';
|
||||
/**
|
||||
* QuickSearch : Interacts with the dom document
|
||||
* 1-SearchFacade : Controls which multisearch is active
|
||||
* *-MultiSearch : One multi search is typically Project or Cloud
|
||||
* *-SearchParams : The search params for the individual searches
|
||||
*/
|
||||
|
||||
export class QuickSearch {
|
||||
/**
|
||||
* Interacts with the dom document and deligates the input down to the SearchFacade
|
||||
* @param {selector string} searchToggle The quick-search toggle
|
||||
* @param {*} kwargs
|
||||
*/
|
||||
constructor(searchToggle, kwargs) {
|
||||
this.$body = $('body');
|
||||
this.$quickSearch = $('.quick-search');
|
||||
this.$inputComponent = $(kwargs['inputTarget']);
|
||||
this.$inputComponent.empty();
|
||||
this.$inputComponent.append(create$input(kwargs['searches']));
|
||||
this.$searchInput = this.$inputComponent.find('input');
|
||||
this.$searchSelect = this.$inputComponent.find('select');
|
||||
this.$resultTarget = $(kwargs['resultTarget']);
|
||||
this.$searchSymbol = this.$inputComponent.find('.qs-busy-symbol');
|
||||
this.searchFacade = new SearchFacade(kwargs['searches'] || {});
|
||||
this.$searchToggle = $(searchToggle);
|
||||
this.isBusy = false;
|
||||
this.attach();
|
||||
}
|
||||
|
||||
attach() {
|
||||
if (this.$searchSelect.length) {
|
||||
this.$searchSelect
|
||||
.change(this.execute.bind(this))
|
||||
.change(() => this.$searchInput.focus());
|
||||
this.$searchInput.addClass('multi-scope');
|
||||
}
|
||||
|
||||
this.$searchInput
|
||||
.keyup(this.onInputKeyUp.bind(this));
|
||||
|
||||
this.$inputComponent
|
||||
.on('pillar:workStart', () => {
|
||||
this.$searchSymbol.addClass('spinner')
|
||||
this.$searchSymbol.toggleClass('pi-spin pi-cancel')
|
||||
})
|
||||
.on('pillar:workStop', () => {
|
||||
this.$searchSymbol.removeClass('spinner')
|
||||
this.$searchSymbol.toggleClass('pi-spin pi-cancel')
|
||||
});
|
||||
|
||||
this.searchFacade.setOnResultCB(this.renderResult.bind(this));
|
||||
this.searchFacade.setOnFailureCB(this.onSearchFailed.bind(this));
|
||||
this.$searchToggle
|
||||
.one('click', this.execute.bind(this)); // Initial search executed once
|
||||
|
||||
this.registerShowGui();
|
||||
this.registerHideGui();
|
||||
}
|
||||
|
||||
registerShowGui() {
|
||||
this.$searchToggle
|
||||
.click((e) => {
|
||||
this.showGUI();
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
registerHideGui() {
|
||||
this.$searchSymbol
|
||||
.click(() => {
|
||||
this.hideGUI();
|
||||
});
|
||||
this.$body.click((e) => {
|
||||
let $target = $(e.target);
|
||||
let isClickInResult = $target.hasClass('.qs-result') || !!$target.parents('.qs-result').length;
|
||||
let isClickInInput = $target.hasClass('.qs-input') || !!$target.parents('.qs-input').length;
|
||||
if (!isClickInResult && !isClickInInput) {
|
||||
this.hideGUI();
|
||||
}
|
||||
});
|
||||
$(document).keyup((e) => {
|
||||
if (e.key === 'Escape') {
|
||||
this.hideGUI();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showGUI() {
|
||||
this.$body.addClass('has-overlay');
|
||||
this.$quickSearch.trigger('pillar:searchShow');
|
||||
this.$quickSearch.addClass('show');
|
||||
if (!this.$searchInput.is(':focus')) {
|
||||
this.$searchInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
hideGUI() {
|
||||
this.$body.removeClass('has-overlay');
|
||||
this.$searchToggle.addClass('pi-search');
|
||||
this.$searchInput.blur();
|
||||
this.$quickSearch.removeClass('show');
|
||||
this.$quickSearch.trigger('pillar:searchHidden');
|
||||
}
|
||||
|
||||
onInputKeyUp(e) {
|
||||
let newQ = this.$searchInput.val();
|
||||
let currQ = this.searchFacade.getSearchWord();
|
||||
this.searchFacade.setSearchWord(newQ);
|
||||
let searchUrl = this.searchFacade.getSearchUrl();
|
||||
if (e.key === 'Enter') {
|
||||
window.location.href = searchUrl;
|
||||
return;
|
||||
}
|
||||
if (newQ !== currQ) {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
|
||||
execute() {
|
||||
this.busy(true);
|
||||
let scope = this.getScope();
|
||||
this.searchFacade.setCurrentScope(scope);
|
||||
let q = this.$searchInput.val();
|
||||
this.searchFacade.setSearchWord(q);
|
||||
this.searchFacade.execute();
|
||||
}
|
||||
|
||||
renderResult(results) {
|
||||
this.$resultTarget.empty();
|
||||
this.$resultTarget.append(this.create$result(results));
|
||||
this.busy(false);
|
||||
}
|
||||
|
||||
create$result(results) {
|
||||
let withHits = results.reduce((aggr, subResult) => {
|
||||
if (subResult.hasResults) {
|
||||
aggr.push(subResult);
|
||||
}
|
||||
return aggr;
|
||||
}, []);
|
||||
|
||||
if (!withHits.length) {
|
||||
return create$noHits(this.searchFacade.getSearchUrl());
|
||||
}
|
||||
return create$results(results, this.searchFacade.getSearchUrl());
|
||||
}
|
||||
|
||||
onSearchFailed(err) {
|
||||
toastr.error(xhrErrorResponseMessage(err), 'Unable to perform search:');
|
||||
this.busy(false);
|
||||
this.$inputComponent.trigger('pillar:failed', err);
|
||||
}
|
||||
|
||||
getScope() {
|
||||
return !!this.$searchSelect.length ? this.$searchSelect.val() : 'cloud';
|
||||
}
|
||||
|
||||
busy(val) {
|
||||
if (val !== this.isBusy) {
|
||||
var eventType = val ? 'pillar:workStart' : 'pillar:workStop';
|
||||
this.$inputComponent.trigger(eventType);
|
||||
}
|
||||
this.isBusy = val;
|
||||
}
|
||||
}
|
||||
|
||||
$.fn.extend({
|
||||
/**
|
||||
* $('#qs-toggle').quickSearch({
|
||||
* resultTarget: '#search-overlay',
|
||||
* inputTarget: '#qs-input',
|
||||
* searches: {
|
||||
* project: {
|
||||
* name: 'Project',
|
||||
* uiUrl: '{{ url_for("projects.search", project_url=project.url)}}',
|
||||
* apiUrl: '/api/newsearch/multisearch',
|
||||
* searchParams: [
|
||||
* {name: 'Assets', params: {project: '{{ project._id }}', node_type: 'asset'}},
|
||||
* {name: 'Blog', params: {project: '{{ project._id }}', node_type: 'post'}},
|
||||
* {name: 'Groups', params: {project: '{{ project._id }}', node_type: 'group'}},
|
||||
* ]
|
||||
* },
|
||||
* cloud: {
|
||||
* name: 'Cloud',
|
||||
* uiUrl: '/search',
|
||||
* apiUrl: '/api/newsearch/multisearch',
|
||||
* searchParams: [
|
||||
* {name: 'Assets', params: {node_type: 'asset'}},
|
||||
* {name: 'Blog', params: {node_type: 'post'}},
|
||||
* {name: 'Groups', params: {node_type: 'group'}},
|
||||
* ]
|
||||
* },
|
||||
* },
|
||||
* });
|
||||
* @param {*} kwargs
|
||||
*/
|
||||
quickSearch: function (kwargs) {
|
||||
$(this).each((i, qsElem) => {
|
||||
new QuickSearch(qsElem, kwargs);
|
||||
});
|
||||
}
|
||||
})
|
68
src/scripts/js/es6/common/quicksearch/SearchFacade.js
Normal file
68
src/scripts/js/es6/common/quicksearch/SearchFacade.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import {MultiSearch} from './MultiSearch';
|
||||
|
||||
export class SearchFacade {
|
||||
/**
|
||||
* One SearchFacade holds n-number of MultiSearch objects, and delegates search requests to the active mutlisearch
|
||||
* @param {*} kwargs
|
||||
*/
|
||||
constructor(kwargs) {
|
||||
this.searches = SearchFacade.createMultiSearches(kwargs);
|
||||
this.currentScope = 'cloud'; // which multisearch to use
|
||||
this.currRequest;
|
||||
this.resultCB;
|
||||
this.failureCB;
|
||||
this.q = '';
|
||||
}
|
||||
|
||||
setSearchWord(q) {
|
||||
this.q = q;
|
||||
$.each(this.searches, (k, mSearch) => {
|
||||
mSearch.setSearchWord(q);
|
||||
});
|
||||
}
|
||||
|
||||
getSearchWord() {
|
||||
return this.q;
|
||||
}
|
||||
|
||||
getSearchUrl() {
|
||||
return this.searches[this.currentScope].getSearchUrl();
|
||||
}
|
||||
|
||||
setCurrentScope(scope) {
|
||||
this.currentScope = scope;
|
||||
}
|
||||
|
||||
execute() {
|
||||
if (this.currRequest) {
|
||||
this.currRequest.abort();
|
||||
}
|
||||
this.currRequest = this.searches[this.currentScope].thenExecute();
|
||||
this.currRequest
|
||||
.then((results) => {
|
||||
this.resultCB(results);
|
||||
})
|
||||
.fail((err, reason) => {
|
||||
if (reason == 'abort') {
|
||||
return;
|
||||
}
|
||||
this.failureCB(err);
|
||||
});
|
||||
}
|
||||
|
||||
setOnResultCB(cb) {
|
||||
this.resultCB = cb;
|
||||
}
|
||||
|
||||
setOnFailureCB(cb) {
|
||||
this.failureCB = cb;
|
||||
}
|
||||
|
||||
static createMultiSearches(kwargs) {
|
||||
var searches = {};
|
||||
$.each(kwargs, (key, value) => {
|
||||
searches[key] = new MultiSearch(value);
|
||||
});
|
||||
return searches;
|
||||
}
|
||||
}
|
14
src/scripts/js/es6/common/quicksearch/SearchParams.js
Normal file
14
src/scripts/js/es6/common/quicksearch/SearchParams.js
Normal file
@@ -0,0 +1,14 @@
|
||||
export class SearchParams {
|
||||
constructor(kwargs) {
|
||||
this.name = kwargs['name'] || '';
|
||||
this.params = kwargs['params'] || {};
|
||||
}
|
||||
|
||||
setSearchWord(q) {
|
||||
this.params['q'] = q || '';
|
||||
}
|
||||
|
||||
getParamStr() {
|
||||
return jQuery.param(this.params);
|
||||
}
|
||||
}
|
1
src/scripts/js/es6/common/quicksearch/init.js
Normal file
1
src/scripts/js/es6/common/quicksearch/init.js
Normal file
@@ -0,0 +1 @@
|
||||
export { QuickSearch } from './QuickSearch';
|
93
src/scripts/js/es6/common/quicksearch/templates.js
Normal file
93
src/scripts/js/es6/common/quicksearch/templates.js
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Creates the jQuery object that is rendered when nothing is found
|
||||
* @param {String} advancedUrl Url to the advanced search with the current query
|
||||
* @returns {$element} The jQuery element that is rendered wher there are no hits
|
||||
*/
|
||||
function create$noHits(advancedUrl) {
|
||||
return $('<div>')
|
||||
.addClass('qs-msg text-center p-3')
|
||||
.append(
|
||||
$('<div>')
|
||||
.addClass('h1 pi-displeased'),
|
||||
$('<div>')
|
||||
.addClass('h2')
|
||||
.append(
|
||||
$('<a>')
|
||||
.attr('href', advancedUrl)
|
||||
.text('Advanced search')
|
||||
)
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Creates the jQuery object that is rendered as the search input
|
||||
* @param {Dict} searches The searches dict that is passed in on construction of the Quick-Search
|
||||
* @returns {$element} The jQuery object that renders the search input components.
|
||||
*/
|
||||
function create$input(searches) {
|
||||
let input = $('<input>')
|
||||
.addClass('qs-input')
|
||||
.attr('type', 'search')
|
||||
.attr('autocomplete', 'off')
|
||||
.attr('spellcheck', 'false')
|
||||
.attr('autocorrect', 'false')
|
||||
.attr('placeholder', 'Search...');
|
||||
let workingSymbol = $('<i>')
|
||||
.addClass('pi-cancel qs-busy-symbol');
|
||||
let inputComponent = [input, workingSymbol];
|
||||
if (Object.keys(searches).length > 1) {
|
||||
let i = 0;
|
||||
let select = $('<select>')
|
||||
.append(
|
||||
$.map(searches, (it, value) => {
|
||||
let option = $('<option>')
|
||||
.attr('value', value)
|
||||
.text(it['name']);
|
||||
if (i === 0) {
|
||||
option.attr('selected', 'selected');
|
||||
}
|
||||
i += 1;
|
||||
return option;
|
||||
})
|
||||
);
|
||||
inputComponent.push(select);
|
||||
}
|
||||
return inputComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the search result
|
||||
* @param {List} results
|
||||
* @param {String} advancedUrl
|
||||
* @returns {$element} The jQuery object that is rendered as the result
|
||||
*/
|
||||
function create$results(results, advancedUrl) {
|
||||
let $results = results.reduce((agg, res)=> {
|
||||
if(res['result'].length) {
|
||||
agg.push(
|
||||
$('<a>')
|
||||
.addClass('h4 mt-4 d-flex')
|
||||
.attr('href', res['url'])
|
||||
.text(res['name'])
|
||||
)
|
||||
agg.push(
|
||||
$('<div>')
|
||||
.addClass('card-deck card-deck-responsive card-padless js-asset-list p-3')
|
||||
.append(
|
||||
...pillar.templates.Nodes.createListOf$nodeItems(res['result'], 10, 0)
|
||||
)
|
||||
)
|
||||
}
|
||||
return agg;
|
||||
}, [])
|
||||
$results.push(
|
||||
$('<a>')
|
||||
.attr('href', advancedUrl)
|
||||
.text('Advanced search...')
|
||||
)
|
||||
|
||||
return $('<div>')
|
||||
.addClass('m-auto qs-result')
|
||||
.append(...$results)
|
||||
}
|
||||
|
||||
export { create$noHits, create$results, create$input }
|
Reference in New Issue
Block a user