mirror of
https://github.com/discourse/discourse.git
synced 2024-11-26 10:50:26 -06:00
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';
|
||||
|
||||
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) {
|
||||
if (!opts) opts = {};
|
||||
|
||||
@ -16,63 +78,7 @@ function searchForTerm(term, opts) {
|
||||
}
|
||||
|
||||
return Discourse.ajax('/search/query', { data: data }).then(function(results){
|
||||
// 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);
|
||||
return translateResults(results, opts);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -99,4 +99,6 @@ export default function() {
|
||||
});
|
||||
|
||||
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')
|
||||
});
|
43
app/assets/stylesheets/common/base/search.scss
Normal file
43
app/assets/stylesheets/common/base/search.scss
Normal 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;
|
||||
}
|
@ -2,10 +2,30 @@ require_dependency 'search'
|
||||
|
||||
class SearchController < ApplicationController
|
||||
|
||||
skip_before_filter :check_xhr, only: :show
|
||||
|
||||
def self.valid_context_types
|
||||
%w{user topic category private_messages}
|
||||
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
|
||||
params.require(:term)
|
||||
|
||||
|
0
app/views/search/show.html.erb
Normal file
0
app/views/search/show.html.erb
Normal file
@ -881,6 +881,8 @@ en:
|
||||
search:
|
||||
title: "search topics, posts, users, or categories"
|
||||
no_results: "No results found."
|
||||
no_more_results: "No more results found."
|
||||
search_help: Search help
|
||||
searching: "Searching ..."
|
||||
post_format: "#{{post_number}} by {{username}}"
|
||||
|
||||
|
@ -405,6 +405,7 @@ Discourse::Application.routes.draw do
|
||||
|
||||
get "top" => "list#top"
|
||||
get "search/query" => "search#query"
|
||||
get "search2" => "search#show"
|
||||
|
||||
# Topics resource
|
||||
get "t/:id" => "topics#show"
|
||||
|
@ -99,6 +99,7 @@ class Search
|
||||
@guardian = @opts[:guardian] || Guardian.new
|
||||
@search_context = @opts[:search_context]
|
||||
@include_blurbs = @opts[:include_blurbs] || false
|
||||
@blurb_length = @opts[:blurb_length]
|
||||
@limit = Search.per_facet
|
||||
|
||||
term = process_advanced_search!(term)
|
||||
@ -116,7 +117,7 @@ class Search
|
||||
@limit = Search.per_filter
|
||||
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
|
||||
|
||||
def self.execute(term, opts=nil)
|
||||
|
@ -14,18 +14,19 @@ class Search
|
||||
:more_posts, :more_categories, :more_users,
|
||||
: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
|
||||
@term = term
|
||||
@search_context = search_context
|
||||
@include_blurbs = include_blurbs
|
||||
@blurb_length = blurb_length
|
||||
@posts = []
|
||||
@categories = []
|
||||
@users = []
|
||||
end
|
||||
|
||||
def blurb(post)
|
||||
GroupedSearchResults.blurb_for(post.cooked, @term)
|
||||
GroupedSearchResults.blurb_for(post.cooked, @term, @blurb_length)
|
||||
end
|
||||
|
||||
def add(object)
|
||||
@ -39,15 +40,15 @@ class Search
|
||||
end
|
||||
|
||||
|
||||
def self.blurb_for(cooked, term=nil)
|
||||
def self.blurb_for(cooked, term, blurb_length)
|
||||
cooked = SearchObserver::HtmlScrubber.scrub(cooked).squish
|
||||
|
||||
blurb = nil
|
||||
if term
|
||||
terms = term.split(/\s+/)
|
||||
blurb = TextHelper.excerpt(cooked, terms.first, radius: 100)
|
||||
blurb = TextHelper.excerpt(cooked, terms.first, radius: blurb_length / 2, seperator: " ")
|
||||
end
|
||||
blurb = TextHelper.truncate(cooked, length: 200) if blurb.blank?
|
||||
blurb = TextHelper.truncate(cooked, length: blurb_length, seperator: " ") if blurb.blank?
|
||||
Sanitize.clean(blurb)
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user