UX: move search to its own route

previously search was bundled with discovery, something that makes stuff confusing internally
This commit is contained in:
Sam 2015-07-27 16:13:11 +10:00
parent 68a262ff08
commit 41ceff8430
13 changed files with 244 additions and 63 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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}}
&ndash;
{{{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}}

View File

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

View File

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

View File

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

View File

View File

@ -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}}"

View File

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

View File

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

View File

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