UX: move search to its own route
previously search was bundled with discovery, something that makes stuff confusing internally
This commit is contained in:
parent
68a262ff08
commit
41ceff8430
|
@ -0,0 +1,33 @@
|
||||||
|
import DiscourseController from 'discourse/controllers/controller';
|
||||||
|
import { translateResults } from 'discourse/lib/search-for-term';
|
||||||
|
|
||||||
|
export default DiscourseController.extend({
|
||||||
|
loading: Em.computed.not('model'),
|
||||||
|
queryParams: ['q'],
|
||||||
|
q: null,
|
||||||
|
modelChanged: function(){
|
||||||
|
if (this.get('searchTerm') !== this.get('q')) {
|
||||||
|
this.set('searchTerm', this.get('q'));
|
||||||
|
}
|
||||||
|
}.observes('model'),
|
||||||
|
|
||||||
|
qChanged: function(){
|
||||||
|
var model = this.get('model');
|
||||||
|
if (model && this.get('model.q') !== this.get('q')){
|
||||||
|
this.set('searchTerm', this.get('q'));
|
||||||
|
this.send('search');
|
||||||
|
}
|
||||||
|
}.observes('q'),
|
||||||
|
actions: {
|
||||||
|
search: function(){
|
||||||
|
var self = this;
|
||||||
|
this.set('q', this.get('searchTerm'));
|
||||||
|
this.set('model', null);
|
||||||
|
|
||||||
|
Discourse.ajax('/search2', {data: {q: this.get('searchTerm')}}).then(function(results) {
|
||||||
|
self.set('model', translateResults(results) || {});
|
||||||
|
self.set('model.q', self.get('q'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,5 +1,67 @@
|
||||||
import Topic from 'discourse/models/topic';
|
import Topic from 'discourse/models/topic';
|
||||||
|
|
||||||
|
export function translateResults(results, opts) {
|
||||||
|
if (!opts) opts = {};
|
||||||
|
|
||||||
|
// Topics might not be included
|
||||||
|
if (!results.topics) { results.topics = []; }
|
||||||
|
if (!results.users) { results.users = []; }
|
||||||
|
if (!results.posts) { results.posts = []; }
|
||||||
|
if (!results.categories) { results.categories = []; }
|
||||||
|
|
||||||
|
const topicMap = {};
|
||||||
|
results.topics = results.topics.map(function(topic){
|
||||||
|
topic = Topic.create(topic);
|
||||||
|
topicMap[topic.id] = topic;
|
||||||
|
return topic;
|
||||||
|
});
|
||||||
|
|
||||||
|
results.posts = results.posts.map(function(post){
|
||||||
|
post = Discourse.Post.create(post);
|
||||||
|
post.set('topic', topicMap[post.topic_id]);
|
||||||
|
return post;
|
||||||
|
});
|
||||||
|
|
||||||
|
results.users = results.users.map(function(user){
|
||||||
|
user = Discourse.User.create(user);
|
||||||
|
return user;
|
||||||
|
});
|
||||||
|
|
||||||
|
results.categories = results.categories.map(function(category){
|
||||||
|
return Discourse.Category.list().findProperty('id', category.id);
|
||||||
|
}).compact();
|
||||||
|
|
||||||
|
const r = results.grouped_search_result;
|
||||||
|
results.resultTypes = [];
|
||||||
|
|
||||||
|
// TODO: consider refactoring front end to take a better structure
|
||||||
|
[['topic','posts'],['user','users'],['category','categories']].forEach(function(pair){
|
||||||
|
const type = pair[0], name = pair[1];
|
||||||
|
if (results[name].length > 0) {
|
||||||
|
var result = {
|
||||||
|
results: results[name],
|
||||||
|
componentName: "search-result-" + ((opts.searchContext && opts.searchContext.type === 'topic' && type === 'topic') ? 'post' : type),
|
||||||
|
type,
|
||||||
|
more: r['more_' + name]
|
||||||
|
};
|
||||||
|
|
||||||
|
if (result.more && name === "posts" && opts.fullSearchUrl) {
|
||||||
|
result.more = false;
|
||||||
|
result.moreUrl = opts.fullSearchUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.resultTypes.push(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const noResults = !!(results.topics.length === 0 &&
|
||||||
|
results.posts.length === 0 &&
|
||||||
|
results.users.length === 0 &&
|
||||||
|
results.categories.length === 0);
|
||||||
|
|
||||||
|
return noResults ? null : Em.Object.create(results);
|
||||||
|
}
|
||||||
|
|
||||||
function searchForTerm(term, opts) {
|
function searchForTerm(term, opts) {
|
||||||
if (!opts) opts = {};
|
if (!opts) opts = {};
|
||||||
|
|
||||||
|
@ -16,63 +78,7 @@ function searchForTerm(term, opts) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Discourse.ajax('/search/query', { data: data }).then(function(results){
|
return Discourse.ajax('/search/query', { data: data }).then(function(results){
|
||||||
// Topics might not be included
|
return translateResults(results, opts);
|
||||||
if (!results.topics) { results.topics = []; }
|
|
||||||
if (!results.users) { results.users = []; }
|
|
||||||
if (!results.posts) { results.posts = []; }
|
|
||||||
if (!results.categories) { results.categories = []; }
|
|
||||||
|
|
||||||
const topicMap = {};
|
|
||||||
results.topics = results.topics.map(function(topic){
|
|
||||||
topic = Topic.create(topic);
|
|
||||||
topicMap[topic.id] = topic;
|
|
||||||
return topic;
|
|
||||||
});
|
|
||||||
|
|
||||||
results.posts = results.posts.map(function(post){
|
|
||||||
post = Discourse.Post.create(post);
|
|
||||||
post.set('topic', topicMap[post.topic_id]);
|
|
||||||
return post;
|
|
||||||
});
|
|
||||||
|
|
||||||
results.users = results.users.map(function(user){
|
|
||||||
user = Discourse.User.create(user);
|
|
||||||
return user;
|
|
||||||
});
|
|
||||||
|
|
||||||
results.categories = results.categories.map(function(category){
|
|
||||||
return Discourse.Category.list().findProperty('id', category.id);
|
|
||||||
}).compact();
|
|
||||||
|
|
||||||
const r = results.grouped_search_result;
|
|
||||||
results.resultTypes = [];
|
|
||||||
|
|
||||||
// TODO: consider refactoring front end to take a better structure
|
|
||||||
[['topic','posts'],['user','users'],['category','categories']].forEach(function(pair){
|
|
||||||
const type = pair[0], name = pair[1];
|
|
||||||
if (results[name].length > 0) {
|
|
||||||
var result = {
|
|
||||||
results: results[name],
|
|
||||||
componentName: "search-result-" + ((opts.searchContext && opts.searchContext.type === 'topic' && type === 'topic') ? 'post' : type),
|
|
||||||
type,
|
|
||||||
more: r['more_' + name]
|
|
||||||
};
|
|
||||||
|
|
||||||
if (result.more && name === "posts" && opts.fullSearchUrl) {
|
|
||||||
result.more = false;
|
|
||||||
result.moreUrl = opts.fullSearchUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
results.resultTypes.push(result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const noResults = !!(results.topics.length === 0 &&
|
|
||||||
results.posts.length === 0 &&
|
|
||||||
results.users.length === 0 &&
|
|
||||||
results.categories.length === 0);
|
|
||||||
|
|
||||||
return noResults ? null : Em.Object.create(results);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -99,4 +99,6 @@ export default function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.resource('queued-posts', { path: '/queued-posts' });
|
this.resource('queued-posts', { path: '/queued-posts' });
|
||||||
|
|
||||||
|
this.route('full-page-search', {path: '/search2'});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { translateResults } from 'discourse/lib/search-for-term';
|
||||||
|
|
||||||
|
export default Discourse.Route.extend({
|
||||||
|
queryParams: {
|
||||||
|
q: {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
model: function(params) {
|
||||||
|
return PreloadStore.getAndRemove("search", function() {
|
||||||
|
return Discourse.ajax('/search2', {data: {q: params.q}});
|
||||||
|
}).then(function(results){
|
||||||
|
var model = translateResults(results) || {};
|
||||||
|
model.q = params.q;
|
||||||
|
return model;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,38 @@
|
||||||
|
<div class="search row">
|
||||||
|
{{input type="text" value=searchTerm class="input-xxlarge search no-blur" action="search"}}
|
||||||
|
<button {{action "search"}} class="btn btn-primary"><i class='fa fa-search'></i></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#conditional-loading-spinner condition=loading}}
|
||||||
|
|
||||||
|
{{#unless model.posts}}
|
||||||
|
<h3>{{i18n "search.no_results"}} <a href class="show-help" {{action "showSearchHelp" bubbles=false}}>{{i18n "search.search_help"}}</a>
|
||||||
|
</h3>
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
|
{{#each model.posts as |result|}}
|
||||||
|
<div class='fps-result'>
|
||||||
|
<div class='topic'>
|
||||||
|
{{avatar result imageSize="tiny"}}
|
||||||
|
<a class='search-link' href='{{unbound result.url}}'>
|
||||||
|
{{topic-status topic=result.topic disableActions=true}}<span class='topic-title'>{{unbound result.topic.title}}</span>
|
||||||
|
</a>{{category-link result.topic.category}}
|
||||||
|
</div>
|
||||||
|
<div class='blurb container'>
|
||||||
|
{{format-age result.created_at}}{{#if result.blurb}}
|
||||||
|
–
|
||||||
|
{{{unbound result.blurb}}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
{{#if model.posts}}
|
||||||
|
<h3 class="search-footer">
|
||||||
|
{{i18n "search.no_more_results"}}
|
||||||
|
<a href class="show-help" {{action "showSearchHelp" bubbles=false}}>{{i18n "search.search_help"}}</a>
|
||||||
|
</h3>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{/conditional-loading-spinner}}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import ScrollTop from 'discourse/mixins/scroll-top';
|
||||||
|
|
||||||
|
export default Ember.View.extend(ScrollTop, {
|
||||||
|
|
||||||
|
_highlightOnInsert: function() {
|
||||||
|
const term = this.get('controller.q');
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
if(!_.isEmpty(term)) {
|
||||||
|
Em.run.next(function(){
|
||||||
|
self.$('.blurb').highlight(term.split(/\s+/), {className: 'search-highlight'});
|
||||||
|
self.$('.topic-title').highlight(term.split(/\s+/), {className: 'search-highlight'} );
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}.observes('controller.model').on('didInsertElement')
|
||||||
|
});
|
|
@ -0,0 +1,43 @@
|
||||||
|
.fps-result {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
max-width: 675px;
|
||||||
|
.topic {
|
||||||
|
a {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
.avatar {
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
.search-link {
|
||||||
|
.topic-statuses, .topic-title {
|
||||||
|
font-size: 1.15em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.blurb {
|
||||||
|
font-size: 1.0em;
|
||||||
|
line-height: 20px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
clear: both;
|
||||||
|
color: scale-color($primary, $lightness: 45%);
|
||||||
|
|
||||||
|
.search-highlight {
|
||||||
|
color: scale-color($primary, $lightness: 25%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search.row {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
input {
|
||||||
|
height: 22px;
|
||||||
|
padding-left: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-footer {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
|
@ -2,10 +2,30 @@ require_dependency 'search'
|
||||||
|
|
||||||
class SearchController < ApplicationController
|
class SearchController < ApplicationController
|
||||||
|
|
||||||
|
skip_before_filter :check_xhr, only: :show
|
||||||
|
|
||||||
def self.valid_context_types
|
def self.valid_context_types
|
||||||
%w{user topic category private_messages}
|
%w{user topic category private_messages}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
search = Search.new(params[:q], type_filter: 'topic', guardian: guardian, include_blurbs: true, blurb_length: 300)
|
||||||
|
result = search.execute
|
||||||
|
|
||||||
|
serializer = serialize_data(result, GroupedSearchResultSerializer, :result => result)
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
|
store_preloaded("search", MultiJson.dump(serializer))
|
||||||
|
end
|
||||||
|
|
||||||
|
format.json do
|
||||||
|
render_json_dump(serializer)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
def query
|
def query
|
||||||
params.require(:term)
|
params.require(:term)
|
||||||
|
|
||||||
|
|
|
@ -881,6 +881,8 @@ en:
|
||||||
search:
|
search:
|
||||||
title: "search topics, posts, users, or categories"
|
title: "search topics, posts, users, or categories"
|
||||||
no_results: "No results found."
|
no_results: "No results found."
|
||||||
|
no_more_results: "No more results found."
|
||||||
|
search_help: Search help
|
||||||
searching: "Searching ..."
|
searching: "Searching ..."
|
||||||
post_format: "#{{post_number}} by {{username}}"
|
post_format: "#{{post_number}} by {{username}}"
|
||||||
|
|
||||||
|
|
|
@ -405,6 +405,7 @@ Discourse::Application.routes.draw do
|
||||||
|
|
||||||
get "top" => "list#top"
|
get "top" => "list#top"
|
||||||
get "search/query" => "search#query"
|
get "search/query" => "search#query"
|
||||||
|
get "search2" => "search#show"
|
||||||
|
|
||||||
# Topics resource
|
# Topics resource
|
||||||
get "t/:id" => "topics#show"
|
get "t/:id" => "topics#show"
|
||||||
|
|
|
@ -99,6 +99,7 @@ class Search
|
||||||
@guardian = @opts[:guardian] || Guardian.new
|
@guardian = @opts[:guardian] || Guardian.new
|
||||||
@search_context = @opts[:search_context]
|
@search_context = @opts[:search_context]
|
||||||
@include_blurbs = @opts[:include_blurbs] || false
|
@include_blurbs = @opts[:include_blurbs] || false
|
||||||
|
@blurb_length = @opts[:blurb_length]
|
||||||
@limit = Search.per_facet
|
@limit = Search.per_facet
|
||||||
|
|
||||||
term = process_advanced_search!(term)
|
term = process_advanced_search!(term)
|
||||||
|
@ -116,7 +117,7 @@ class Search
|
||||||
@limit = Search.per_filter
|
@limit = Search.per_filter
|
||||||
end
|
end
|
||||||
|
|
||||||
@results = GroupedSearchResults.new(@opts[:type_filter], term, @search_context, @include_blurbs)
|
@results = GroupedSearchResults.new(@opts[:type_filter], term, @search_context, @include_blurbs, @blurb_length)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.execute(term, opts=nil)
|
def self.execute(term, opts=nil)
|
||||||
|
|
|
@ -14,18 +14,19 @@ class Search
|
||||||
:more_posts, :more_categories, :more_users,
|
:more_posts, :more_categories, :more_users,
|
||||||
:term, :search_context, :include_blurbs
|
:term, :search_context, :include_blurbs
|
||||||
|
|
||||||
def initialize(type_filter, term, search_context, include_blurbs)
|
def initialize(type_filter, term, search_context, include_blurbs, blurb_length)
|
||||||
@type_filter = type_filter
|
@type_filter = type_filter
|
||||||
@term = term
|
@term = term
|
||||||
@search_context = search_context
|
@search_context = search_context
|
||||||
@include_blurbs = include_blurbs
|
@include_blurbs = include_blurbs
|
||||||
|
@blurb_length = blurb_length
|
||||||
@posts = []
|
@posts = []
|
||||||
@categories = []
|
@categories = []
|
||||||
@users = []
|
@users = []
|
||||||
end
|
end
|
||||||
|
|
||||||
def blurb(post)
|
def blurb(post)
|
||||||
GroupedSearchResults.blurb_for(post.cooked, @term)
|
GroupedSearchResults.blurb_for(post.cooked, @term, @blurb_length)
|
||||||
end
|
end
|
||||||
|
|
||||||
def add(object)
|
def add(object)
|
||||||
|
@ -39,15 +40,15 @@ class Search
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def self.blurb_for(cooked, term=nil)
|
def self.blurb_for(cooked, term, blurb_length)
|
||||||
cooked = SearchObserver::HtmlScrubber.scrub(cooked).squish
|
cooked = SearchObserver::HtmlScrubber.scrub(cooked).squish
|
||||||
|
|
||||||
blurb = nil
|
blurb = nil
|
||||||
if term
|
if term
|
||||||
terms = term.split(/\s+/)
|
terms = term.split(/\s+/)
|
||||||
blurb = TextHelper.excerpt(cooked, terms.first, radius: 100)
|
blurb = TextHelper.excerpt(cooked, terms.first, radius: blurb_length / 2, seperator: " ")
|
||||||
end
|
end
|
||||||
blurb = TextHelper.truncate(cooked, length: 200) if blurb.blank?
|
blurb = TextHelper.truncate(cooked, length: blurb_length, seperator: " ") if blurb.blank?
|
||||||
Sanitize.clean(blurb)
|
Sanitize.clean(blurb)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue